手把手教你寫網路爬蟲(8):徹底解決亂碼問題

發表於2018-05-14

1368640-20180426083108287-1201983270

字元編解碼是爬蟲裡必學的一項知識,在我們的爬蟲生涯中早晚會爬到亂碼的網頁,與其遇到時驚慌失措,不如早學早好,徹底避免亂碼問題。

字元編碼簡介

什麼是字符集

在介紹字元編碼之前,我們先了解下什麼是字符集。

字元(Character)是各種文字和符號的總稱,包括各國家文字、標點符號、圖形符號、數字等。字符集(Character set)是多個字元的集合,字符集種類較多,每個字符集包含的字元個數不同,常見字符集:ASCII字符集、GBK字符集、Unicode字符集等。

什麼是字元編碼

字元編碼和字符集不同。字符集只是字元的集合,無法進行網路傳送、處理,必須經編碼後才能使用。如Unicode字符集可依不同需求以UTF-8、UTF-16、UTF-32等方式編碼。

字元編碼就是以二進位制的數字來對應字符集的字元。各個國家和地區在制定編碼標準的時候,“字元的集合”和“編碼”一般都是同時制定的。因此,平常我們所說的“字符集”,除了有“字元的集合”這層含義外,同時也包含了“編碼”的含義。

常用字符集

簡單介紹幾個常見的。

ASCII:

ASCII是學計算機同學的啟蒙字符集,一般是從這本書裡學到的:

1368640-20180427084222299-735009224

請允許我懷舊一下,以下引用譚浩強老師的講解:

1368640-20180427084222299-735009224

中文字符集:

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中。亂碼產生的過程如下:

  1. OkHttp請求指定url,返回了一個GBK編碼的網頁位元組流;
  2. OkHttp以預設UTF-8進行解碼(此時已亂),並以UTF-16方式編碼為Java的String型別,返回給處理程式。(為什麼以UTF-16方式編碼?因為Java的資料在記憶體中的編碼是UTF-16);
  3. 爬蟲拿到這個編碼錯誤的String型別的網頁,呼叫MongoDB的API,將資料編碼為UTF-8儲存到資料庫中。所以最後在資料庫看到的資料是亂的。

1368640-20180427084222299-735009224

顯然,導致亂碼的根本原因就是OkHttp在最初使用了錯誤的解碼方式進行解碼。所以要解決這個問題,就要讓OkHttp知道網頁的編碼型別,進行正確的解碼。

1368640-20180427084222299-735009224

網頁有兩種約定的方式告訴爬蟲自己使用的是什麼編碼方式:

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使用

安裝:

使用:

注意:返回結果中有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》

(https://github.com/shabanali-faghani/IUST-HTMLCharDet/blob/master/wiki/Charset-Encoding-Detection-of-HTML-Documents.pdf)

利用現有的探測技術,通過一些技巧來提高探測的準確性。主要原理是組合使用Mozilla CharDet和IBM ICU,並在探測前巧妙的去掉了HTML標籤。雖然這是伊朗大學發的Paper,但據說這種方法已經在生產環境取得了很好的效果,目前正應用在一個10億級別資料量的大型爬蟲上。

下一步

最近聊的話題越來越沉重,想必大家也累了。下期打算帶大家一起放鬆一下,聊點輕鬆的話題。從系列的開篇到現在也有半年了,技術領域有了不小的更新,出現了一些好用的工具,我們需要替換哪些工具呢?請聽下回分解!

相關文章