bigywater.tech
爬虫工程 · 15

请求头顺序与大小写:requests、curl、浏览器实际发出什么

同一个 GET 请求,不同客户端发出的字节并不一样。服务器看得到你发了哪些头、什么顺序、什么大小写。这篇用一个套接字服务器抓下 requests、urllib3、http.client、aiohttp、curl、wget、curl_cffi、Node、真实的 Chrome 发出的原始请求头,再看看在 requests 里自己改头会发生什么。

14 个
真实 Chrome 154 发出的请求头个数(requests 默认 5 个)
8 种
9 个客户端里头名顺序彼此不同的种数
python-urllib3/2.7.0
把 requests 的 User-Agent 设成 None 后,服务器实际收到的值

九个客户端的原始请求头

实测 · 本机执行输出
  requests:GET / HTTP/1.1
      Host: 127.0.0.1:<端口>
      User-Agent: python-requests/2.34.2
      Accept-Encoding: gzip, deflate, zstd
      Accept: */*
      Connection: keep-alive
  urllib3:GET / HTTP/1.1
      Host: 127.0.0.1:<端口>
      Accept-Encoding: identity
      User-Agent: python-urllib3/2.7.0
  http.client:GET / HTTP/1.1
      Host: 127.0.0.1:<端口>
      Accept-Encoding: identity
  aiohttp:GET / HTTP/1.1
      Host: 127.0.0.1:<端口>
      Accept: */*
      Accept-Encoding: gzip, deflate, zstd
      User-Agent: Python/3.14 aiohttp/3.14.3
  curl 命令行:GET / HTTP/1.1
      Host: 127.0.0.1:<端口>
      User-Agent: curl/8.7.1
      Accept: */*
  wget:GET / HTTP/1.1
      Host: 127.0.0.1:<端口>
      User-Agent: Wget/1.25.0
      Accept: */*
      Accept-Encoding: identity
      Connection: Keep-Alive
  curl_cffi:GET / HTTP/1.1
      Host: 127.0.0.1:<端口>
      Accept-Encoding: gzip, deflate, br
  Node 的 fetch:GET / HTTP/1.1
      host: 127.0.0.1:<端口>
      connection: keep-alive
      accept: */*
      accept-language: *
      sec-fetch-mode: cors
      user-agent: node
      accept-encoding: gzip, deflate
  Chrome(headless):GET / HTTP/1.1
      Host: 127.0.0.1:<端口>
      Connection: keep-alive
      sec-ch-ua: "Chromium";v="154", "Google Chrome";v="154", ...
      sec-ch-ua-mobile: ?0
      sec-ch-ua-platform: "macOS"
      Upgrade-Insecure-Requests: 1
      User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_...
      Accept: text/html,application/xhtml+xml,application/x...
      Sec-Fetch-Site: none
      Sec-Fetch-Mode: navigate
      Sec-Fetch-User: ?1
      Sec-Fetch-Dest: document
      Accept-Encoding: gzip, deflate, br, zstd
      Accept-Language: en-US,en;q=0.9

HTTP/1.1 里头名不分大小写,但服务器看得到你发的原始字节。requests 默认发 5 个头;urllib3 和 http.client 更少(http.client 只有 Host 和 Accept-Encoding: identity);curl_cffi 默认也只有 Host 和 Accept-Encoding;Node 的 fetch 所有头名都是小写;真实的 Chrome 154 发 14 个,包括 sec-ch-ua*、Sec-Fetch-*、Upgrade-Insecure-Requests、Accept-Language。

头的顺序和大小写

实测 · 本机执行输出
  requests:Host, User-Agent, Accept-Encoding, Accept, Connection
  urllib3:Host, Accept-Encoding, User-Agent
  http.client:Host, Accept-Encoding
  aiohttp:Host, Accept, Accept-Encoding, User-Agent
  curl 命令行:Host, User-Agent, Accept
  wget:Host, User-Agent, Accept, Accept-Encoding, Connection
  curl_cffi:Host, Accept-Encoding
  Node 的 fetch:host, connection, accept, accept-language, sec-fetch-mode, user-agent, accept-encoding
  Chrome(headless):Host, Connection, sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform, Upgrade-Insecure-Requests, User-Agent, Accept, Sec-Fetch-Site, Sec-Fetch-Mode, Sec-Fetch-User, Sec-Fetch-Dest, Accept-Encoding, Accept-Language
  9 个客户端里,头名(忽略大小写)的顺序彼此不同的有 8 种;区分大小写后有 8 种
  顺序完全相同(忽略大小写)的客户端:['http.client', 'curl_cffi']
  Host 头是第一个头的客户端:['requests', 'urllib3', 'http.client', 'aiohttp', 'curl 命令行', 'wget', 'curl_cffi', 'Node 的 fetch', 'Chrome(headless)']
  头名全是小写的客户端:['Node 的 fetch']

9 个客户端里,头名顺序彼此不同的有 8 种;只有 http.client 和 curl_cffi 恰好相同。全部以 Host 开头。Chrome 的头名大小写混合(sec-ch-ua* 小写,其余首字母大写)。服务器如果愿意,可以用这些特征区分客户端——这是为什么“把 User-Agent 改成浏览器的”并不能让请求看起来像浏览器。

在 requests 里改头

实测 · 本机执行输出
  默认:['Host', 'User-Agent: python-requests/2.34.2', 'Accept-Encoding: gzip, deflate, zstd', 'Accept', 'Connection']
  headers={"x-custom": "1", "User-Agent": "my"}:['Host', 'User-Agent: my', 'Accept-Encoding: gzip, deflate, zstd', 'Accept', 'Connection', 'x-custom']
  headers={"user-agent": "my"}(小写名字覆盖默认的 User-Agent):['Host', 'user-agent: my', 'Accept-Encoding: gzip, deflate, zstd', 'Accept', 'Connection']
  headers={"Accept": "text/html", "User-Agent": "my", "Accept-Encoding": "gzip"}(想改顺序):['Host', 'User-Agent: my', 'Accept-Encoding: gzip', 'Accept', 'Connection']
  headers={"User-Agent": None}(去掉默认头):['Host', 'User-Agent: python-urllib3/2.7.0', 'Accept-Encoding: gzip, deflate, zstd', 'Accept', 'Connection']
  headers={"Connection": None, "Accept-Encoding": None, "Accept": None}:['Host', 'Accept-Encoding: identity', 'User-Agent: python-requests/2.34.2']
  Session.headers.clear() 后只加 Accept:['Host', 'Accept-Encoding: identity', 'User-Agent: python-urllib3/2.7.0', 'Accept']
  同时给出 "X-A" 和 "x-a" 两个同名头(大小写不同):['Host', 'User-Agent: python-requests/2.34.2', 'Accept-Encoding: gzip, deflate, zstd', 'Accept', 'Connection', 'x-a: 2']

自己加的头排在默认头之后;默认头的位置固定,即使你传入的字典顺序不同也不会重排;用小写名字覆盖默认头,位置不变但名字的大小写按你给的来。把某个默认头设成 None 不一定能去掉:User-Agent 被去掉后,下层的 urllib3 又补上了自己的 python-urllib3/2.7.0;Accept-Encoding 去掉后 http.client 补上 identity。大小写不同的同名头在请求里只保留最后一个。

分步演示

请求头顺序与大小写
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / requests 2.34.2 / aiohttp 3.14.3 / lxml 6.1.1(系统 Python);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、断连、压缩、重定向等行为,并记录收到的原始请求;计时用 time.perf_counter,数字每次运行会有小幅抖动。服务器是套接字服务器,读到 \r\n\r\n 后按字节原样解析头;curl 8.7.1(系统自带)、wget 1.25.0、Node v26.10.0 的 fetch、Chrome 154.0.8037.98(headless,独立的 --user-data-dir,--no-proxy-server,请求到达后结束这批进程)、curl_cffi 0.15.0 默认设置、aiohttp 3.14.3、urllib3 2.7.0、http.client 为 Python 3.14.8;各客户端各抓一次请求
    • 没有验证:HTTP/2 下头名强制小写及伪头(:method 等)的顺序;带 Cookie、请求体时的头顺序;curl_cffi 的 impersonate 功能(没有测,也不在本文范围);其它浏览器(Firefox、Safari)的头;非 headless 的 Chrome 与 headless 的差异;同一客户端不同版本的差异;服务器按头顺序识别客户端的实际做法(我只展示了“能区分”,没有看任何真实服务器)
    京ICP备2026067502号-1京公网安备11010502063487号