私は 1 か月前に思いつきで Python を使用して単純な douban.fm クライアントを実装しました。計画は、これを段階的に改良して、Ubuntu 上の Web バージョンを置き換えることができるようにすることです。しかし、その後、やるべきことが多すぎたために保留になり、それ以上改善されることはありませんでした。昨日、庭の友人がコメントでログインの実装について言及しましたが、最近まだやるべきことがたくさんありますが、突然この機能を実装したいと考えました。たまたまですが、数日前、必要に応じて Python を使用して Web サイトへのログインを実装しました。douban.fm のログインはそれほど変わらないと思います。
Webサイトの認証について
httpプロトコルはコネクションレス型のプロトコルとして設計されていますが、実際には多くのWebサイトでユーザーを識別する必要があり、Cookieはその目的のために生まれています。私たちがブラウザを使用して Web サイトを閲覧すると、ブラウザは Cookie を透過的に処理します。第三者が Web サイトにログインする必要があるため、Cookie のワークフローをある程度理解する必要があります。
さらに、多くの Web サイトでは、プログラムによる自動ログインを防ぐために、認証コードの仕組みが使用されています。認証コードの介入により、ログインプロセスが煩雑になりますが、対処するのはそれほど難しいことではありません。
douban.fmの実際のログインプロセス
クリーンな(既存のCookieを使用しない)ログインプロセスをシミュレートするために、chromiumのシークレットモードを使用します。
リクエストヘッダーとレスポンスヘッダーを観察すると、最初のリクエストのリクエストヘッダーには Cookie フィールドがなく、サーバーのレスポンスヘッダーには Set-Cookie フィールドが含まれており、ブラウザーに Web サイトをリクエストするように指示していることがわかります。次回からは Cookie が必要になります。
ここで興味深い現象に気づきました。douban.fm にアクセスしたとき、実際に 3 つのリダイレクトが発生しました。もちろん、一般にこれらの詳細に注意を払う必要はありません。ブラウザと高度な httplib はリダイレクトを透過的に処理しますが、基礎となる C ソケットを使用する場合は、これらのリダイレクトを慎重に処理する必要があります。
ログイン ボタンをクリックすると、ブラウザはいくつかの重要なリクエストを含むいくつかの新しいリクエストを開始します。これらのリクエストは、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 には httplib、urllib、urllib2 という 3 つの http ライブラリが用意されていることに注意してください。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: username
form_password: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'}))
サーバーから返されるデータ形式はここでは説明しません。自分でテストできます。
ログインが機能しているかどうかはどうやって確認できますか?はい、前の記事で、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:])