한 달 전에 임의로 Python을 사용하여 간단한 douban.fm 클라이언트를 구현했습니다. 우분투에서 웹 버전을 대체할 수 있는 douban.fm 클라이언트로 점진적으로 개선할 계획입니다. 하지만 나중에 할 일이 너무 많아서 보류하고 더 이상 개선되지 않았습니다. 바로 어제, 정원 친구가 댓글로 로그인 구현에 대해 언급했습니다. 최근에도 할 일이 많지만 갑자기 이 기능을 구현하고 싶어졌습니다. 며칠 전 몇 가지 요구 사항으로 인해 Python을 사용하여 웹 사이트 로그인을 구현했습니다. douban.fm의 로그인은 크게 다르지 않을 것으로 예상됩니다.
웹사이트 인증에 대하여
http 프로토콜은 비연결형 프로토콜로 설계되어 있으나 실제로는 많은 웹사이트에서 이용자 식별이 필요하며, 쿠키는 다음 목적을 위해 사용됩니다. 이것으로부터 탄생했습니다. 우리가 웹사이트를 탐색하기 위해 브라우저를 사용할 때, 브라우저는 우리를 위해 쿠키를 투명하게 처리합니다. 이제 웹사이트에 로그인하려면 제3자가 필요하므로 쿠키 작업 흐름을 어느 정도 이해해야 합니다.
또한 많은 웹사이트에서는 프로그램이 자동으로 로그인하는 것을 방지하기 위해 인증코드 메커니즘을 사용하고 있습니다. 인증코드를 개입시키면 로그인 과정이 번거로워지지만, 별로 어렵지 않습니다.
douban.fm의 실제 로그인 프로세스
깨끗한(기존 쿠키를 사용하지 않는) 로그인 프로세스를 시뮬레이션하기 위해 chromium의 시크릿 모드를 사용합니다.
요청 및 응답 헤더를 관찰하면 첫 번째 요청의 요청 헤더에 Cookie 필드가 없고 서버의 응답 헤더에 Set-Cookie가 포함되어 있음을 알 수 있습니다. 필드는 브라우저가 다음에 웹사이트를 요청할 때 쿠키를 전달하도록 지시합니다.
여기서 흥미로운 현상을 발견했습니다. douban.fm을 방문했을 때 실제로 세 번의 리디렉션을 거쳤습니다. 물론 일반적으로 말하면 이러한 세부 사항에 주의를 기울일 필요는 없습니다. 브라우저와 고급 httplib는 리디렉션을 투명하게 처리하지만 기본 C 소켓을 사용하는 경우 이러한 리디렉션을 주의 깊게 처리해야 합니다.
로그인 버튼을 클릭하면 브라우저는 몇 가지 중요한 요청을 포함하여 몇 가지 새로운 요청을 시작합니다. 이러한 요청은 douban.fm에 대한 제3자 로그인을 위한 것입니다.
우선, 요청한 URL인 http://douban.fm/j/new_captcha가 있습니다. 이 URL을 요청하면 서버가 임의의 문자열을 반환합니다. 요점은 무엇입니까? (사실은 인증코드입니다)
다음 요청인 http://douban.fm/misc/captcha?size=m&id=0iPlm837LsnSsJTMJrf5TZ7e를 보면 이 요청이 인증코드를 반환해줍니다. http://douban.fm/j/new_captcha를 요청하고 서버에서 반환된 문자열을 다음 요청의 id 매개변수 값으로 사용하는 것으로 나타났습니다.
파이썬 코드를 작성하여 아이디어를 검증할 수 있습니다.
파이썬에서 쿠키를 투명하게 처리할 수 있는 라이브러리는 httplib, urllib, urllib2 세 가지를 제공한다는 점에 주목할 필요가 있습니다. 쿠키. 그 고통.
코드는 다음과 같습니다.
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(CookieJar())) captcha_id = opener.open(urllib2.Request('http://douban.fm/j/new_captcha')).read().strip('"') captcha = opener.open(urllib2.Request('http://douban.fm/misc/captcha?size=m&id=' + captcha_id)).read()) file = open('captcha.jpg', 'wb') file = write(captcha) file.close()
이 코드는 인증코드 다운로드를 구현합니다.
다음으로 양식을 작성하여 제출합니다.
보시다시피 로그인 양식의 대상 주소는 http://douban.fm/j/login이고 매개변수는
소스: radio
alias: 사용자 이름 form_password: 비밀번호captcha_solution: 인증 코드captcha_id: 인증 코드 IDtask: sync_channel_listWhat 다음에 할 일 Python을 사용하여 양식을 구성합니다.opener.open( urllib2.Request('http://douban.fm/j/login'), urllib.urlencode({ 'source': 'radio', 'alias': username, 'form_password': password, 'captcha_solution': captcha, 'captcha_id': captcha_id, 'task': 'sync_channel_list'}))서버에서 반환하는 데이터 형식은 json입니다. 구체적인 형식은 여기서 설명하지 않습니다.
View Code #!/usr/bin/python # coding: utf-8 import sys import os import subprocess import getopt import time import json import urllib import urllib2 import getpass import ConfigParser from cookielib import CookieJar # 保存到文件 def save(filename, content): file = open(filename, 'wb') file.write(content) file.close() # 获取播放列表 def getPlayList(channel='0', opener=None): url = 'http://douban.fm/j/mine/playlist?type=n&channel=' + channel if opener == None: return json.loads(urllib.urlopen(url).read()) else: return json.loads(opener.open(urllib2.Request(url)).read()) # 发送桌面通知 def notifySend(picture, title, content): subprocess.call([ 'notify-send', '-i', os.getcwd() + '/' + picture, title, content]) # 登录douban.fm def login(username, password): opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(CookieJar())) while True: print '正在获取验证码……' captcha_id = opener.open(urllib2.Request( 'http://douban.fm/j/new_captcha')).read().strip('"') save( '验证码.jpg', opener.open(urllib2.Request( 'http://douban.fm/misc/captcha?size=m&id=' + captcha_id )).read()) captcha = raw_input('验证码: ') print '正在登录……' response = json.loads(opener.open( urllib2.Request('http://douban.fm/j/login'), urllib.urlencode({ 'source': 'radio', 'alias': username, 'form_password': password, 'captcha_solution': captcha, 'captcha_id': captcha_id, 'task': 'sync_channel_list'})).read()) if 'err_msg' in response.keys(): print response['err_msg'] else: print '登录成功' return opener # 播放douban.fm def play(channel='0', opener=None): while True: if opener == None: playlist = getPlayList(channel) else: playlist = getPlayList(channel, opener) if playlist['song'] == []: print '获取播放列表失败' break picture, for song in playlist['song']: picture = 'picture/' + song['picture'].split('/')[-1] # 下载专辑封面 save( picture, urllib.urlopen(song['picture']).read()) # 发送桌面通知 notifySend( picture, song['title'], song['artist'] + '\n' + song['albumtitle']) # 播放 player = subprocess.Popen(['mplayer', song['url']]) time.sleep(song['length']) player.kill() def main(argv): # 默认参数 channel = '0' user = '' password = '' # 获取、解析命令行参数 try: opts, args = getopt.getopt( argv, 'u:p:c:', ['user=', 'password=', 'channel=']) except getopt.GetoptError as error: print str(error) sys.exit(1) # 命令行参数处理 for opt, arg in opts: if opt in ('-u', '--user='): user = arg elif opt in ('-p', '--password='): password = arg elif opt in ('-c', '--channel='): channel = arg if user == '': play(channel) else: if password == '': password = getpass.getpass('密码:') opener = login(user, password) play(channel, opener) if __name__ == '__main__': main(sys.argv[1:])