Claude帮我优化爬虫请求头,顺序和大小写模拟真实浏览器,但漏了Accept-Encoding,导致返回压缩内容
发布时间:2026/9/27 1:34:54来源:尧图网络
引言上个月在爬一个汽车资讯网站文章列表和详情页都是服务端渲染的按理说用requests就能搞定。但跑起来之后频繁被拦返回403或者一个空白的访问被拒绝页面。我排查了一圈UA换了、Referer加了、Cookie也带了还是不行。后来我想到一个细节——有经验的爬虫玩家都知道现代反爬系统不仅看你请求头里有什么还看请求头的顺序和大小写格式。真实浏览器的请求头是有固定顺序的比如Host在最前User-Agent靠前Accept-Encoding在中间而Python的requests默认会按字典顺序排序而且大小写也不对比如user-agentvsUser-Agent。这些细节可能暴露爬虫身份。于是我打开Claude帮我优化一下请求头要模拟真实Chrome浏览器的完整头部包括顺序和大小写。Claude很快给了一套完整的Header配置用OrderedDict保证了顺序大小写也跟Chrome一致。我照着替换请求成功率果然上去了从40%涨到了80%。但好景不长我发现有些请求返回的内容是乱码——一堆看不懂的二进制字符用json.loads解析报错用BeautifulSoup解析出来也是空。我打开日志一看响应的Content-Type是application/json但内容是一堆\x1f\x8b\x08开头的字节。我当时就懵了明明请求成功了怎么返回的内容是乱的折腾了半天才反应过来Claude给的请求头里加了Accept-Encoding: gzip, deflate, br告诉服务器我支持压缩服务器就真的返回了gzip压缩的内容。但我拿到响应后直接当文本处理了没有解压。而requests默认会自动解压gzip——前提是你没有手动设置Accept-Encoding。Claude优化了请求头的外观却忘了告诉我要处理压缩这个副作用。这篇文章完整复盘这次请求头优化的踩坑过程。读完你会掌握请求头顺序和大小写对反爬检测的真实影响Accept-Encoding的工作原理以及为什么手动设置它会导致requests不再自动解压三种处理压缩内容的方案手动解压、禁用压缩、用requests的自动解压一套完整且安全的请求头配置模板问题复现现象描述先给你看看Claude给我的优化后请求头fromcollectionsimportOrderedDictimportrequests# Claude说用OrderedDict保证顺序大小写跟Chrome一致headersOrderedDict([(Host,www.example.com),(Connection,keep-alive),(Cache-Control,max-age0),(sec-ch-ua,Chromium;v136, Google Chrome;v136),(sec-ch-ua-mobile,?0),(sec-ch-ua-platform,Windows),(Upgrade-Insecure-Requests,1),(User-Agent,Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36),(Accept,text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8),(Sec-Fetch-Site,none),(Sec-Fetch-Mode,navigate),(Sec-Fetch-User,?1),(Sec-Fetch-Dest,document),(Accept-Encoding,gzip, deflate, br),(Accept-Language,zh-CN,zh;q0.9,en;q0.8),])resprequests.get(https://www.example.com/api/list,headersheaders)print(resp.status_code)# 200print(resp.text[:200])# 输出一堆乱码打印出来的内容是这样的\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\xec\xbd\x...resp.status_code是200resp.headers[Content-Type]是application/json但resp.text就是一堆乱码。用json.loads(resp.text)直接报JSONDecodeError。我的错误思路第一反应是服务器返回了错误的格式但我在浏览器里打开同一个接口返回的JSON完全正常。说明不是服务器的问题。然后我怀疑是不是编码问题尝试用resp.content.decode(utf-8)、resp.content.decode(gbk)甚至resp.content.decode(latin-1)都是乱码。又想到是不是被反爬系统返回了假数据但如果是假数据应该是明文HTML或者JSON而不是这种二进制乱码。我盯着那串\x1f\x8b\x08看了半天突然想到\x1f\x8b是gzip文件的魔数。也就是说服务器返回的是gzip压缩后的内容而我的代码没有解压直接把压缩后的字节当文本读了。根因找到了Claude的请求头里设置了Accept-Encoding: gzip, deflate, br告诉服务器我接受压缩内容。服务器很配合地返回了gzip压缩的数据。但我拿到响应后没有解压所以看到的是乱码。根因分析这里面有个容易忽略的细节requests库默认会自动处理gzip压缩但前提是它自己设置了Accept-Encoding。requests的默认行为是如果没有手动设置Accept-Encodingrequests会自动加上Accept-Encoding: gzip, deflate不含br收到响应后requests检测到Content-Encoding: gzip自动解压resp.text返回的是解压后的明文但如果你手动设置了Accept-Encoding比如Claude加上了brrequests就以为你知道自己在干什么不再自动处理解压——尤其是brBrotli这种压缩格式老版本的requests根本不支持解压。场景Accept-Encodingrequests行为resp.text默认自动添加gzip, deflate自动解压明文手动设gzipgzip, deflate部分情况仍自动解压明文取决于版本手动设brgzip, deflate, br不自动解压br乱码手动设identityidentity服务器返回未压缩明文核心问题Claude为了模拟真实浏览器加上了brBrotli压缩但Python的requests对Brotli支持有限导致服务器返回了Brotli压缩内容而requests无法解压。解决方案下面是我的完整修复过程一共5步。第一步确认响应是否被压缩目标先确认返回的内容确实是压缩的以及压缩格式是什么。操作importrequestsfromcollectionsimportOrderedDict headersOrderedDict([# ... Claude给的完整头部(Accept-Encoding,gzip, deflate, br),])resprequests.get(https://www.example.com/api/list,headersheaders)# 检查响应头中的Content-Encodingprint(Content-Encoding:,resp.headers.get(Content-Encoding))# 输出br 或 gzip# 检查原始字节的魔数print(前4字节:,resp.content[:4])# gzip的魔数是 \x1f\x8b# br的魔数通常是 \xce\xb2\xcf\x81 或类似# 如果看到这些说明内容被压缩了# 检查requests是否自动解压对比content和textprint(content长度:,len(resp.content))print(text长度:,len(resp.text))# 如果text长度远小于content说明解压了# 如果两者接近且都是乱码说明没解压运行验证如果Content-Encoding显示br或gzip且resp.text包含乱码字符就确认了问题。第二步方案一——手动解压Brotli内容目标如果你确实需要发送br比如服务器对Brotli有偏好可以手动解压。操作# 先安装brotli库pipinstallbrotliimportrequestsimportbrotlifromcollectionsimportOrderedDict headersOrderedDict([# ... 其他头部(Accept-Encoding,gzip, deflate, br),])resprequests.get(https://www.example.com/api/list,headersheaders)# 根据Content-Encoding判断压缩格式encodingresp.headers.get(Content-Encoding,).lower()ifencodingbr:# Brotli压缩手动解压# 为什么这么改requests不自动解压br需要手动调用brotli.decompressdecompressedbrotli.decompress(resp.content)textdecompressed.decode(utf-8)elifencodinggzip:# gzip压缩用gzip库解压或requests通常会自动处理importgzip decompressedgzip.decompress(resp.content)textdecompressed.decode(utf-8)elifencodingdeflate:importzlib decompressedzlib.decompress(resp.content)textdecompressed.decode(utf-8)else:# 没有压缩直接用texttextresp.textprint(text[:200])# 现在应该正常了运行验证打印text[:200]如果看到正常的JSON或者HTML内容说明解压成功。第三步方案二——改用requests的默认自动解压目标如果不需要br最简单的方式是让requests自己处理——不手动设置Accept-Encoding。操作importrequests# 不设置Accept-Encoding让requests自己加headers{User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...,Accept:text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8,Accept-Language:zh-CN,zh;q0.9,# 关键不要写Accept-Encoding这一行}resprequests.get(https://www.example.com/api/list,headersheaders)print(resp.status_code)# 200print(resp.text[:200])# 明文requests已自动解压gziprequests会自动添加Accept-Encoding: gzip, deflate服务器返回gzip压缩内容requests收到后自动解压resp.text返回明文。运行验证打印resp.headers.get(Accept-Encoding)注意这是响应头不是请求头虽然响应头里不会有这个字段但可以通过resp.text是否正常来判断。如果text是明文说明自动解压生效。第四步方案三——手动设置为identity禁用压缩目标有些反爬系统会检测Accept-Encoding是否包含br因为现代浏览器都支持br。如果你必须保留br来通过检测但不想处理解压可以设置Accept-Encoding: identity告诉服务器不要压缩。操作importrequestsfromcollectionsimportOrderedDict headersOrderedDict([# ... 其他头部保持Claude给的顺序和大小写(Accept-Encoding,identity),# 告诉服务器不要压缩(Accept-Language,zh-CN,zh;q0.9,en;q0.8),])resprequests.get(https://www.example.com/api/list,headersheaders)print(resp.status_code)print(resp.text[:200])# 明文因为服务器没压缩注意设置identity可能会被一些反爬系统识别为非浏览器行为——真实浏览器不会主动要求identity。所以这个方案只适合那些不检测Accept-Encoding值的网站。运行验证检查响应头中的Content-Encoding字段。如果为空或不存在说明服务器没有压缩resp.text应该是明文。第五步封装成带自动解压的请求函数目标把以上方案整合成一个工具函数自动检测压缩格式并解压同时保留Claude优化的请求头顺序和大小写。操作importrequestsimportgzipimportzlibfromcollectionsimportOrderedDict# 检测brotli是否可用try:importbrotli HAS_BROTLITrueexceptImportError:HAS_BROTLIFalsedefbuild_browser_headers(host,url_path/): 构建模拟Chrome的请求头保持顺序和大小写 returnOrderedDict([(Host,host),(Connection,keep-alive),(Cache-Control,max-age0),(sec-ch-ua,Chromium;v136, Google Chrome;v136),(sec-ch-ua-mobile,?0),(sec-ch-ua-platform,Windows),(Upgrade-Insecure-Requests,1),(User-Agent,Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36),(Accept,text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8),(Sec-Fetch-Site,none),(Sec-Fetch-Mode,navigate),(Sec-Fetch-User,?1),(Sec-Fetch-Dest,document),(Accept-Encoding,gzip, deflate, brifHAS_BROTLIelsegzip, deflate),(Accept-Language,zh-CN,zh;q0.9,en;q0.8),])deffetch_with_decompress(url,headersNone,timeout15): 带自动解压的请求函数 为什么这么改处理服务器返回的压缩内容避免resp.text乱码 ifheadersisNone:fromurllib.parseimporturlparse parsedurlparse(url)headersbuild_browser_headers(parsed.netloc,parsed.path)resprequests.get(url,headersheaders,timeouttimeout)# 检查Content-Encoding决定是否手动解压encodingresp.headers.get(Content-Encoding,).lower()ifencodingbr:ifnotHAS_BROTLI:raiseRuntimeError(服务器返回Brotli压缩但brotli库未安装请执行 pip install brotli)contentbrotli.decompress(resp.content)elifencodinggzip:try:contentgzip.decompress(resp.content)exceptException:# 如果gzip解压失败可能requests已经自动解压了contentresp.contentelifencodingdeflate:try:contentzlib.decompress(resp.content)exceptException:contentresp.contentelse:contentresp.content# 尝试用utf-8解码失败则用requests的编码检测try:textcontent.decode(utf-8)exceptUnicodeDecodeError:# 用requests的apparent_encoding作为后备textcontent.decode(resp.apparent_encodingorutf-8,errorsreplace)returntext,resp# 使用urlhttps://www.example.com/api/listtext,respfetch_with_decompress(url)print(f状态码:{resp.status_code})print(f内容长度:{len(text)})print(f前200字符:{text[:200]})这个函数的逻辑是构建带完整浏览器头部顺序的请求头发送请求根据Content-Encoding判断压缩格式手动解压解码为文本运行验证用之前返回乱码的URL测试观察text是否为正常的JSON或HTML。如果内容开头是{或说明解压成功。最终对比方案优点缺点手动解压br保留完整浏览器头部需要额外安装brotli不设Accept-Encoding简单自动解压请求头不够像真实浏览器设为identity简单不用解压容易被识别为爬虫封装工具函数自动处理健壮代码稍复杂我最终选择了封装工具函数同时根据brotli库是否安装来动态决定是否在请求头中加br。这样既保持了请求头的真实性又避免了乱码问题。复盘与避坑建议这个坑的本质请求头优化不只是加字段还要处理字段带来的副作用。Claude为了模拟真实浏览器加上Accept-Encoding: gzip, deflate, br从伪装角度是对的——真实Chrome确实会发送这些值。但它没有告诉我这个字段会导致服务器返回压缩内容而requests对br的支持需要额外处理。更深层的教训是HTTP协议里的请求头和响应头是联动的改一个请求头就要想清楚它会影响响应头。Accept-Encoding影响Content-EncodingRange影响Content-RangeIf-Modified-Since影响304 Not Modified。这些联动关系在优化请求头时必须考虑。3条避坑建议手动设置Accept-Encoding时一定要确认requests的版本和解压能力。旧版requests不支持br解压要么升级requests要么安装brotli库要么不要在请求头里写br。用resp.content和resp.text对比检查是否解压。如果两者长度接近且都是乱码说明没有自动解压如果text长度远小于content且是明文说明解压成功。通用爬虫工具函数里永远加一层根据Content-Encoding自动解压的逻辑。不要依赖requests的自动解压因为一旦你手动设置了某些头部比如Accept-Encoding自动解压可能就不生效了。自己写一个解压分发函数兼容gzip、deflate、br三种格式。产出总结本篇涉及的新增/修改文件文件作用browser_headers.py构建模拟Chrome的请求头保持顺序和大小写http_utils.py带自动解压的请求函数兼容gzip/deflate/brcrawler_with_headers.py使用新请求头的爬虫示例requirements.txt新增依赖brotli改完之后我的爬虫请求成功率维持在80%以上靠请求头顺序和大小写骗过了反爬同时再也没出现过乱码问题。brotli库的安装成本很低但避免了一类很难排查的bug。记住优化请求头是在骗过服务器但骗完之后要记得把服务器给你的回礼压缩内容拆开看。不然你手里捧着的是一堆加了密的礼物还以为自己拿到了空盒子。互动引导你在优化爬虫请求头时踩过什么坑是顺序、大小写还是Accept-Encoding有没有遇到过返回乱码的情况欢迎在评论区分享你的经历每条评论我都会认真回复。关注我然后通过CSDN后台私信发送爱学Python可以领取《Python全栈学习路线图》2026最新版。如需交流可前往 https://bbs.csdn.net/topics/620104702 加入技术讨论。
网站建设高端定制企业官网