關(guān)于網(wǎng)站身份驗(yàn)證
http協(xié)議被設(shè)計(jì)為無(wú)連接協(xié)議,但現(xiàn)實(shí)中,很多網(wǎng)站需要對(duì)用戶(hù)進(jìn)行身份識(shí)別,cookie就是為此而誕生的。當(dāng)我們用瀏覽器瀏覽網(wǎng)站時(shí),瀏覽器會(huì)幫我們透明的處理cookie。而我們現(xiàn)在要第三方登錄網(wǎng)站,這就必須對(duì)cookie的工作流程有一定的了解。
另外,很多網(wǎng)站為了防止程序自動(dòng)登錄而使用了驗(yàn)證碼機(jī)制,驗(yàn)證碼的介入會(huì)使登錄過(guò)程變得麻煩,但也還不算太難處理。
實(shí)際中douban.fm的登錄流程
為了模擬一個(gè)干凈(不使用已有cookie)的登錄流程,我使用chromium的隱身模式。
觀察請(qǐng)求和響應(yīng)頭,可以看到,第一次請(qǐng)求的請(qǐng)求頭是沒(méi)有Cookie字段的,而服務(wù)器的響應(yīng)頭中包含著Set-Cookie字段,這告訴瀏覽器下次請(qǐng)求該網(wǎng)站時(shí)需要攜帶Cookie。
這里我注意到了一個(gè)有意思的現(xiàn)象,訪問(wèn)douban.fm,實(shí)際中經(jīng)過(guò)了3次重定向。當(dāng)然,一般來(lái)說(shuō)我們并不需要關(guān)注這些細(xì)節(jié),瀏覽器和高級(jí)的httplib會(huì)透明的處理重定向,但如果使用底層的C Socket,就必須小心的處理這些重定向。
點(diǎn)擊登錄按鈕,瀏覽器發(fā)起幾個(gè)新的請(qǐng)求,其中有幾個(gè)至關(guān)重要的請(qǐng)求,這幾個(gè)請(qǐng)求是我們第三方登錄douban.fm的關(guān)鍵所在。
首先,有一條請(qǐng)求的URL是http://douban.fm/j/new_captcha,請(qǐng)求該URL,服務(wù)器會(huì)返回一個(gè)隨機(jī)字符串,這有什么用呢?(其實(shí)是個(gè)驗(yàn)證碼)
再看下一條請(qǐng)求,http://douban.fm/misc/captcha?size=m&id=0iPlm837LsnSsJTMJrf5TZ7e,這條請(qǐng)求會(huì)返回驗(yàn)證碼。原來(lái)如此,請(qǐng)求http://douban.fm/j/new_captcha,將服務(wù)器返回的字符串作為下一條請(qǐng)求的id參數(shù)值。
我們可以寫(xiě)一段python代碼來(lái)驗(yàn)證我們的想法。
值得注意的是python提供了3個(gè)http庫(kù),httplib、urllib和urllib2,能透明處理cookie的是urllib2,想我之前用httplib手動(dòng)處理cookie,那個(gè)痛苦啊。
代碼如下:
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()
這段代碼實(shí)現(xiàn)了驗(yàn)證碼的下載。
接著,我們填寫(xiě)表單,并提交。
可以看到,登錄表單的目標(biāo)地址為http://douban.fm/j/login,參數(shù)有:
source: radio
alias: 用戶(hù)名
form_password: 密碼
captcha_solution: 驗(yàn)證碼
captcha_id: 驗(yàn)證碼ID
task: sync_channel_list
接下來(lái)要做的是用python構(gòu)造一個(gè)表單。
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'}))
服務(wù)器返回的數(shù)據(jù)格式是json,具體格式這里不贅訴了,大家可以自己測(cè)試。
我們?cè)趺粗赖卿浭欠衿鹱饔昧四兀渴橇耍暗奈恼绿岬竭^(guò)channel=-3為紅心兆赫,是用戶(hù)的收藏列表,沒(méi)有登錄是獲取不到該頻道的播放列表的。請(qǐng)求http://douban.fm/j/mine/playlist?type=n&channel=-3,如果返回你自己收藏過(guò)的音樂(lè)列表,那么就說(shuō)明登錄起作用了。
代碼整理
結(jié)合之前的版本和新增的登錄功能,再加上命令行參數(shù)處理、頻道選擇,一個(gè)稍稍完善的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()) # 發(fā)送桌面通知 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 '正在獲取驗(yàn)證碼……' captcha_id = opener.open(urllib2.Request( 'http://douban.fm/j/new_captcha')).read().strip('"') save( '驗(yàn)證碼.jpg', opener.open(urllib2.Request( 'http://douban.fm/misc/captcha?size=m&id=' + captcha_id )).read()) captcha = raw_input('驗(yàn)證碼: ') 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] # 下載專(zhuān)輯封面 save( picture, urllib.urlopen(song['picture']).read()) # 發(fā)送桌面通知 notifySend( picture, song['title'], song['artist'] + ' ' + song['albumtitle']) # 播放 player = subprocess.Popen(['mplayer', song['url']]) time.sleep(song['length']) player.kill() def main(argv): # 默認(rèn)參數(shù) channel = '0' user = '' password = '' # 獲取、解析命令行參數(shù) try: opts, args = getopt.getopt( argv, 'u:p:c:', ['user=', 'password=', 'channel=']) except getopt.GetoptError as error: print str(error) sys.exit(1) # 命令行參數(shù)處理 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:])
聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問(wèn)題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com