首頁 >後端開發 >Python教學 >python實作douban.fm簡易客戶端

python實作douban.fm簡易客戶端

高洛峰
高洛峰原創
2016-10-18 14:28:341374瀏覽

一個月前心血來潮用python實現了一個簡單的douban.fm客戶端,計劃是陸續將其完善成為Ubuntu下可替代web版本的douban.fm客戶端。但後來因為事多,被一直擱著,沒有再繼續完善。就在昨天,一位園友在評論中提到了登入的實現,雖然最近依然事多,但突然很想實現這個功能。剛好,前幾天因為一些需要,曾用python實現過網站登錄,約摸估計這douban.fm的登入不會差太多。


關於網站身份驗證

http協議被設計為無連接協議,但現實中,許多網站需要對用戶進行身份識別,cookie就是為此而誕生的。當我們用瀏覽器瀏覽網站時,瀏覽器會幫我們透明的處理cookie。而我們現在要第三方登入網站,這就必須對cookie的工作流程有一定的了解。


另外,很多網站為了防止程式自動登入而使用了驗證碼機制,驗證碼的介入會使登入過程變得麻煩,但也還不算太難處理。


實際中douban.fm的登入流程

為了模擬一個乾淨(不使用已有cookie)的登入流程,我使用chromium的隱身模式。

python實作douban.fm簡易客戶端

觀察請求和響應頭,可以看到,第一次請求的請求頭是沒有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: 驗證碼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
 #!/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:])

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn