1. 程式人生 > >python requests接收chunked編碼問題

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