Fork me on GitHub

爬虫笔记之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)

看下输出:

0

和chrome的网页标题对照一下:

1

似乎长得有点不太一样...

这个的原因就比较坑爹了,requests的响应内容的编码方式是先尝试从响应头中根据content-type获取,如果获取不到就再尝试依据chardet来判断,而很不幸的,我们的逻辑并没有撑到chardet那里,它神奇的从响应头中获取到了编码方式,被识别为了ISO-8859-1:

print(f"网页的编码方式为:{response.encoding}")  # 网页的编码方式为:ISO-8859-1

但是,为什么呢?我们在网页上找是找不到有哪个地方设置了这个编码方式的,这就是为什么说它坑爹的地方,我们来看下requests源码中对响应的编码方式的识别这部分,在requests.adapters.HTTPAdapter#build_response方法中的这一行是设置响应的编码方式:

2

我们跟进去这个get_encoding_from_headers方法,这个方法在utils中,方法代码不长只有几个小逻辑:

3

先是从响应头中获取content-type响应头,如果这个响应头不存在的话,则认为无法从响应头中获取到编码方式,直接返回None。

然后是487行,调用了一个_parse_content_type_header方法:

4

要理解这个函数,先看下一般情况下content-type这个响应头长什么样子:

5

但是这个网页的content-type是:

6

这会解析到一个字符串text/html和一个空字典:

7

OK,继续往下走,然后是一个是否设置了编码方式的判断:

8

当设置了编码方式,也就是正常的content-type,比如下面这个:

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

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

9

所以如果content-type中设置了charset是能够解析出来按照这个charset设置上编码方式的,但是很不幸,这个网站没有在content-type中设置charset参数,这个判断逻辑也没能进去,然后就剩下最后一个判断了:

10

很不幸,进入了这个逻辑,也就是说如果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

11

意思是说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月发布的:

12

一个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

13

我理解的意思是说,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部分提到了:

14

这段是说charset这个参数应该始终显示的指定出来,并且推荐优先使用utf形式,所以我们可以看到很多网站都是Content-Type: text/html; charset=utf-8这种形式的。然后就看到了让人兴奋的一句话:

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

马上跳到section 6看下默认的编码规则到底是啥:

15

呵呵,越看心越凉,说了个屁啊,这段话除去废话相当于啥都没说。

然后想到了也许该去requests的仓库看看issue:

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

搜到了两个issue,先看第一个:

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

16

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

17

emmm,被怼了一顿,说这改动不能兼容之前的版本,而且你丫提问题之前应该先搜搜有没有人已经提过了,应该指的就是第二个了,看下另一个:

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

楼主提出问题:

18

然后...我靠这人是傻逼吗,人家都已经说规范已经明确不推荐这个值了,你还说如果规范改了我们会遵守,末了还嘲讽一句你觉得不合理你去参加制定规则的会议修改呀,狗头没用先打死再说...

19

然后...这哥们在说什么,我靠这家伙真的是作者吗...他说的和requests实际的行为根本不一致,实际上根本走不到chardet这一步,并且给出的解决方案看起来也怪怪的,不过他这是13年评论的,7年之前啥样我也不知道,暂且认为说得OK:

20

在下面还有一个亮点:

21

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

然后这个讨论断断续续持续了几年,估计是提的人太多,终于在某个楼说已经有一个issue在追踪这个问题了:

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

看到这个issue还处于open状态,我松了一口气,他们终于肯认真处理问题并愿意读rfc了!

翻译这些东西没啥意思,读者可以自行阅读,只说几个点,红框里说的这个就是后面要介绍的编码检测方法,不过那个现在也要移出去了,毕竟过了很多年了...

22

然后就是很长的讨论和追踪,已经失去了继续追寻下去的耐心,在这个问题上已经浪费了太多的时间,结束进入下一个话题。

requests提供了一个方法可以探测网页的编码方式,其返回结果是一个列表:

requests.utils.get_encodings_from_content(response.text)

点进去看下它的实现:

23

其实就是从网页上抽取有没有设置编码方式,也就是说得到的编码在网页上就是存在的,我们看下对zozo.jp检测到的编码方式:

print(f"网页使用的编码方式为: {encoding}")  # 网页使用的编码方式为: ['Shift_JIS']

那么网页上一定是存在Shift_JIS这个字样的,我们回到网页上搜索一下:

24

原来网页上是设置了编码方式的,那么手动解码指定正确的编码方式就可以了:

#!/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"))

可以看到这个时候解码得到的就没问题了,虽然还是看不懂...

25

需要注意的是这个方法只是帮我们节省了一点搜索的工作量,应该在发现可能是编码问题之后辅助debug使用,而不是每次都对同一个网页这么判断一下,因为正则还是有一定的代价的。而且这个方法在3.0之后就要被移除了,而且很高冷的只留了一个issue的id并没有粘贴链接啥的...

26

我们去github的仓库看下这个issue:

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

给出的理由是requests是一个http库,而这个工具类中的有些方法更倾向于html处理,如我们刚才所见,这个编码方式的原理就是正则搜索html,所以他们决定把这些方法移动到第三方库request-toolbelt( https://github.com/requests/toolbelt )中。

27

除此之外当怀疑是编码问题的时候还可以使用chardet来检测文本编码方式:

# chardet检测到的编码方式为: {'encoding': 'SHIFT_JIS', 'confidence': 0.99, 'language': 'Japanese'}
print(f"chardet检测到的编码方式为: {chardet.detect(response.content)}")

特别需要注意的是通常情况下同一个地址的网页编码方式多次请求是相同的,我们应该尽量一次推出编码方式,然后手动指定好编码方式,而不是每次都推测,因为推测编码方式是有代价的。


请注意爬虫文章具有时效性,本文写于2020-11-4日。

posted @ 2020-11-25 19:38  CC11001100  阅读(1150)  评论(1编辑  收藏  举报