手把手教你写网络爬虫(8)
作者:拓海
摘要:从零开始写爬虫,初学者的速成指南!
封面:
字符编解码是爬虫里必学的一项知识,在我们的爬虫生涯中早晚会爬到乱码的网页,与其遇到时惊慌失措,不如早学早好,彻底避免乱码问题。
字符编码简介
什么是字符集
在介绍字符编码之前,我们先了解下什么是字符集。
字符(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使用
安装:
pip install chardet
使用:
>>> 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亿级别数据量的大型爬虫上。
下一步
最近聊的话题越来越沉重,想必大家也累了。下期打算带大家一起放松一下,聊点轻松的话题。从系列的开篇到现在也有半年了,技术领域有了不小的更新,出现了一些好用的工具,我们需要替换哪些工具呢?请听下回分解!