一個月前心血來潮用python實現了一個簡單的douban.fm客戶端,計劃是陸續將其完善成為Ubuntu下可替代web版本的douban.fm客戶端。但後來因為事多,被一直擱著,沒有再繼續完善。就在昨天,一位園友在評論中提到了登入的實現,雖然最近依然事多,但突然很想實現這個功能。剛好,前幾天因為一些需要,曾用python實現過網站登錄,約摸估計這douban.fm的登入不會差太多。
關於網站身份驗證
http協議被設計為無連接協議,但現實中,許多網站需要對用戶進行身份識別,cookie就是為此而誕生的。當我們用瀏覽器瀏覽網站時,瀏覽器會幫我們透明的處理cookie。而我們現在要第三方登入網站,這就必須對cookie的工作流程有一定的了解。
另外,很多網站為了防止程式自動登入而使用了驗證碼機制,驗證碼的介入會使登入過程變得麻煩,但也還不算太難處理。
實際中douban.fm的登入流程
為了模擬一個乾淨(不使用已有cookie)的登入流程,我使用chromium的隱身模式。
觀察請求和響應頭,可以看到,第一次請求的請求頭是沒有Cookie字段的,而伺服器的響應頭中包含著Set-Cookie字段,這告訴瀏覽器下次請求該網站時需攜帶Cookie。
這裡我注意到了一個有意思的現象,訪問douban.fm,實際中經過了3次重定向。當然,一般來說我們並不需要注意這些細節,瀏覽器和進階的httplib會透明的處理重定向,但如果使用底層的C Socket,就必須小心的處理這些重定向。
點擊登入按鈕,瀏覽器發起幾個新的請求,其中有幾個至關重要的請求,這幾個請求是我們第三方登入douban.fm的關鍵所在。
首先,有一條請求的URL是http://douban.fm/j/new_captcha,請求該URL,伺服器會回傳一個隨機字串,這有什麼用呢? (其實是驗證碼)
再看下一條請求,http://douban.fm/misc/captcha?size=m&id=0iPlm837LsnSsJTMJrf5TZ7e,這條請求會回傳驗證碼。原來如此,請求http://douban.fm/j/new_captcha,將伺服器傳回的字串作為下一請求的id參數值。
我們可以寫一段python程式碼來驗證我們的想法。
值得注意的是python提供了3個http函式庫,httplib、urllib和urllib2,能透明處理cookie的是urllib2,想我之前用httplib手動處理cookie,那個痛苦啊。
程式碼如下:
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,參數有:
source: radio
alias: 使用者名稱
form_password: 密碼
captcha_id: 驗證碼IDtask: sync_channel_list接下來要做的是用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'}))
我們怎麼知道登入是否運作了呢?是了,之前的文章提到過channel=-3為紅心兆赫,是用戶的收藏列表,沒有登入是獲取不到該頻道的播放列表的。請求http://douban.fm/j/mine/playlist?type=n&channel=-3,如果回傳你自己收藏過的音樂列表,那麼就表示登入起作用了。
程式碼整理
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:])