likes
comments
collection
share

爬虫工程师,UTF8/GBK/GB2312的乱码让你头疼吗

作者站长头像
站长
· 阅读数 13

阅读收益预览

1、你将会了解到源码跟进的过程;

2、你将会看到问题分析的思路;

3、你可以解决 Requests 库关于编码猜测不准确的问题;

内容介绍

这一篇我们来观摩 Python 中的 Requests、Scrapy 库以及 Golang 中的 Charset 库对于网页编码的处理逻辑,并让你具备提高 Requests 库编码猜测准确性的能力。

乱码现象

近期在工程实践中发现了一个长期潜伏的网页文本乱码问题,也就是爬取网页后,打印出来的文本是乱码。如果你是 Python 工程师,你可以试试下面这段代码:

import requests​
# GB2312
resp = requests.get("http://news.inewsweek.cn/society/2022-05-30/15753.shtml")
print(resp.text)

爬虫工程师,UTF8/GBK/GB2312的乱码让你头疼吗

打印出来的网页文本,中文部分就是乱码,这真是令我狗头 🐶 变大。

你心里可能会有这样的疑问:“按理说,Python 的 Requests 库应该能够帮助我们自动识别编码,然后自动转换才对的”。“但事实却并不是那么回事,为什么?”

不仅仅是 Python 的 Requests 库有这样的症状,Golang 的 Charset 库一样有类似的情况。

周树人:“我这里遇到了两个问题,一个是乱码,另一个也是乱码”

这时候,脑海中想起周树人那句话“我大抵是变菜了,这样的问题这么长时间居然都不知道”。

为了证明自己并不是变菜,而是本来就这么菜,我决定翻(拨)开它们的源码(衣服)瞧一瞧。

虽然手动指定编码(例如 resp.encoding="gb2312" )可以解决这样的问题,但是手动并非我等工程师所求。而且,手动指定也会乱码。

这里要注意的是,为了方便能看到源码,Python 工程师请用 Pycharm 查看,VSCode 这类工具我没试过,不确定能否追查到底层源码。

看源码之 Python Requests

Python 工程师,请把上面的代码复制粘贴到 Pycharm 中,然后按 Ctrl/Command 键+鼠标左键跟进 resp.text 的这个 text

爬虫工程师,UTF8/GBK/GB2312的乱码让你头疼吗

它的代码不多,我们来阅读一下

爬虫工程师,UTF8/GBK/GB2312的乱码让你头疼吗

大致意思是当需要获取网页文本的时候,编码部分的处理逻辑为:

1.先从对象中找编码,找到就用;2.找不到编码的话,就通过 apparent_encoding 去分析(其实就是猜)。

第 2 步  apparent_encoding 这里其实还可以继续跟进看代码,这里就是猜测的过程,因为并不是文章重点,我就不写了,感兴趣的可以自己跟进瞅一瞅。

我们要看第 1 步,它想用对象自身的编码,也就是 self.encoding。那么问题来了:self.encoding 的值它又是哪里来的呢?

你可以从对象中初始化和赋值的地方找,然后跟进看源码。也可以简单点跟着我的路线一步步走,下面是我的查找步骤。在代码里找到 requests.get()  中的 get 函数,然后一直往里面跟,路径如下:

1. requests.get
2. request
3. session.request
4. self.send
5. adapter.send
6. self.build_response
7. get_encoding_from_headers

代码只有几行,我们阅读一下

爬虫工程师,UTF8/GBK/GB2312的乱码让你头疼吗

很简单,它从响应头中读取 content-type,然后试图从里面找 charset,如果找到就把对应的值传回去,找不到就把 ISO-8859-1 传回去。

这个传回去的值,就是 response.text 中用 self.encoding 想要获取的值。如果它读取到了,那就用读取到的那个值,读取不到,就用 apparent_encoding 猜出来的值,这也就是为什么它遇到 GBK/GB2312/GB18030 的时候容易出现乱码。

小知识: GB18030 能够兼容处理 GBK/GB2312 编码,例如 Golang 中遇到 GBK 或者 GB2312 的时候,直接用 GB18020 编码就不会乱码。

那这怎么办呢?

看源码之 Golang Charset

我们来看看 Golang Charset 库(golang.org/x/net/html/charset)对应那部分(也就是 DetermineEncoding 函数)的源码,跟进路径如下:

1. charset.DetermineEncoding
2. Lookup
3. htmlindex.Get

爬虫工程师,UTF8/GBK/GB2312的乱码让你头疼吗

大致的意思是,通过读取你传入的网页文本,从里面找到 charset=xxx 这样的句式,然后把那个值取出来,也就是把编码类型的名称取出来。

过多的不用看了,剩下的逻辑就是按照读取到的编码类型,对网页进行重新编码,最后把编码完成的文本返回。

按理说,这样的方法是比较准确的。只要从网页中读取到编码,就能够顺利返回正确的文本,不会产生乱码。然而事实就是不按这个理,因为测试的时候出现了乱码。

通过阅读 Requests 的源码和 Charset 的源码,我们掌握了几个信息:

1、网页文本里可以读取到编码类型;

2、响应头里也可以读取到编码类型;

3、从 Requests 库的逻辑来看,响应头里面的编码类型才是正确的类型,网页文本里写的 charset=xxx 有可能会导致乱码。

Python Requests 和 Golang Charset 的逻辑,就是待会我们 1+1=3 的重要依据。

想必聪明的你现在也猜出来了“把 Requests 的优点和 Charset 的优点结合到一起就好了嘛!

看源码之 Python Scrapy

上面的发现,我跟青南分享后,他说用 Scrapy 的时候没有发生过乱码的问题,并且他给出了对应部分的源码

github.com/scrapy/w3li…

我们来简单阅读一下

爬虫工程师,UTF8/GBK/GB2312的乱码让你头疼吗

大致的逻辑,是先尝试从响应头的 Content-Type 里面读取编码,有值的话直接使用。读不到再去网页文本里面找。

跟上面讲到的思路如出一辙,我都不知道这是我自己想到的的方法,还是抄别人的方法。

“管他呢,好用就可以了”

解决问题

这下就明朗了,Requests 和 Charset 库的处理逻辑其实应该跟 Scrapy 的逻辑一样,这样就能够提高各自的编码猜测准确率。

真的是这样吗?

那也不一定,还是得动手测试。

因此我收集了很多不同站点、不同类型网页的文本进行测试,大概有五六十个网页(测试基于 Golang)。测试时候用的逻辑跟 Scrapy 一致,最后的测试结果是全部通过。\

在未使用这个处理逻辑之前, Golang Charset 库的错误数是 2,Python Requests 库的错误率很高,测试了几个我就不想测了……

由于我的测试代码是 Golang 的,而且项目是商业项目,所以具体的测试代码和用例我就不放出来了。

我从中随机找几个样本给大家,大家可以自己动手玩一玩

http://news.inewsweek.cn/society/2022-05-30/15753.shtml
https://jrz.cnstock.com/yw/202206/4893506.htm
https://www.cs.com.cn/zt/2022dzt/03/202206/t20220607_6275320.html

我并没有对 Scrapy 进行测试,大抵是因为我太懒了,也有可能因为创业太忙也太累了。

写这篇文章分享出来,已经是超工作负荷了,希望能给帮助到大家。

我是东鸽,我还活着。

说明:本文测试结果和解决思路基于测试样本。如果有不同的案例,可以在评论区发出来讨论。