1. 程式人生 > 實用技巧 >爬蟲筆記之requests檢測網站編碼方式(zozo.jp)(碎碎念)

爬蟲筆記之requests檢測網站編碼方式(zozo.jp)(碎碎念)

發現有些網站的編碼方式比較特殊,使用requests直接請求拿response.text是得不到正確的文字的,比如這個網站:

https://zozo.jp/

當使用requests訪問網站,使用response.text方式取響應文字的時候,會發現得到的是奇怪的內容:

#!/usr/bin/env python3
# encoding: utf-8
"""
@author: CC11001100
"""
import requests

if __name__ == "__main__":
    url = "https://zozo.jp/"
    response = requests.get(url)
    print(response.text)

看下輸出:

和chrome的網頁標題對照一下:

似乎長得有點不太一樣...

這個的原因就比較坑爹了,requests的響應內容的編碼方式是先嚐試從響應頭中根據content-type獲取,如果獲取不到就再嘗試依據chardet來判斷,而很不幸的,我們的邏輯並沒有撐到chardet那裡,它神奇的從響應頭中獲取到了編碼方式,被識別為了ISO-8859-1:

print(f"網頁的編碼方式為:{response.encoding}")  # 網頁的編碼方式為:ISO-8859-1

但是,為什麼呢?我們在網頁上找是找不到有哪個地方設定了這個編碼方式的,這就是為什麼說它坑爹的地方,我們來看下requests原始碼中對響應的編碼方式的識別這部分,在requests.adapters.HTTPAdapter#build_response方法中的這一行是設定響應的編碼方式:

我們跟進去這個get_encoding_from_headers方法,這個方法在utils中,方法程式碼不長只有幾個小邏輯:

先是從響應頭中獲取content-type響應頭,如果這個響應頭不存在的話,則認為無法從響應頭中獲取到編碼方式,直接返回None。

然後是487行,呼叫了一個_parse_content_type_header方法:

要理解這個函式,先看下一般情況下content-type這個響應頭長什麼樣子:

但是這個網頁的content-type是:

這會解析到一個字串text/html和一個空字典:

OK,繼續往下走,然後是一個是否設定了編碼方式的判斷:

當設定了編碼方式,也就是正常的content-type,比如下面這個:

Content-Type: text/html; charset=utf-8

解析到的params字典就是有一個charset的key:

所以如果content-type中設定了charset是能夠解析出來按照這個charset設定上編碼方式的,但是很不幸,這個網站沒有在content-type中設定charset引數,這個判斷邏輯也沒能進去,然後就剩下最後一個判斷了:

很不幸,進入了這個邏輯,也就是說如果content-type的mime型別設定的是text文字型別,則預設是ISO-8859-1編碼的。我嘗試為此預設策略找到證據支撐,我擦咋還真的找到了...

在rfc2616 HTTP/1.1規範的3.7.1 Canonicalization and Text Defaults的最後一小節:

https://tools.ietf.org/html/rfc2616#section-3.7.1

意思是說mime型別為text型別的子型別的預設編碼方式為ISO-8859-1,如果不是的話需要自己使用charset指定,子型別是指text/html、text/css、text/javascript之類的這種。這樣看來requests做得並沒有問題,只是zozo.jp這個網站在自己不瞭解的情況下隱式聲明瞭自己是ISO-8859-1編碼型別但是實際上並不是,它自己指定錯誤了,只不過requests很實誠的你說你是你就是,而瀏覽器則可以識別出來併兼容,所以就成了兩個效果。

但是呢,我陳二是這麼輕易認慫的人嗎,我一定要找到一個反駁它的證據,然後我繼續查資料,先說明一下剛才那個HTTP 1.1的規範rfc2616是1999年6月釋出的:

一個rfc肯定會隨著時間不斷慢慢完善,或是增加刪除某些東西,或是修改某些東西,這個ISO-8859-1的編碼到了今天明顯不是一個很合適的預設值了,我覺得後續應該會有文件對其進行修正的,然後我把上面那些Obsoleted by、Updated by捋了一下(不要誤會,只是按照ISO-8859-1的關鍵詞搜了一遍,對命中的小節看了一下),最終找到了我想要的,在rfc7231的Appendix B. Changes from RFC 2616下有一個小節的一句話提到了:

https://tools.ietf.org/html/rfc7231#appendix-B

我理解的意思是說,text mime型別的預設編碼從ISO-8859-1移除,現在mime型別的預設編碼是什麼取決於它們的具體型別,但是這句話有點神神叨叨的,你好歹告訴我每種mime型別的具體預設編碼我到哪裡去看啊,這裡有個關於這個問題討論的帖子,但是也沒有得出具體的結論:

https://stackoverflow.com/questions/49552112/is-the-charset-component-mandatory-in-the-http-content-type-header

沒辦法,繼續找到The 'text/html' Media Type的rfc看看能不能找到答案:

https://tools.ietf.org/html/rfc2854

在2. Registration of MIME media type text/html部分提到了:

這段是說charset這個引數應該始終顯示的指定出來,並且推薦優先使用utf形式,所以我們可以看到很多網站都是Content-Type: text/html; charset=utf-8這種形式的。然後就看到了讓人興奮的一句話:

See Section 6 below for a discussion of charset default rules.

馬上跳到section 6看下預設的編碼規則到底是啥:

呵呵,越看心越涼,說了個屁啊,這段話除去廢話相當於啥都沒說。

然後想到了也許該去requests的倉庫看看issue:

https://github.com/psf/requests/issues?q=ISO-8859-1++RFC7231

搜到了兩個issue,先看第一個:

https://github.com/psf/requests/issues/5629

嗯,有理有據令人信服,然後看看作者咋回的:

emmm,被懟了一頓,說這改動不能相容之前的版本,而且你丫提問題之前應該先搜搜有沒有人已經提過了,應該指的就是第二個了,看下另一個:

https://github.com/psf/requests/issues/1604

樓主提出問題:

然後...我靠這人是傻逼嗎,人家都已經說規範已經明確不推薦這個值了,你還說如果規範改了我們會遵守,末了還嘲諷一句你覺得不合理你去參加制定規則的會議修改呀,狗頭沒用先打死再說...

然後...這哥們在說什麼,我靠這傢伙真的是作者嗎...他說的和requests實際的行為根本不一致,實際上根本走不到chardet這一步,並且給出的解決方案看起來也怪怪的,不過他這是13年評論的,7年之前啥樣我也不知道,暫且認為說得OK:

在下面還有一個亮點:

哈哈,本篇文章抓取的案例網站zozo.jp也是個日本網站 :)

然後這個討論斷斷續續持續了幾年,估計是提的人太多,終於在某個樓說已經有一個issue在追蹤這個問題了:

https://github.com/psf/requests/issues/2086

看到這個issue還處於open狀態,我鬆了一口氣,他們終於肯認真處理問題並願意讀rfc了!

翻譯這些東西沒啥意思,讀者可以自行閱讀,只說幾個點,紅框裡說的這個就是後面要介紹的編碼檢測方法,不過那個現在也要移出去了,畢竟過了很多年了...

然後就是很長的討論和追蹤,已經失去了繼續追尋下去的耐心,在這個問題上已經浪費了太多的時間,結束進入下一個話題。

requests提供了一個方法可以探測網頁的編碼方式,其返回結果是一個列表:

requests.utils.get_encodings_from_content(response.text)

點進去看下它的實現:

其實就是從網頁上抽取有沒有設定編碼方式,也就是說得到的編碼在網頁上就是存在的,我們看下對zozo.jp檢測到的編碼方式:

print(f"網頁使用的編碼方式為: {encoding}")  # 網頁使用的編碼方式為: ['Shift_JIS']

那麼網頁上一定是存在Shift_JIS這個字樣的,我們回到網頁上搜索一下:

原來網頁上是設定了編碼方式的,那麼手動解碼指定正確的編碼方式就可以了:

#!/usr/bin/env python3
# encoding: utf-8
"""
@author: CC11001100
"""
import requests

if __name__ == "__main__":
    url = "https://zozo.jp/"
    response = requests.get(url)

    # 編碼方式比較特殊,解碼的時候需要額外處理下
    encoding = requests.utils.get_encodings_from_content(response.text)
    print(f"網頁使用的編碼方式為: {encoding}")  # 網頁使用的編碼方式為: ['Shift_JIS']

    print(response.content.decode("Shift_JIS"))

可以看到這個時候解碼得到的就沒問題了,雖然還是看不懂...

需要注意的是這個方法只是幫我們節省了一點搜尋的工作量,應該在發現可能是編碼問題之後輔助debug使用,而不是每次都對同一個網頁這麼判斷一下,因為正則還是有一定的代價的。而且這個方法在3.0之後就要被移除了,而且很高冷的只留了一個issue的id並沒有貼上連結啥的...

我們去github的倉庫看下這個issue:

https://github.com/psf/requests/issues/2266

給出的理由是requests是一個http庫,而這個工具類中的有些方法更傾向於html處理,如我們剛才所見,這個編碼方式的原理就是正則搜尋html,所以他們決定把這些方法移動到第三方庫request-toolbelt( https://github.com/requests/toolbelt )中。

除此之外當懷疑是編碼問題的時候還可以使用chardet來檢測文字編碼方式:

# chardet檢測到的編碼方式為: {'encoding': 'SHIFT_JIS', 'confidence': 0.99, 'language': 'Japanese'}
print(f"chardet檢測到的編碼方式為: {chardet.detect(response.content)}")

特別需要注意的是通常情況下同一個地址的網頁編碼方式多次請求是相同的,我們應該儘量一次推出編碼方式,然後手動指定好編碼方式,而不是每次都推測,因為推測編碼方式是有代價的。


請注意爬蟲文章具有時效性,本文寫於2020-11-4日。