python requests接收chunked編碼問題
很久以前寫爬蟲用C++和libcurl來實現,體會了libcurl的複雜和強大,後來學會了python,才發現用python+urllib/urllib2寫爬蟲比C++來得容易,再後來發現了python的requests庫,這個更簡潔簡單,只要懂HTTP和HTTPS就可以寫某米搶購器、火車票刷票工具、醫院掛號刷號工具、駕校約車軟體……,太強大了,著名的HTTP工具httpie就是基於requests實現的。
最近就用python的requests寫個了爬蟲,匯出某汽車4s店的訂單excel檔案,我們都知道網頁下載檔案大多數是chunked編碼,而requests庫在解析chunked編碼時就報錯了:
requests.exceptions.ChunkedEncodingError: ('Connection broken: IncompleteRead(4360 bytes read)', IncompleteRead(4360 bytes read))
詳細錯誤資訊如下:
其中requests_chunked.py的第514行只是呼叫requests.session.post:
從Traceback資訊可以看出是/usr/local/lib/python2.7/dist-packages/requests/models.py的第641行拋異常了,遇到這種情況我肯定是看看這一行程式碼之前都幹了啥為啥拋異常,果斷開啟之:
這一塊應該是迴圈接收chunk資料,異常到這裡就中止了,要追查拋異常的源是哪裡只能把try...except...注掉:
這回出錯資訊比較細了:
同樣追蹤/usr/local/lib/python2.7/dist-packages/requests/packages/urllib3/response.py第214行:
同樣也是把這個except分支注掉,讓上一層異常資訊暴露出來:
這回的出錯資訊為:
vim /usr/lib/python2.7/httplib.py +586
從Traceback可以看出是第586行拋的異常,而這裡try分支只有第581行一行程式碼: chunk_left = int(line, 16),這句程式碼的意思是將十六進位制字串line轉換成整型,這時先想到的是line值到底是什麼,用pdb偵錯程式跟一下就知道了:
可以看出接收第一個chunked正常,第二次時line為空,導致int轉換時出異常,元凶終於找到了,那為什麼line為空呢,line是self.fp.readline返回的,應該是tcp連線被關閉了,用tcpdump抓包下來看的確是收到了fin包,在windows下用瀏覽器正常訪問,用Fiddler看看具體的HTTP包互動過程,導致問題的這個chunked包檔案內容是完整的,但最後沒有結束chunked,包太大就不截圖了,自己寫個php驗證了一下:
<?php
echo 'xxxxxxxxxxxxxxxxxxxxxxx<br>';
ob_flush();
flush();
?>
響應如下:
HTTP/1.1 200 OK
Date: Thu, 23 Oct 2014 00:58:43 GMT
Server: Apache/2.4.9 (Win32) OpenSSL/1.0.1g PHP/5.5.11
X-Powered-By: PHP/5.5.11
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/html
1b
xxxxxxxxxxxxxxxxxxxxxxx<br>
0
導致問題的chunked包大概是這樣:
HTTP/1.1 200 OK
Date: Thu, 23 Oct 2014 00:41:31 GMT
Server: Apache/2.4.9 (Win32) OpenSSL/1.0.1g PHP/5.5.11
X-Powered-By: PHP/5.5.11
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/html
1b
xxxxxxxxxxxxxxxxxxxxxxx<br>
最後沒有0\r\n\r\n來結束chunked,所以導致httplib.py解析出了問題。
解決方案:
方案1、修改httplib.py第581行為:
chunk_left = int(line, 16) if line else 0
方案2、自己的程式忽略這個異常;(我自己最開始也是這麼幹的,但接收特別大的chunked包時有時時間太長,對端還沒發完資料就把tcp連線斷掉了,導致資料不完整,最後放棄了這個方案);
方案3、參考http://stackoverflow.com/questions/14442222/how-to-handle-incompleteread-in-python,這個我沒試過,不知道咋樣;
方案4、用pycurl來代替requests,但必須將HTTP協議版本設定為1.0,否則與方案2無差別,因為Transfer-Encoding:chunked , Connection:keep-alive 都是HTTP 1.1的新特性,如果將自己的HTTP協議版本設定為1.0,那麼服務端將不會再返回chunked,而是以TCP分段的方式直接返回整個檔案內容,最後重組成一個完整的HTTP包。
def __pypost(self, url, data):
sio = StringIO.StringIO()
c = pycurl.Curl()
c.setopt(pycurl.URL, url)
c.setopt(pycurl.REFERER, url)
c.setopt(pycurl.HTTPHEADER, ['Connection: close', 'Cache-Control: max-age=0', 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Content-Type: application/x-www-form-urlencoded', 'User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.143 Safari/537.36', 'Content-Type: application/x-www-form-urlencoded', 'Accept-Language: zh-CN,zh;q=0.8'])
c.setopt(pycurl.HTTP_VERSION, pycurl.CURL_HTTP_VERSION_1_0)
c.setopt(pycurl.COOKIE, self.__login_cookie)
c.setopt(pycurl.POST, 1)
c.setopt(pycurl.POSTFIELDS, urllib.urlencode(data, True))
c.setopt(pycurl.CONNECTTIMEOUT, 300)
c.setopt(pycurl.TIMEOUT, 300)
c.setopt(pycurl.WRITEFUNCTION, sio.write)
try:
c.perform()
except Exception, ex:
# print 'error', ex
pass
c.close()
resp = sio.getvalue()
sio.close()
return resp
另外,對於HTTP 1.0來講,如果一次HTTP的響應內容很多,而且又無法提前預知內容的多少,那麼就不使用content-length,輸出完成後,直接關閉連線即可,一定程度上來講,content-length對於HTTP 1.0來講,是可有可無的;通過wireshark抓包來看也是沒有Transfer-Encoding:chunked和Content-Length頭部的:
遇到IIS這種web伺服器也只能這麼對付了。
如果對chunked編碼比較熟悉的話直接抓包就能知道原因了,本文只是講了自己的debug思路,在寫程式遇到問題時第一步可能是google之,但有些問題是google不出來的,那就只能深入的研究了,希望本文對你有幫助。
參考:
http://www.tuicool.com/articles/RfAfqa
http://stackoverflow.com/questions/14442222/how-to-handle-incompleteread-in-python