Python的requests庫是一個非常好用的庫,這應該已經是大多寫過爬蟲的人的共識了。它的簡潔易用給我們帶來很大方便。然而,它也並不是非常完美。今天我們就說說它在處理中文編碼方面的不足。
requests的使用非常簡單,如下:
一句函式呼叫,就可以獲得請求結果的物件response,透過response.content 可以得到原始的二進位制資料,透過response.text可以得到解碼後的文字資料,解碼是根據response.encoding進行的。然而,requests對這個encoding(編碼)的獲取是有問題的。
它獲取編碼的過程分為兩步,不幸的是每一步都有問題:
第一步:從http返回的headers裡面找編碼。
這一步的程式碼在原始檔utils.py裡面是get_encoding_from_headers(headers)函式:
最後兩行程式碼,它認為headers裡面的‘Content-Type’包含‘text’就是‘ISO-8859-1’編碼。這種想法是不嚴謹的。
我們用chrome瀏覽器開啟最開始程式碼中的那個網址,這是一箇中文網頁:
http://epaper.sxrb.com/
在用Chrome的F12檢視http響應的頭,如下:
這個網站給出的Content-Type不是下面的正規格式:
Content-Type: text/html; charset=UTF-8
然後,requests的get_encoding_from_headers函式就得到了ISO-8859-1的編碼,再用這個編碼去解碼中文,當然就會出現亂碼。
第二步:如果不能從響應headers得到編碼,就用chardet從二進位制的content猜測
嚴格講,這步出現的編碼問題不是requests的,而是chardet的,就判requests一個失察之責吧。
在requests的原始碼models.py中定義了requests.get()返回的類Response。我們再看看其中text()的定義:
響應頭找不到編碼時,self.encoding就是None。它就會透過self.apparent_encoding獲得編碼,那就再看看這個apparent_encoding是怎麼來的:
很簡單,就是透過chardet檢測的。問題就出現在這個chardet上面。那我們就打破砂鍋問到底,去看看chardet的程式碼。
上圖是chardet的全部原始碼。其中處理國標中文編碼的gb2312開頭的兩個檔案。我們用grep再看看全部程式碼中含有gb的部分:
grep -i gb *py
以上說明,chardet對國標中文編碼返回的就是(只是)GB2312。那麼問題就來了,國標不只是GB2312,還有GBK,GB18030編碼。
(1)GB 2312 標準共收錄 6763 個漢字
(2)GBK 即漢字內碼擴充套件規範,共收入 21886 個漢字和圖形符號,相容GB2312
(3)GB 18030 與 GB 2312-1980 和 GBK 相容,共收錄漢字70244個
由此可知,三種國標中文編碼的漢字個數是如下關係:
GB2312 < GBK < GB18030
如果不屬於GB2312的漢字用GB2312去編解碼會出現上面問題呢?我們來做個實驗:
例子中的“鎔”字不在GB2312中,用這個編碼時就會報錯,用GBK編碼後的二進位制資料再用GB2312解碼時同樣會報錯,都是因為“鎔”不是GB2312裡面的漢字。
這時候,我們像requests那樣把errors設定為replace再用GB2312解碼得到的文字就會有亂碼出現,“鎔”字變成亂碼了。
最後我們用chardet檢驗二進位制資料的編碼,得到的是GB2312,但應該是GBK或GB18030編碼。當然,chardet的這個bug已經有人在github提出issues,最早是2014年的#33, 後來有#99,#168,但是不懂中文的老外一直沒有merge到master。
問題弄明白了,那麼建議是什麼呢?在爬蟲中,尤其是抓取中文網頁(非英文網頁)時用cchardet檢驗response.content,而不是直接用response.text。
cchardet是uchardet的Python繫結,後者是用C++實現的字元編碼檢測庫,來自Mozilla組織,質量過硬,速度更快,值得信賴。
我的公眾號:猿人學 Python 上會分享更多心得體會,敬請關注。
***版權申明:若沒有特殊說明,文章皆是猿人學 yuanrenxue.com 原創,沒有猿人學授權,請勿以任何形式轉載。***