一個月前心血來潮用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_solution: 驗證碼
- captcha_id: 驗證碼ID
- task: 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'}))
服務器返回的數據格式是json,具體格式這里不贅訴了,大家可以自己測試。
我們怎么知道登錄是否起作用了呢?是了,之前的文章提到過channel=-3為紅心兆赫,是用戶的收藏列表,沒有登錄是獲取不到該頻道的播放列表的。請求http://douban.fm/j/mine/playlist?type=n&channel=-3,如果返回你自己收藏過的音樂列表,那么就說明登錄起作用了。
代碼整理
結合之前的版本和新增的登錄功能,再加上命令行參數處理、頻道選擇,一個稍稍完善的douban.fm就完成的。

1 #!/usr/bin/python 2 # coding: utf-8 3 4 import sys 5 import os 6 import subprocess 7 import getopt 8 import time 9 import json 10 import urllib 11 import urllib2 12 import getpass 13 import ConfigParser 14 from cookielib import CookieJar 15 16 # 保存到文件 17 def save(filename, content): 18 file = open(filename, 'wb') 19 file.write(content) 20 file.close() 21 22 23 # 獲取播放列表 24 def getPlayList(channel='0', opener=None): 25 url = 'http://douban.fm/j/mine/playlist?type=n&channel=' + channel 26 if opener == None: 27 return json.loads(urllib.urlopen(url).read()) 28 else: 29 return json.loads(opener.open(urllib2.Request(url)).read()) 30 31 32 # 發送桌面通知 33 def notifySend(picture, title, content): 34 subprocess.call([ 35 'notify-send', 36 '-i', 37 os.getcwd() + '/' + picture, 38 title, 39 content]) 40 41 42 # 登錄douban.fm 43 def login(username, password): 44 opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(CookieJar())) 45 while True: 46 print '正在獲取驗證碼……' 47 captcha_id = opener.open(urllib2.Request( 48 'http://douban.fm/j/new_captcha')).read().strip('"') 49 save( 50 '驗證碼.jpg', 51 opener.open(urllib2.Request( 52 'http://douban.fm/misc/captcha?size=m&id=' + captcha_id 53 )).read()) 54 captcha = raw_input('驗證碼: ') 55 print '正在登錄……' 56 response = json.loads(opener.open( 57 urllib2.Request('http://douban.fm/j/login'), 58 urllib.urlencode({ 59 'source': 'radio', 60 'alias': username, 61 'form_password': password, 62 'captcha_solution': captcha, 63 'captcha_id': captcha_id, 64 'task': 'sync_channel_list'})).read()) 65 if 'err_msg' in response.keys(): 66 print response['err_msg'] 67 else: 68 print '登錄成功' 69 return opener 70 71 72 # 播放douban.fm 73 def play(channel='0', opener=None): 74 while True: 75 if opener == None: 76 playlist = getPlayList(channel) 77 else: 78 playlist = getPlayList(channel, opener) 79 80 if playlist['song'] == []: 81 print '獲取播放列表失敗' 82 break 83 picture, 84 85 for song in playlist['song']: 86 picture = 'picture/' + song['picture'].split('/')[-1] 87 88 # 下載專輯封面 89 save( 90 picture, 91 urllib.urlopen(song['picture']).read()) 92 93 # 發送桌面通知 94 notifySend( 95 picture, 96 song['title'], 97 song['artist'] + '\n' + song['albumtitle']) 98 99 # 播放 100 player = subprocess.Popen(['mplayer', song['url']]) 101 time.sleep(song['length']) 102 player.kill() 103 104 105 def main(argv): 106 # 默認參數 107 channel = '0' 108 user = '' 109 password = '' 110 111 # 獲取、解析命令行參數 112 try: 113 opts, args = getopt.getopt( 114 argv, 'u:p:c:', ['user=', 'password=', 'channel=']) 115 except getopt.GetoptError as error: 116 print str(error) 117 sys.exit(1) 118 119 # 命令行參數處理 120 for opt, arg in opts: 121 if opt in ('-u', '--user='): 122 user = arg 123 elif opt in ('-p', '--password='): 124 password = arg 125 elif opt in ('-c', '--channel='): 126 channel = arg 127 128 if user == '': 129 play(channel) 130 else: 131 if password == '': 132 password = getpass.getpass('密碼:') 133 opener = login(user, password) 134 play(channel, opener) 135 136 137 if __name__ == '__main__': 138 main(sys.argv[1:])
以下是本人使用自己的帳號登錄並播放紅心兆赫:
接下來,我會繼續完善這個python douban.fm客戶端程序,添加頻道搜索和查看,全局按鍵控制等功能。