
1. 写在前面为什么HTTP请求值得你花时间彻底搞懂先聊个实际场景。很多刚接触Python爬虫或者接口调试的朋友上手就是requests.get(url)一把梭能拿到数据就万事大吉。但真到了工作中你会发现光是“请求能通”远远不够——别人写的代码换个网站就挂你的代码能扛住各种反爬策略别人遇到超时就干瞪眼你能从状态码和响应头里一眼看出问题在哪。差距就藏在今天要聊的这两个基础但极其重要的东西里HTTP请求头和状态码。这篇内容我打算用最直白的方式把Python里get和post请求从头到尾拆一遍。不光是教你怎么调库发请求更重要的是讲清楚背后的HTTP协议逻辑、请求头每个字段到底是干什么的、状态码怎么读、常见的坑在哪里。不管你是刚开始学爬虫还是已经在写接口自动化脚本这篇文章都能帮你把基础补扎实。毕竟所有花里胡哨的框架和高级玩法最后都绕不开这一层最原始的交互。2. 请求的本质客户端和服务端的一次对话2.1 把HTTP请求拆成四部分来看我们在Python里发一条requests.get()看起来就是一行代码但实际上这一行代码在网络上展开之后是一个结构完整的“对话请求”。HTTP请求报文由四部分组成请求行、请求头、空行、请求体。get请求通常没有请求体post请求才把数据放在请求体里。拿实际的报文来举个例子当你执行import requests resp requests.get(https://httpbin.org/get)这背后发送的原始报文大致长这样GET /get HTTP/1.1 Host: httpbin.org User-Agent: python-requests/2.31.0 Accept-Encoding: gzip, deflate Accept: */* Connection: keep-alive这里第一行GET /get HTTP/1.1就是请求行包含了请求方法GET、请求路径/get和HTTP版本。接下来几行都是请求头Headers空行之后理论上还有请求体但GET通常没有。服务端收到这个请求后会返回一个响应报文同样也是四部分状态行、响应头、空行、响应体。状态行就是大家熟悉的HTTP/1.1 200 OK响应头则是一堆关于返回内容的描述信息响应体才是真正我们要的数据。理解这个结构是看懂后面所有坑的基础。很多初学者调试接口出问题就是因为只盯着响应体看忽略了请求头和状态行里藏着的关键线索。2.2 GET和POST的本质区别关于GET和POST的区别网上流传的说法很多什么“GET把数据放在URL里POST放在请求体里”、“GET比POST快”、“POST比GET安全”这些说法都有一定的道理但都不够准确。真正的核心区别在于语义GET是幂等的、用于获取资源POST是非幂等的、用于提交处理。打个比方GET就像你站在货架前看商品标签不管看多少次货架上的东西都不会变POST就像你把商品放进购物车提交订单每提交一次订单系统里就多一条记录。我带过的一些新手在写登录接口的时候喜欢把用户名密码放在GET请求的URL参数里。这么做的问题在于URL会出现在历史记录、服务器日志、代理日志里等于把凭证写在额头上到处晃。而POST请求把数据放在请求体里配合HTTPS加密安全性要好得多。但这不意味着POST就一定安全——如果你用POST但没走HTTPS数据同样是明文传输。所以在实际工程里选择GET还是POST主要看两个标准这次操作会不会改变服务器上的数据会就用POST不会优先GET。参数是否敏感敏感参数走POST的请求体。3. Python实现GET请求的完整拆解3.1 基础GET请求的三层写法先用标准库urllib写一个最原始的GET请求再对比requests库的写法这样你能清楚知道requests帮你省了哪些事。# 方式一urllib标准库 from urllib.request import urlopen resp urlopen(https://httpbin.org/get) # 这里urlopen默认发的是GET请求 print(resp.status) # 状态码200 print(resp.read().decode(utf-8)) # 响应体需要自己手动解码这种写法的痛点很明显响应体要手动解码状态码要自己去拿属性处理请求头更是麻烦。所以实际开发中我更推荐用requests库。# 方式二requests库 import requests resp requests.get(https://httpbin.org/get) print(resp.status_code) # 状态码 print(resp.text) # 自动解码后的响应内容 print(resp.json()) # 如果返回的是JSON直接解析成字典这两段代码干的是同一件事但requests把你的工作量减少了一大半。尤其是resp.json()这个方法处理接口返回的JSON数据时极其方便。3.2 GET请求携带参数的两种姿势GET请求要传参数最常见的是查询字符串Query String。手动拼URL是这样url https://httpbin.org/get?namezhangsanage25但更规范的做法是用requests的params参数让库帮你处理URL编码import requests params { name: 张三, age: 25, city: 北京 朝阳区 } resp requests.get(https://httpbin.org/get, paramsparams) print(resp.url) # 打印实际请求的完整URL你会看到中文和空格被自动编码了这里有个细节值得注意resp.url打出来的是编码后的结果比如%E5%BC%A0%E4%B8%89这种这是URL编码百分号编码在起作用。如果你手动拼URL很容易忘记对特殊字符做编码导致服务器解析出错。中文、空格、、这些字符在URL里都是特殊处理的。3.3 GET请求如何携带请求头有了参数下一步就是加请求头。为什么要加请求头因为很多服务器会校验客户端的身份信息尤其是User-Agent和Cookie。import requests headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: application/json, Referer: https://www.example.com/ } resp requests.get(https://httpbin.org/get, headersheaders) print(resp.status_code)这里重点讲一下User-Agent。默认情况下requests库发出去的UA是python-requests/2.31.0这种UA在服务器看来就是个“机器人”很多网站会直接拒绝或返回验证码。把UA伪装成浏览器是爬虫入门第一课。还有Referer字段它告诉服务器“我是从哪个页面跳转过来的”。有些网站的图片、接口做了防盗链会校验Referer如果Referer不对就返回403。这时候带上正确的Referer就能绕过限制。3.4 超时设置我踩过的坑不设置超时是新手写爬虫最容易埋下的雷。默认情况下requests.get()会一直等下去如果对方服务器响应慢或者根本不响应你的程序就卡死在那里看起来像“假死”。import requests # 正确的做法设置超时时间 resp requests.get(https://httpbin.org/get, timeout3) # 3秒没响应就抛异常 # 更精细的做法分别设置连接超时和读取超时 resp requests.get(https://httpbin.org/get, timeout(3, 10))timeout(3, 10)的意思是连接服务器最多等3秒连接成功后读取数据最多等10秒。为什么要分开因为有些服务器连接很快但响应数据很慢有些是连都连不上一直卡在握手阶段。分开设置能更精准地判断问题出在哪一段。实际项目中我习惯把超时时间做成配置项比如TIMEOUT_CONNECT 5 TIMEOUT_READ 15 resp requests.get(url, timeout(TIMEOUT_CONNECT, TIMEOUT_READ))使用爬虫批量抓数据时如果不设超时因为某个慢接口导致整个任务队列阻塞那真是灾难现场。4. Python实现POST请求的完整拆解4.1 三种常见POST提交格式POST请求的格式比GET复杂主要体现在请求体Body的编码格式不同。常见的有三种表单格式application/x-www-form-urlencoded、JSON格式application/json、文件上传格式multipart/form-data。如果你老是遇到接口返回“参数错误”大概率就是提交格式不对。第一种表单格式import requests data { username: zhangsan, password: 123456 } resp requests.post(https://httpbin.org/post, datadata) print(resp.text)用data传参requests默认会按表单格式编码请求头里的Content-Type会自动设为application/x-www-form-urlencoded。这种格式适合传统的网页表单提交。第二种JSON格式import requests import json payload { username: zhangsan, password: 123456, remember_me: True } # 方法一用json参数requests自动帮你序列化 resp requests.post(https://httpbin.org/post, jsonpayload) # 方法二手动序列化 headers {Content-Type: application/json} resp requests.post(https://httpbin.org/post, datajson.dumps(payload), headersheaders)推荐直接用jsonpayload这种写法requests会自动做两件事把字典序列化成JSON字符串、把Content-Type设为application/json。现在很多前后端分离的项目接口都是接收JSON格式的数据所以这个格式最常用。第三种文件上传格式import requests files { file: open(report.pdf, rb) } resp requests.post(https://httpbin.org/post, filesfiles) print(resp.text)用files传文件requests会自动生成multipart/form-data格式的请求体。这个在自动化测试上传文件接口时非常常用需要注意的是文件要记得close()或者用with open(...) as f:的上下文管理器写法。4.2 POST请求同时带参数、请求头、Cookie真实场景里POST请求往往不是简单的“一个data走天下”而是要同时处理URL参数、请求头、Cookie等多方面信息。比如我写过一个登录后下载报表的脚本接口需求是URL里带一个task_id请求体是JSON请求头需要带AuthorizationToken请求还需要带上登录后的Cookie。import requests # 1. URL参数 params {task_id: 1024} # 2. 请求体JSON payload { export_type: excel, date_range: 2024-01-01~2024-01-31 } # 3. 请求头 headers { User-Agent: Mozilla/5.0 ..., Authorization: Bearer eyJhbGciOiJIUzI1NiIs..., Content-Type: application/json } # 4. Cookie cookies { session_id: abc123def456, login_token: xyz789 } # 5. 发起请求 resp requests.post( https://api.example.com/report/export, paramsparams, jsonpayload, headersheaders, cookiescookies, timeout(5, 30) ) print(resp.status_code) print(resp.json())这里要特别提醒一个易混点params和json或data是可以同时存在的。URL上的参数走params请求体里的数据走json或data两者互不干扰。很多刚开始写接口脚本的朋友会把所有参数一股脑塞进data里结果URL上该带的参数没带服务器自然报错。4.3 重定向与Session别让登录状态悄悄丢了requests默认会自动跟随重定向。比如你访问一个需要登录的页面服务器返回302跳转到登录页requests会默默帮你完成跳转最终拿到的就是登录页的内容。大多数时候这是好事但有时候你想知道“到底有没有发生重定向”这时候就要看resp.history。resp requests.get(https://www.example.com/redirect-test, allow_redirectsTrue) print(resp.history) # 打印所有重定向过程中的响应对象列表 print(resp.url) # 最终的URLresp.history是一个列表里面是每次重定向的响应对象如果列表为空说明没有发生任何重定向。如果你不希望requests帮你自动跳转可以把allow_redirectsFalse设为False。另外一个更实用的东西是Session。Session对象最大的价值在于跨请求保持Cookie。登录接口返回的Cookie如果不用Session每次请求都得手动携带用了Session它会自动帮你保存和附带。import requests session requests.Session() # 第一次请求登录拿到Cookie login_data {username: zhangsan, password: 123456} resp1 session.post(https://api.example.com/login, jsonlogin_data) # 第二次请求访问需要登录才能看的页面 # Cookie会自动带上不需要手动处理 resp2 session.get(https://api.example.com/profile) print(resp2.json())requests.Session()虽然名字叫Session但它的底层原理是维护了一个CookieJar。你用同一个Session对象发请求时服务器返回的Set-Cookie会被自动存储下一次请求自动带上。这个机制在写需要登录态的爬虫时几乎是必须的比每次手动从响应里提取Cookie再塞到下一请求头里要靠谱得多。5. HTTP请求头每一个字段都有它的脾气5.1 常用请求头字段详解请求头字段有很多但日常开发中真正高频使用的我总结下来就这几个字段名称作用典型场景User-Agent标识客户端身份和浏览器环境绕过简单的UA反爬Referer标识来源页面防盗链、来源统计Cookie携带会话状态保持登录态Authorization携带认证令牌Bearer Token、Basic AuthContent-Type声明请求体的格式表单、JSON、文件上传Accept声明客户端接受的数据类型让服务器返回JSON而非HTMLAccept-Encoding声明支持的压缩算法gzip、deflate、brX-Requested-With标记是否为Ajax请求有些接口需要这个字段才返回数据这里挑几个重点说一下。Accept这个字段很多人容易忽略。默认的Accept: */*意思是“什么类型都行”服务器可能给你返回HTML页面而不是JSON数据。如果你明确只需要JSON可以设置Accept: application/json服务器通常就会按这个格式返回。Accept-Encoding同样值得注意。如果这个字段包含gzip服务器会返回压缩后的响应体浏览器和requests库会自动解压所以一般感受不到差异。但在某些调试工具里你可能看到一堆乱码那其实是压缩后的数据没解压。X-Requested-With是Ajax请求特有的标记。有些后端接口会判断这个字段如果发现不是Ajax请求就返回一个完整的HTML页面而不是数据。带上X-Requested-With: XMLHttpRequest往往能“骗”过这类接口。5.2 响应头里的关键信息请求头是发出去的信件封面响应头则是对方回信时贴的标签。常见的响应头字段有Content-Type响应体的媒体类型告诉你返回的是JSON、HTML、图片还是文件流。Content-Length响应体长度单位字节可以用它判断数据是否完整接收。Set-Cookie服务器要求客户端设置的CookieSession机制的根基。Location配合3xx状态码使用表示重定向的目标URL。Cache-Control缓存策略no-cache说明内容不可缓存max-age3600说明可以缓存一小时。实际排查问题的时候Content-Type是最先看的。比如你请求一个接口返回的明明是个JSON数据但Content-Type却写着text/html那大概率是服务器没识别到你的Accept头或者路由出了问题。5.3 常见反爬和请求头校验爬虫写多了你就会发现服务器校验请求头的方式五花八门。最简单的是校验User-Agent高级一点会校验Referer更狠的会用JS动态生成Cookie或者Token还有的会校验请求头中字段的顺序这个非常变态但确实存在。常规的应对思路是先用浏览器F12开发者工具打开Network面板找到实际发出的请求把里面的请求头完整复制下来基本就能绕过大多数简单的校验。headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Accept-Encoding: gzip, deflate, br, Connection: keep-alive, Upgrade-Insecure-Requests: 1 }这样一套完整的浏览器请求头在大多数场景下已经够用了。但要注意请求头不是越全越好——某些服务器反而会检测“异常完整”的请求头组合比如一个声称是Chrome浏览器的请求却同时包含了Upgrade-Insecure-Requests和缺失某些Chrome必带字段也会被识别出来。最好的办法就是抓真实浏览器的请求包原样模拟不要自己发挥。6. 状态码服务器对你说的话6.1 五类状态码的语义和判断逻辑HTTP状态码本质上是服务器在处理你的请求后返回的三位数字第一位数字代表大类。网络上一堆人背状态码表说实话没必要全背下来但是下面这些高频状态码必须做到看到数字立刻反应出含义。1xx信息性响应服务器还在处理中或者已经收到了请求但还在犹豫要不要给你完整响应。常见的100 Continue意思是“你可以继续发送请求体”。这类状态码在实际爬虫开发中很少直接遇到因为requests已经帮你处理掉了。2xx成功200 OK请求成功响应体内容就是你要的东西。201 Created资源创建成功多用于POST请求创建新数据。204 No Content请求成功但没有返回内容比如删除操作。3xx重定向301 Moved Permanently资源永久移动到新地址以后请直接用新地址。302 Found资源临时移动服务器会在响应头Location字段给出新地址。304 Not Modified内容没修改过你可以直接用本地缓存。这里有个实际场景经常遇到你用requests.get()请求一个URL结果发现响应体内容不对页面内容怎么跟浏览器看到的不一样先看resp.url是不是变了再看resp.history里有没有301或302记录大概率是服务器做了重定向。4xx客户端错误400 Bad Request请求格式有误常见原因是参数格式不对、请求体格式不对。401 Unauthorized没带认证信息或认证失败。403 Forbidden服务器理解你的请求但拒绝执行。可能是权限不够也可能是被反爬了。404 Not Found资源不存在。可能是URL错了也可能是服务器故意隐藏资源。429 Too Many Requests请求太频繁被限流了。5xx服务器错误500 Internal Server Error服务器内部故障问题在服务端。502 Bad Gateway网关或代理服务器收到上游服务器的无效响应。503 Service Unavailable服务器暂时过载或正在维护。504 Gateway Timeout网关等待上游服务器响应超时。6.2 真实场景下的状态码排查思路新手最常见的问题是一看到非200状态码就不知道怎么办或者直接忽略继续解析。正确的处理方式是把状态码作为排查的第一线索。拿到一个异常状态码我会按照下面的逻辑走确认状态码属于哪个大类。4xx是客户端问题5xx是服务端问题先定责任方。查看响应体的内容。很多服务器虽然返回了4xx或5xx但响应体里会有具体的错误描述信息比如{error: invalid parameter}这比状态码本身更有价值。查看响应头。Retry-After字段常见于429响应告诉你多久后再试。检查请求头。403大概率是请求头没伪装好401大概率是Token过期。import requests resp requests.get(https://httpbin.org/status/403) # 先看状态码 if resp.status_code 200: print(resp.json()) elif resp.status_code 403: print(被拒绝了先检查请求头和Cookie) elif resp.status_code 429: retry_after resp.headers.get(Retry-After, unknown) print(f请求太频繁建议等 {retry_after} 秒后重试) else: # 打印响应体看具体错误信息 print(resp.text)这个判断逻辑看着简单实际用起来非常顺手。尤其是429状态码配合Retry-After响应头做限流处理比盲目的time.sleep(1)要优雅得多。6.3 用断言和异常处理来强化代码在编写接口自动化脚本时我习惯把状态码校验写成一个通用函数避免每次请求都重复判断。import requests def check_response(resp, expected_code200): if resp.status_code ! expected_code: raise Exception( f请求失败状态码 {resp.status_code} fURL: {resp.url}响应内容: {resp.text[:200]} ) return resp # 使用示例 resp requests.get(https://httpbin.org/status/200) check_response(resp) # 状态码不是200就抛异常另外requests库本身遇到网络层面的错误连接不上、超时会抛出异常但遇到HTTP层面的错误4xx、5xx不会抛异常——它只是把状态码和响应体正常返回。这个设计逻辑要搞清楚别把网络异常和HTTP错误混为一谈。问题类型表现形式对应处理网络异常抛ConnectionError、Timeout异常try/except捕获HTTP错误正常返回resp但状态码是4xx/5xx判断status_code编码错误resp.text乱码检查响应头charset7. 实战案例模拟登录与数据抓取7.1 完整案例场景把前面的知识串起来我写了一个模拟登录并抓取个人订单列表的完整Demo。这个案例非常贴近真实的爬虫工作流先用POST登录拿Token再用GET带Token访问数据接口。import requests # 创建Session对象维护登录状态 session requests.Session() # 基础请求头 BASE_HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: application/json, } def login(username, password): 登录接口返回是否成功 login_url https://api.example.com/user/login login_payload { username: username, password: password } resp session.post(login_url, jsonlogin_payload, headersBASE_HEADERS, timeout(5, 10)) if resp.status_code ! 200: raise Exception(f登录失败状态码: {resp.status_code}错误: {resp.text[:200]}) token resp.json().get(token) if not token: raise Exception(响应中未找到token) # 把Token加入后续请求的请求头 BASE_HEADERS[Authorization] fBearer {token} print(登录成功) return token def get_orders(page1, page_size20): 获取订单列表 orders_url https://api.example.com/orders params { page: page, page_size: page_size } resp session.get(orders_url, paramsparams, headersBASE_HEADERS, timeout(5, 10)) if resp.status_code 401: print(Token失效需要重新登录) return [] elif resp.status_code 429: print(请求太频繁被限流了) return [] if resp.status_code ! 200: raise Exception(f获取订单失败状态码: {resp.status_code}) return resp.json().get(orders, []) # 执行流程 token login(zhangsan, 123456) orders_page1 get_orders(page1) print(f第一页共 {len(orders_page1)} 条订单)这个代码示例里最核心的点是Session对象的使用。登录后拿到的Token被放进了Session之后get_orders里虽然也传了headersBASE_HEADERS但Cookie和Session内部的CookieJar是共享的——如果你的登录接口返回了Set-Cookie那么后续请求的Cookie也会被Session自动带上完全不需要手动处理。7.2 踩坑记录登录成功但请求还是401写这段Demo的时候我故意留了一个常见的坑如果登录接口返回的Token是在响应体里而你又用headersBASE_HEADERS覆盖了Authorization那没问题但如果Token是通过Set-Cookie传递的你用同一个Session会自动带上。怕就怕你用requests.post()而不是session.post()登录然后用Session去发后续请求Cookie对不上自然就401了。# 错误写法 requests.post(login_url, jsonlogin_payload) # 独立的请求不保存Cookie session.get(orders_url) # 这个Session里什么都没有服务器不认识你 # 正确写法 session.post(login_url, jsonlogin_payload) # Cookie保存在Session里 session.get(orders_url) # 自动携带Cookie7.3 会话保持的一些补充技巧Session还有一个很实用的能力设置全局请求头和全局超时时间。这样你在一个Session下发的所有请求都会自动带上这些设置。import requests session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 ..., Accept: application/json }) session.timeout (5, 10) # 全局超时 # 后续请求只需要关注URL和数据本身 resp1 session.get(https://api.example.com/api1) resp2 session.post(https://api.example.com/api2, json{key: value})Session还有一个参数proxies用来设置代理。有些时候你调试接口需要抓包就可以把请求指向本地代理工具来查看原始流量。proxies { http: http://127.0.0.1:8888, https: http://127.0.0.1:8888 } resp session.get(https://api.example.com, proxiesproxies, verifyFalse)verifyFalse表示跳过SSL证书验证。这个参数在调试阶段有用但是别在生产环境随便关掉证书验证中间人攻击的风险不是闹着玩的。8. 常见问题与排查技巧实录8.1 问题总览快速排查表我把这几年写接口脚本、爬虫过程中遇到的最高频问题整理成了一张表方便你遇到问题时直接对照。现象可能原因排查方法100%出现之后程序卡死未设置超时加timeout参数resp.text返回乱码编码设置错误手动设置resp.encoding登录成功后请求仍401Cookie或Token没带上改用Session或手动携带返回403UA被识别/Referer校验伪造完整请求头返回404URL拼错/参数没编码打印resp.url检查返回429请求频率过高加延时、用Retry-Afterresp.json()报错响应不是JSON或为空先打印resp.text手机端和PC端数据不一致服务器识别了UA修改UA后重试请求超时不稳定网络抖动/代理问题增加重试机制8.2 高频问题详细排查问题一返回结果乱码requests库本身会从响应头里读取编码方式但有些服务器的响应头没有charset字段或者写了错误的值导致解码失败。解决办法是手动指定resp.encoding utf-8 print(resp.text)如果指定了utf-8还是乱码可以尝试utf-8-sig处理带BOM的情况或者gbk老网站常见。不确定的话用requests.utils.get_encoding_from_headers(resp.headers)来看一下requests自动识别的编码再结合肉眼判断。问题二Token经常过期Token过期本身不是bug是服务端的安全策略。但是在接口自动化脚本里频繁过期会影响效率。常规做法是在请求数据接口前加一个Token有效性检查发现401就自动重新登录再重试一次。def request_with_retry(session, method, url, **kwargs): 请求失败时自动重试一次重新登录后重试 resp session.request(method, url, **kwargs) if resp.status_code 401: print(Token失效重新登录) login(zhangsan, 123456) return session.request(method, url, **kwargs) return resp问题三接口返回的内容跟浏览器不一致这种情况一般不是因为接口变了而是因为你发出去的请求头跟浏览器的不一样。最常见的差异就是User-Agent。某些手机版网页会返回一套精简数据而PC浏览器返回的是完整数据反过来也一样。解决办法把浏览器实际发出的请求头完整复制过来试试。问题四SSL证书验证失败遇到CERTIFICATE_VERIFY_FAILED错误通常是因为目标网站用了自签名证书或者你本地没装相应的CA证书。调试阶段可以临时用verifyFalse跳过验证但要加上urllib3.disable_warnings()消除警告。import urllib3 import requests urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) resp requests.get(https://self-signed.badssl.com/, verifyFalse)问题五请求被限流这是所有爬虫都会遇到的痛。服务器限流的信号一般是429状态码或者响应体里出现too many requests字样。常规破解思路是降低请求频率加随机延时。用代理IP池轮换。处理Retry-After字段等服务器告诉你再试的时间到了再继续。8.3 定位问题的黄金步骤遇到任何请求异常先别慌着改代码按顺序做下面这几件事打印resp.status_code确定问题类别。打印resp.url确认URL是不是你想要的那个。打印resp.headers检查响应头关键信息。打印resp.text前200个字符看看服务器到底返回了什么内容。如果还是无从下手把resp.request.headers打出来检查你实际发出的请求头对不对。resp requests.get(https://httpbin.org/get, headersheaders) print( 实际请求头 ) print(resp.request.headers) print( 响应头 ) print(resp.headers) print( 响应体前200字 ) print(resp.text[:200])这一步几乎是万能的排查利器。很多问题看到实际发送出去的请求头一眼就能发现是Cookie少了、Content-Type不对还是UA被识别了。9. 我的一些实操心得和个人建议写到这里我再分享几点这几年在实际项目中积累的经验吧。第一点关于请求方式的直觉判断。很多人老是纠结“这里该用GET还是POST”我的建议很直接改变服务器状态的一律POST只是查数据的优先GET。不要为了图省事把该POST的接口改成GET传参后面各种问题会让你后悔。第二点关于请求头的复制粘贴。很多教程推荐“直接复制浏览器的请求头”这话对但要看怎么用。我见过不少人把浏览器里几十个请求头字段全部复制到代码里结果反而触发了风控——因为真实浏览器的请求头字段顺序、大小写习惯跟你在代码里手动拼的压根不一样。我的经验是先抓包看真实请求只挑关键字段带上比如User-Agent、Accept、Content-Type、Referer、Cookie够用就好。第三点关于异常处理的粒度。写爬虫或者接口脚本时异常处理不是笼统地try: ... except: pass。我习惯把网络异常、HTTP错误、数据格式错误分开捕获分别处理并记录日志。这样出了问题第一时间能从日志里看出是哪一类问题而不是一团乱麻。第四点不要小看超时和重试。实际线上环境里极少有接口能保证100%稳定返回200。超时后立刻重试是很多“抗造”脚本的基本功。import time def request_with_retry(url, max_retries3, delay1): 带重试机制的基础请求函数 for attempt in range(max_retries): try: resp requests.get(url, timeout(5, 10)) if resp.status_code 200: return resp elif resp.status_code 429: time.sleep(delay * (attempt 1)) # 线性退避 else: return resp except requests.exceptions.Timeout: print(f第 {attempt 1} 次请求超时) time.sleep(delay) except requests.exceptions.ConnectionError: print(f第 {attempt 1} 次连接失败) time.sleep(delay) raise Exception(重试多次仍然失败)这个函数虽然简单但在稳定性要求高的项目里非常实用。退避策略可以按需调整从固定延时到指数退避都行。学习Python HTTP请求这部分内容最快的路径不是看一堆理论而是拿一个真实网站或接口去调试。先把requests.get跑通然后试着加参数、加请求头、换POST、处理响应头遇到问题就用上面整理的排查方法对照解决。钻进去一次比看十遍教程都有用。