引言
大家好,本篇文章来分享一下如何使用 requests 的时候保持 headers 有序。
header反爬
这是一个不太明显的反爬手段,正常来说在进行反爬策略测试的时候,我们最先要保证的就是保证自己的程序发送的请求和 web 端发送的请求是一模一样的(sign
之类的除外)。但是,有的时候即使是发送了一模一样的请求,最终的请求结果还是失败的,这个时候就要将两个请求的请求体放到一起来比较,逐字符分析到底是哪个字段不一样了。
例如我这里比较一下两个请求的响应,只有一个字段不一样:
OK,我们进入正题,在遇到类似的反爬时,应该如何保证 requests 的 header 字段有序呢?先看一段示例代码:
import requests
import urllib3
urllib3.disable_warnings()
h = {
'Connection': 'keep-alive',
'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"',
'Accept': 'application/json, text/javascript, */*; q=0.01',
'X-Requested-With': 'XMLHttpRequest',
'sec-ch-ua-mobile': '?0',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Dest': 'empty',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.9',
}
params = {'page': 1}
url = 'https://httpbin.org/post'
r = requests.post(url, headers=h, data=params, verify=False, proxies={'https': 'http://127.0.0.1:9090'})
以上代码运行后,可以看到,请求头的顺序和我在代码中定义的不一样了。
除了自动添加的 UA 和 host 字段外,红框中的字段被提前了,代码中其实是放到最后的。
header 有序
那么应该如何保证 header 有序呢?来看代码:
import requests
import urllib3
urllib3.disable_warnings()
h = {
'Connection': 'keep-alive',
'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"',
'Accept': 'application/json, text/javascript, */*; q=0.01',
'X-Requested-With': 'XMLHttpRequest',
'sec-ch-ua-mobile': '?0',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Dest': 'empty',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.9',
}
session = requests.Session()
# 别忘了 clear 方法
session.headers.clear()
session.headers.update(h)
params = {'page': 1}
url = 'https://httpbin.org/post'
r = session.post(url, data=params, verify=False, proxies={'https': 'http://127.0.0.1:9090'})
使用 session 来发送请求,并且在 session 初始化之后先要清除之前的 header,然后传入我们已经定义好顺序的 header,即可控制 header 的顺序,看下效果:
除了自动生成的 host 和 UA 之外, 其他的 header 都是按照我们传入的顺序发送的请求,这样就达到控制 header 的顺序的目的了。
源代码分析
那么为什么会这样呢?直接传入的 header 为什么不能保持有序呢?我们从源代码中来寻找答案。
session 方式
打个断点,开始调试,先看为什么 session 可以让 header 保持有序:
可以看到,在 session 初始化后,header 就已经被初始化了三个值进去,这就是为什么要在 update
之前要 clear
的原因,不 clear
的话,默认的三个 header 会影响我们传入的 header 顺序。
接下来我们看下 header 的 update
方法。
update
方法主要就是判断传入的变量类型,根据不同的类型进行不同的操作,最终将我们传入的字典根据 key
来新增或者覆盖原来的值。
来看 post
方法,这个时候我们传入的 header 已经被更新到 session 中的 header 属性了。
接下来就是 session 的 requests
方法,重点就是这个 prepare
方法,我们进入看一下。
可以看到,request
对象的 headers 属性中,值是一个空的字典,但是 session 的 headers 属性中,是我们传入的排序好的 header,这里使用 merge_setting
方法来将两个 headers 进行合并,我们看下合并的逻辑。
合并逻辑不长,首先判断 session 和 request
是否为 None,然后进行类型判断,接下来初始化 dict_class
,从这里可以看出来,session 的 header 优先级是高于 request
的,也就是说如果 session 中已经有的 header,在 request
中再次传入会覆盖 session 中原有的 header。最后就是删除 value
为 None
的值,返回合并后的 headers。
至于传入的 dict_class
,其实是一个自定义的类似 dict
的对象,因为 http 的 header 是不区分大小写的,所以这里自定义类的作用就是忽略大小写限制,让相同的 header 值进行相互覆盖。其中使用了有序字典来存储我们传入的 header,这也是为什么 header 可以被有序发送的原因。
合并完 header 之后,会进入到 prepare
方法中,对每个 http 请求的每个元素进行准备。我们来看下 prepare
方法对 header 做了什么。
新建了一个字典类,覆盖了原有的 session 的 headers 属性,然后将合并后的 headers 依次放入新建的 headers 对象中,结束。其中 check_header_validity
方法会对每个 header 的 key
和 value
进行检查,确保它们符合 http 规范。
接下来就是调用了 session 的 send
方法,在 send
方法中,会通过 get_adapter
方法获取可用的 adapter
适配器,然后调用 adapter
的 send
方法完整最终请求的发送。这里提一下,因为我们在 headers 中没有添加 host 和 UA,所以在最终请求发送时会自动被添加上,这里就不深究了。
post 方式
看完了以上的流程,那为什么直接使用 requests.post
的方式不能保持请求头的顺序呢?
玄机就在这里,如果不使用 session 的话,session 会被隐式初始化,其实最终也是通过 session 发送的请求,只不过 requests 帮我们初始化了一个一次性的 session,session 会设置一个默认的 headers,再结合前面说的,session 的 header 优先级比较高,也就是说这四个 header 就会排在最前面,顺序无法改变了,这就是 post 方式顺序不对的原因。
本文章首发于个人博客 LLLibra146’s blog
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。