python實現的douban.fm客戶端,添加登錄功能


一個月前心血來潮用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就完成的。

View Code
  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客戶端程序,添加頻道搜索和查看,全局按鍵控制等功能。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM