2015年我觸及 Python 的那時候,就得聞 Python 的網絡編程能力非常強悍。因而,在了解 Python 的基礎語法以后,我就和幾個小伙伴們一塊兒協作,試著用 Python 的 urllib 和 urllib2 庫構建了一個百度貼吧 Python 客戶端。
然而,應用的過程中,我發現了兩個標準庫的語法并不自然,以至于可以說非常反人類——用著很不舒服。又有,我平日應用 Python 甚少涉及網絡編程的內容。因而,Python 的網絡編程就被我放下了,直至我了解了 requests 庫。
初次了解 requests
requests 庫的宣言是
HTTP for Humans (給人用的 HTTP 庫)
我們最先來檢驗一下。
在網絡編程中,最為基礎的任務包括:
發送請求
登入
獲取數據
解析數據
反序列化重新打印獲得的內容
我們以 GitHub 為例,先看一下應用 urllib2 要怎樣做。因為要把事兒弄簡單,我們假設實現已經知道,GET 請求 https://api.github.com/ 返回的內容是個 JSON 格式的數據(事實上通過 content-type 也可以判定)。
import urllib2
import json
gh_url = 'https://api.github.com'
cs_user = 'user'
cs_psw = 'password'
req = urllib2.Request(gh_url)
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(None, gh_url, cs_user, cs_psw)
auth_manager = urllib2.HTTPBasicAuthHandler(password_manager)
opener = urllib2.build_opener(auth_manager)
urllib2.install_opener(opener)
handler = urllib2.urlopen(req)
if handler.getcode() == requests.codes.ok:
text = handler.read()
d_text = json.loads(text)
for k, v in d_text.items():
print k, v
如果運行正確,那么代碼應該返回:
issues_url https://api.github.com/issues
current_user_repositories_url https://api.github.com/user/repos{?type,page,per_page,sort}
rate_limit_url https://api.github.com/rate_limit
repository_url https://api.github.com/repos/{owner}/{repo}
...
user_repositories_url https://api.github.com/users/{user}/repos{?type,page,per_page,sort}
team_url https://api.github.com/teams
一樣的作用,用 requests 庫則有如下代碼:
import requests
cs_url = 'https://api.github.com'
cs_user = 'user'
cs_psw = 'password'
r = requests.get(cs_url, auth=(cs_user, cs_psw))
if r.status_code == requests.codes.ok
for k, v in r.json().items():
print k, v
溢美之詞就不比說了,讀完在這里的你內心一定會只有一聲「握草,這才算 Python 該有的模樣」。那么,接下去讓我們看一下 requests 還有哪幾種黑魔法。
安裝
最受歡迎的方式,是直接安裝推薦過的 Anaconda。
當然如果你不愿安裝 Anaconda,那我建議你采用 pip 安裝;只需在命令行下執行:
pip install requests
基本用法
requests 的主要使用方法,呃,簡直不能再主要了。最主要的操作方法,就是說以某種 HTTP 方式向遠端服務器發送1個請求不過如此;而 requests 庫就是這樣做的:
import requests
cs_url = 'http://httpbin.org'
r = requests.get("%s/%s" % (cs_url, 'get'))
r = requests.post("%s/%s" % (cs_url, 'post'))
r = requests.put("%s/%s" % (cs_url, 'put'))
r = requests.delete("%s/%s" % (cs_url, 'delete'))
r = requests.patch("%s/%s" % (cs_url, 'patch'))
r = requests.options("%s/%s" % (cs_url, 'get'))
從語法上看,requests 庫設計構思的十分自然。說白了 requests.get,就是說以 GET 方式發送1個 REQUEST,得到1個 Response 類的結果,儲存為 r。
你能在 r 中獲得全部你想要的和 HTTP 有關的信息。下面,讓我們以 GET 方式為例,先后詳細介紹。
URL 傳參 / 獲得請求的 URL
如果要是你常常上網(屁話,看到這兒的都上過網吧……),一定會見過相似下邊的鏈接:
https://encrypted.google.com/search?q=hello
即:
<協議>://<網站域名>/<接口>?<鍵1>=<值1>&<鍵2>=<值2>
requests 庫給出的 HTTP 方式方法,都提供了名為 params 的基本參數。這一基本參數能夠接收1個 Python 字典,并自動格式化為上述文件格式。
import requests
cs_url = 'http://www.so.com/s'
param = {'ie':'utf-8', 'q':'query'}
r = requests.get (cs_url, params = param)
print r.url
執行將獲得:
http://www.so.com/s?q=query&ie=utf-8
HTTP 情況碼 / 重定向跳轉
requests 庫定義的 Response 類能夠便捷地獲得請求的 HTTP 狀態碼和重定向情況。
360 公司的搜索引擎,以前的名字叫「好搜」,如今改為「360 搜索」;網站域名也從 www.haosou.com 改為了 www.so.com。要是你在瀏覽器的地址欄中輸入 www.haosou.com,那樣會通過 302 跳轉到 www.so.com。讓我們借此機會來演試。
import requests
cs_url = 'http://www.so.com/s'
param = {'ie':'utf-8', 'q':'query'}
r = requests.get (cs_url, params = param)
print r.url, r.status_code
cs_url = 'http://www.haosou.com/s'
r = requests.get (cs_url, params = param)
print r.url, r.status_code, r.history
結果是:
http://www.so.com/s?q=query&ie=utf-8 200
http://www.so.com/s?q=query&ie=utf-8 200 [302]>]
我們發現,requests 默認自動地處理了 302 跳轉。在經過跳轉的請求中,返回的 URL 和狀態碼都是跳轉之后的信息;唯獨在 history 中,用 Python 列表記錄了跳轉情況。
大多數情況下,自動處理是挺好的。不過,有時候我們也想單步追蹤頁面跳轉情況。此時,可以給請求加上 allow_redirects = False 參數。
import requests
cs_url = 'http://www.so.com/s'
param = {'ie':'utf-8', 'q':'query'}
r = requests.get (cs_url, params = param)
print r.url, r.status_code
cs_url = 'http://www.haosou.com/s'
r = requests.get (cs_url, params = param, allow_redirects = False)
print r.url, r.status_code, r.history
輸出結果:
http://www.so.com/s?q=query&ie=utf-8 200
http://www.haosou.com/s?q=query&ie=utf-8 302 []
不容許 requests 自動處置重定向跳轉后,反回的 URL 和狀態碼都合乎預估了。
請求超時設定
requests 的請求超時設定以秒為基本單位。比如,對請求加主要參數 timeout = 5 就可設定請求超時為 5 秒。
# a very short timeout is set intentionally
import requests
cs_url = 'http://www.zhihu.com'
r = requests.get (cs_url, timeout = 0.000001)
反回錯誤碼:
Traceback (most recent call last):
File "D:\test\py\test.py", line 6, in <module>
r = requests.get (cs_url, timeout = 0.000001)
File "C:\Users\username\AppData\Local\Continuum\Anaconda\lib\site-packages\requests\api.py", line 69, in get
return request('get', url, params=params, **kwargs)
File "C:\Users\username\AppData\Local\Continuum\Anaconda\lib\site-packages\requests\api.py", line 50, in request
response = session.request(method=method, url=url, **kwargs)
File "C:\Users\username\AppData\Local\Continuum\Anaconda\lib\site-packages\requests\sessions.py", line 465, in request
resp = self.send(prep, **send_kwargs)
File "C:\Users\username\AppData\Local\Continuum\Anaconda\lib\site-packages\requests\sessions.py", line 573, in send
r = adapter.send(request, **kwargs)
File "C:\Users\username\AppData\Local\Continuum\Anaconda\lib\site-packages\requests\adapters.py", line 419, in send
raise ConnectTimeout(e, request=request)
requests.exceptions.ConnectTimeout: HTTPConnectionPool(host='www.zhihu.com', port=80): Max retries exceeded with url: / (Caused by ConnectTimeoutError(object at 0x0000000002AFABE0>, 'Connection to www.zhihu.com timed out. (connect timeout=1e-06)'))
請求頭
我們使用 httpbin 這一個網站,先了解一下 requests 發出的 HTTP 報文默認的請求頭是啥樣子的。
import requests
cs_url = 'http://httpbin.org/get'
r = requests.get (cs_url)
print r.content
反回結果:
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.7.0 CPython/2.7.10 Windows/7"
},
"origin": "xx.xx.xx.xx",
"url": "http://httpbin.org/get"
}
需注意,在這里采用 r.content 來查詢請求頭部是由于 httpbin 這一個網站的特別性——它什么活都不干,就把對方請求的具體內容反回給請求者。在 requests 之中,應該采用 r.request.headers 來查詢請求的頭部。
一般而言讓我們較為關心其中的 User-Agent 和 Accept-Encoding。要是讓我們要改動 HTTP 頭中的這兩項具體內容,只需用將1個適合的字典基本參數傳到 headers 即可。
import requests
my_headers = {'User-Agent' : 'From Liam Huang', 'Accept-Encoding' : 'gzip'}
cs_url = 'http://httpbin.org/get'
r = requests.get (cs_url, headers = my_headers)
print r.content
反回:
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip",
"Host": "httpbin.org",
"User-Agent": "From Liam Huang"
},
"origin": "xx.xx.xx.xx",
"url": "http://httpbin.org/get"
}
可以看到,UA 和 AE 都已經被改動了。
響應頭
作為 HTTP 請求的響應,返回的內容中也有 HTTP 頭。它是一個反序列化為 Python 字典的數據結構,可以通過 Response.headers 來查看。
import requests
cs_url = 'http://httpbin.org/get'
r = requests.get (cs_url)
print r.headers
返回:
{
"content-length": "263",
"server": "nginx",
"connection": "keep-alive",
"access-control-allow-credentials": "true",
"date": "Fri, 26 Feb 2016 10:26:17 GMT",
"access-control-allow-origin": "*",
"content-type": "application/json"
}
響應內容
字節模式 / 自動解包
長期以來,
互聯網都存在帶寬有限的情況。因此,網絡上傳輸的數據,很多情況下都是經過壓縮的。經由 requests 發送的請求,當收到的響應內容經過 gzip 或 deflate 壓縮時,requests 會自動為我們解包。我們可以用 Response.content 來獲得以字節形式返回的相應內容。
import requests
cs_url = 'http://www.zhihu.com'
r = requests.get (cs_url)
if r.status_code == requests.codes.ok:
print r.content
這相當于 urllib2.urlopen(url).read()。
如果相應內容不是文本,而是二進制數據(比如圖片),那么上述打印結果可能會糊你一臉。這里以圖片為例,示例一下該怎么辦。
import requests
from PIL import Image
from StringIO import StringIO
cs_url = 'http://liam0205.me/uploads/avatar/avatar-2.jpg'
r = requests.get (cs_url)
if r.status_code == requests.codes.ok:
Image.open(StringIO(r.content)).show()
運行無誤的話,能看到我和我愛人的照片。
文本模式 / 編碼
如果響應返回是文本,那么你可以用 Response.text 獲得 Unicode 編碼的響應返回內容。
import requests
cs_url = 'http://www.zhihu.com'
r = requests.get (cs_url)
if r.status_code == requests.codes.ok:
print r.text
要獲得 Unicode 編碼的結果,意味著 requests 會為我們做解碼工作。那么 requests 是按照何種編碼去對返回結果解碼的呢?
requests 會讀取 HTTP header 中關于字符集的內容。如果獲取成功,則會依此進行解碼;若不然,則會根據響應內容對編碼進行猜測。具體來說,我們可以用 Response.encoding 來查看/修改使用的編碼。
import requests
cs_url = 'http://www.zhihu.com'
r = requests.get (cs_url)
if r.status_code == requests.codes.ok:
print r.encoding
反序列化 JSON 數據
開篇給出的第一個 requests 示例中,特別吸引人的一點就是 requests 無需任何其他庫,就能解析序列化為 JSON 格式的數據。
我們以 IP 查詢 Google 公共 DNS 為例:
import requests
cs_url = 'http://ip.taobao.com/service/getIpInfo.php'
my_param = {'ip':'8.8.8.8'}
r = requests.get(cs_url, params = my_param)
print r.json()['data']['country'].encode('utf-8')
結果將輸出:
美國
模擬登錄 GitHub 看看
Cookie 介紹
HTTP 協議是無狀態的。因此,若不借助其他手段,遠程的服務器就無法知道以前和客戶端做了哪些通信。Cookie 就是「其他手段」之一。
Cookie 一個典型的應用場景,就是用于記錄用戶在網站上的登錄狀態。
用戶登錄成功后,服務器下發一個(通常是加密了的)Cookie 文件。
客戶端(通常是網頁
瀏覽器)將收到的 Cookie 文件保存起來。
下次客戶端與服務器連接時,將 Cookie 文件發送給服務器,由服務器校驗其含義,恢復登錄狀態(從而避免再次登錄)。
Cookie 在 requests 中
Cookie? 你說的是小甜點吧!
別忘了,requests 是給人類設計的 Python 庫。想想使用
瀏覽器瀏覽網頁的時候,我們沒有手工去保存、重新發送 Cookie 對嗎?瀏覽器都為我們自動完成了。
在 requests 中,也是這樣。
當瀏覽器作為客戶端與遠端服務器連接時,遠端服務器會根據需要,產生一個 SessionID,并附在 Cookie 中發給瀏覽器。接下來的時間里,只要 Cookie 不過期,瀏覽器與遠端服務器的連接,都會使用這個 SessionID;而瀏覽器會自動與服務器協作,維護相應的 Cookie。
在 requests 中,也是這樣。我們可以創建一個 requests.Session,爾后在該 Session 中與遠端服務器通信,其中產生的 Cookie,requests 會自動為我們維護好。
POST 表單
POST 方法可以將一組用戶數據,以表單的形式發送到遠端服務器。遠端服務器接受后,依照表單內容做相應的動作。
調用 requests 的 POST 方法時,可以用 data 參數接收一個 Python 字典結構。requests 會自動將 Python 字典序列化為實際的表單內容。例如:
import requests
cs_url = 'http://httpbin.org/post'
my_data = {
'key1' : 'value1',
'key2' : 'value2'
}
r = requests.post (cs_url, data = my_data)
print r.content
返回:
{
...
"form": {
"key1": "value1",
"key2": "value2"
},
...
}
實際模擬登錄 GitHub 試試看
模擬登錄的第一步,首先是要搞清楚我們用瀏覽器登錄時都發生了什么。
GitHub 登錄頁面是 https://github.com/login。我們首先清空瀏覽器 Cookie 記錄,然后用 Chrome 打開登錄頁面。
填入 Username 和 Pass
word 之后,我們打開 Tamper Chrome 和 Chrome 的元素審查工具(找到 Network 標簽頁),之后點登錄按鈕。
在 Tamper Chrome 中,我們發現:雖然登錄頁面是 https://github.com/login,但實際接收表單的是 https://github.com/session。若登錄成功,則跳轉到 https://github.com/ 首頁,返回狀態碼 200。
而在 Chrome 的審查元素窗口中,我們可以看到提交給 session 接口的表單信息。內里包含
commit
utf8
authenticity_token
login
其中,commit 和 utf8 兩項是定值;login 和 password 分別是用戶名和密碼,這很好理解。唯獨 authenticity_token 是一長串無規律的字符,我們不清楚它是什么。
POST 動作發生在與 session 接口交互之前,因此可能的信息來源只有 login 接口。我們打開 login 頁面的源碼,試著搜索 authenticity_token 就不難發現有如下內容:
<input name="authenticity_token" type="hidden" value="......" />
原來,所謂的 authenticity_token 是明白卸載 HTML 頁面里的,只不過用 hidden 模式隱藏起來了。為此,我們只需要使用 Python 的正則庫解析一下,就好了。
這樣一來,事情就變得簡單起來,編碼吧!
模擬登錄 GitHub
import requests
import re
cs_url = 'https://github.com/login'
cs_user = 'user'
cs_psw = 'psw'
my_headers = {
'User-Agent' : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36',
'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Encoding' : 'gzip',
'Accept-Language' : 'zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4'
}
sss = requests.Session()
r = sss.get(cs_url, headers = my_headers)
reg = r'<input name="authenticity_token" type="hidden" value="(.*)" />'
pattern = re.compile(reg)
result = pattern.findall(r.content)
token = result[0]
my_data = {
'commit' : 'Sign in',
'utf8' : '%E2%9C%93',
'authenticity_token' : token,
'login' : cs_user,
'password' : cs_psw
}
cs_url = 'https://github.com/session'
r = sss.post(cs_url, headers = my_headers, data = my_data)
print r.url, r.status_code, r.history
輸出:
https://github.com/ 200 [<Response [302]>]
代碼很好理解,其實只是完全地模擬了瀏覽器的行為。
首先,我們準備好了和 Chrome 一致的 HTTP 請求頭部信息。具體來說,其中的 User-Agent 是比較重要的。而后,仿照瀏覽器與服務器的通信,我們創建了一個 requests.Session。接著,我們用 GET 方法打開登錄頁面,并用正則庫解析到 authenticity_token。隨后,將所需的數據,整備成一個 Python 字典備用。最后,我們用 POST 方法,將表單提交到 session 接口。
最終的結果也是符合預期的:經由 302 跳轉,打開了(200)GitHub 首頁。