字元編解碼是爬蟲裡必學的一項知識,在我們的爬蟲生涯中早晚會爬到亂碼的網頁,與其遇到時驚慌失措,不如早學早好,徹底避免亂碼問題。
字元編碼簡介
什麼是字符集
在介紹字元編碼之前,我們先了解下什麼是字符集。
字元(Character)是各種文字和符號的總稱,包括各國家文字、標點符號、圖形符號、數字等。字符集(Character set)是多個字元的集合,字符集種類較多,每個字符集包含的字元個數不同,常見字符集:ASCII字符集、GBK字符集、Unicode字符集等。
什麼是字元編碼
字元編碼和字符集不同。字符集只是字元的集合,無法進行網路傳送、處理,必須經編碼後才能使用。如Unicode字符集可依不同需求以UTF-8、UTF-16、UTF-32等方式編碼。
字元編碼就是以二進位制的數字來對應字符集的字元。各個國家和地區在制定編碼標準的時候,“字元的集合”和“編碼”一般都是同時制定的。因此,平常我們所說的“字符集”,除了有“字元的集合”這層含義外,同時也包含了“編碼”的含義。
常用字符集
簡單介紹幾個常見的。
ASCII:
ASCII是學計算機同學的啟蒙字符集,一般是從這本書裡學到的:
請允許我懷舊一下,以下引用譚浩強老師的講解:
中文字符集:
GB2312:包含6763個漢字。
GBK:包含21003個漢字。GBK相容GB2312,也就是說用GB2312編碼的漢字可以用GBK來解碼。
GB18030:收錄了70000個漢字,這麼多是因為包含了少數民族文字。同樣相容GBK和GB2312。
Unicode:Unicode 是為了解決傳統的字元編碼方案的侷限而產生的,它為每種語言中的每個字元設定了統一併且唯一的二進位制編碼,以滿足跨語言、跨平臺進行文字轉換、處理的要求。具有多種編碼方式,如UTF-7、 UTF-8、UTF-16、UTF-32等。
為什麼會產生亂碼
簡單的說亂碼的出現是因為:編碼和解碼時用了不同的字符集。對應到真實生活中,就好比是一個英國人為了表示祝福在紙上寫了bless(編碼)。而一個法國人拿到了這張紙,由於在法語中bless表示受傷的意思,所以認為他想表達的是受傷(解碼)。同理,在計算機中,一個用UTF-8編碼後的字元,用GBK去解碼。由於兩個字符集的字型檔表不一樣,同一個漢字在兩個字元表的位置也不同,最終就會出現亂碼。
那麼,爬蟲中的亂碼是怎麼產生的,又該如何解決呢?
爬蟲中的亂碼
假設我們的爬蟲是java開發的,網路請求庫使用OkHttp,網頁儲存到MongoDB中。亂碼產生的過程如下:
- OkHttp請求指定url,返回了一個GBK編碼的網頁位元組流;
- OkHttp以預設UTF-8進行解碼(此時已亂),並以UTF-16方式編碼為Java的String型別,返回給處理程式。(為什麼以UTF-16方式編碼?因為Java的資料在記憶體中的編碼是UTF-16);
- 爬蟲拿到這個編碼錯誤的String型別的網頁,呼叫MongoDB的API,將資料編碼為UTF-8儲存到資料庫中。所以最後在資料庫看到的資料是亂的。
顯然,導致亂碼的根本原因就是OkHttp在最初使用了錯誤的解碼方式進行解碼。所以要解決這個問題,就要讓OkHttp知道網頁的編碼型別,進行正確的解碼。
網頁有兩種約定的方式告訴爬蟲自己使用的是什麼編碼方式:
1. Http協議的響應頭中的約定:
Content-Type: text/html;charset=utf-8
2. Html中meta標籤中的約定:
<meta http-equiv=”Content-Type” content=”text/html; charset=UTF-8“/>
從約定中獲取網頁的編碼後,Okhttp就可以正確的解碼了。然而實際情況卻並不樂觀,很多網頁並不遵守約定,缺少這兩個資訊。有人通過Alexa統計各國遵守這個約定的網頁數:
語言 |
URL字尾 |
URL數 |
HTTP頭中包含 charset的URL數 |
Chinese |
.cn |
10086 |
3776 |
English |
.us/.uk |
21565 |
13223 |
Russian |
.ru |
39453 |
28257 |
Japanese |
.jp |
20339 |
6833 |
Arabic |
.iq |
1904 |
1093 |
German |
.de |
35318 |
23225 |
Persian |
.ir |
7396 |
4018 |
Indian |
.in |
12236 |
4867 |
Total |
all |
148297 |
85292 |
結果表明我們不能被動的依賴網頁告訴我們,而要根據網頁內容來主動探測其編碼型別。
探測字元編碼
什麼是字元編碼自動檢測?
它是指當面對一串不知道編碼資訊的位元組流的時候,嘗試著確定一種編碼方式以使我們能夠讀懂其中的文字內容。它就像我們沒有解金鑰匙的時候,嘗試破解出編碼。
那不是不可能的嗎?
通常來說,是的,不可能。但是,有一些編碼方式為特定的語言做了優化,而語言並非隨機存在的。有一些字元序列在某種語言中總是會出現,而其他一些序列對該語言來說則毫無意義。一個熟練掌握英語的人翻開報紙,然後發現“txzqJv 2!dasd0a QqdKjvz”這樣一些序列,他會馬上意識到這不是英語(即使它完全由英語中的字母組成)。通過研究許多具有“代表性(typical)”的文字,計算機演算法可以模擬人的這種對語言的感知,並且對一段文字的語言做出啟發性的猜測。換句話說就是,檢測編碼資訊就是檢測語言的型別,並輔之一些額外資訊,比如每種語言通常會使用哪些編碼方式。
這樣的演算法存在嗎?
結果證明,是的,它存在。所有主流的瀏覽器都有字元編碼自動檢測的功能,因為網際網路上總是充斥著大量缺乏編碼資訊的頁面。Mozilla Firefox包含有一個自動檢測字元編碼的庫,已經移植到Python中,叫做chardet。
chardet使用
安裝:
1 |
pip install chardet |
使用:
1 2 3 4 5 6 7 8 9 |
>>> import urllib >>> rawdata = urllib.urlopen('http://www.jd.com/').read() >>> import chardet >>> chardet.detect(rawdata) {'confidence': 0.98999999999999999, 'language': '', 'encoding': 'utf-8'} |
注意:返回結果中有confidence,即置信度,這說明探測結果不是100%準確的。
使用其他語言的小夥伴不用擔心,chardet在很多語言中都有移植版。不過C++好像沒有太好的選擇,可以考慮使用IBM的ICU(http://site.icu-project.org/)。
擴充套件閱讀
《A composite approach to language/encoding detection》
(https://www-archive.mozilla.org/projects/intl/UniversalCharsetDetection.html)
這篇論文解釋了Chardet背後使用的探測演算法,分別是“編碼模式方法”、“字元分佈方法”和“雙字元序列分佈方法”。最後說明了三種方法組合使用的必要性,並舉例說明如何組合使用。
《Charset Encoding Detection of HTML Documents A Practical Experience》
利用現有的探測技術,通過一些技巧來提高探測的準確性。主要原理是組合使用Mozilla CharDet和IBM ICU,並在探測前巧妙的去掉了HTML標籤。雖然這是伊朗大學發的Paper,但據說這種方法已經在生產環境取得了很好的效果,目前正應用在一個10億級別資料量的大型爬蟲上。
下一步
最近聊的話題越來越沉重,想必大家也累了。下期打算帶大家一起放鬆一下,聊點輕鬆的話題。從系列的開篇到現在也有半年了,技術領域有了不小的更新,出現了一些好用的工具,我們需要替換哪些工具呢?請聽下回分解!