1. 从GET到POST为什么你的爬虫需要换一种说话方式很多人写爬虫的第一个脚本都是requests.get(url)跑得挺顺直到某天遇到一个必须登录才能看到数据的页面或者一个搜索框需要输入关键词才能返回结果的接口才发现GET那一套完全不好使了。这不是代码写错了而是HTTP协议本身的设计逻辑在起作用——GET和POST承担的是两种不同的对话模式。打个比方GET像是你去图书馆借书你把书名写在纸条上递给管理员纸条上的内容一目了然谁都能看到你要借什么。POST则像是你填了一张密封的申请表交上去内容藏在信封里旁人看不到具体写了什么。爬虫面对的场景里凡是涉及登录凭证、表单提交、文件上传、搜索查询这些操作基本都得用POST来递信封。我在实际项目里踩过最典型的坑就是拿GET去请求一个登录接口参数全拼在URL后面结果服务端直接返回400连用户名密码错误这种提示都不给。当时排查了半天以为是参数名写错了后来才意识到是请求方法本身就不对。这个经历让我养成了一个习惯拿到一个接口先看浏览器开发者工具里标注的Request Method是GET还是POST再决定怎么写代码。这篇文章会围绕POST请求的构造方式、Cookies的读写机制、Session的会话保持这三块内容展开把我在实际爬取过程中积累的操作细节、参数计算逻辑、以及那些文档里不会写的坑尽量讲透。不管你是刚学完requests基本用法的新手还是已经写过一些爬虫但总在登录态上翻车的朋友应该都能从中找到能直接用的东西。2. POST请求的构造不只是把参数从params换成data那么简单2.1 请求体的三种编码格式与Content-Type的对应关系POST请求和GET最本质的区别在于数据是放在请求体request body里传输的而不是拼在URL上。但请求体不是随便塞进去就行服务端需要知道你怎么编码的这个信息就藏在请求头Content-Type里。常见的编码格式有三种每种对应的Content-Type和Python构造方式都不一样。第一种是application/x-www-form-urlencoded这是最传统也最常见的表单提交格式。数据被编码成key1value1key2value2的形式和GET的URL参数长得一样只是位置从URL挪到了请求体。用requests构造时直接把字典传给data参数即可requests会自动帮你做URL编码。第二种是application/json现在越来越多的接口尤其是前后端分离的项目偏爱这种格式。数据是JSON字符串嵌套结构也能轻松表达。构造时需要用json参数传字典或者手动json.dumps()后传给data同时确保请求头里Content-Type是application/json。我见过不少人用datajson.dumps(payload)但忘了设请求头结果服务端按表单格式去解析JSON字符串自然拿不到正确的字段。第三种是multipart/form-data主要用于文件上传。这种格式会把数据分成多个部分每部分有自己的边界标识。requests里用files参数来处理同时可以配合data传其他普通字段。编码格式Content-Typerequests参数典型场景表单编码application/x-www-form-urlencodeddatadict传统网页登录、搜索JSON编码application/jsonjsondict前后端分离API、移动端接口多部分表单multipart/form-datafilesdict文件上传、图片提交实际排查时如果POST请求返回400或参数解析错误第一件事就是打开开发者工具看请求头里的Content-Type是什么再对照你的代码是不是匹配。这个检查动作能省掉大量瞎猜的时间。2.2 用开发者工具逆向一个POST接口的完整流程光讲理论不够直观我说一下自己平时逆向一个POST接口的标准操作流程。打开浏览器的开发者工具切到Network面板勾选Preserve log保留日志然后在页面上触发那个需要POST的操作比如点击登录按钮或者提交搜索。在请求列表里找到目标请求点进去看几个关键位置Headers标签下的Request Method确认是POSTRequest Headers里找Content-Type确认编码格式然后看Payload或Form Data标签里面就是实际发送的请求体内容。如果是JSON格式Payload标签会直接显示结构化的JSON如果是表单格式会显示成键值对列表。这里有个细节很多人忽略Payload里看到的参数值有时候是经过JavaScript处理过的比如密码可能被前端做了哈希或者有个签名字段是根据其他参数动态算出来的。这种情况下你直接复制参数去请求可能第一次能成功但过一会儿就失效了因为签名有时效性或者依赖了时间戳。遇到这种接口就得去分析前端JS的逻辑把签名算法复现出来。我一般会先在Sources面板里搜索参数名定位到生成签名的函数然后一步步跟进去看它用了哪些输入、做了什么运算。2.3 参数构造中的编码陷阱与重定向处理构造POST参数时有几个容易翻车的地方值得单独拎出来说。一个是中文和特殊字符的编码问题。虽然requests的data参数会自动做URL编码但如果你手动拼接字符串再传进去就可能出现编码不一致导致服务端解析乱码。我的做法是尽量用字典传参让requests去处理编码除非有特殊需求才手动干预。另一个是重定向。POST请求发出后服务端可能返回302跳转requests默认会自动跟随重定向。但这里有个坑按照HTTP规范302之后的请求方法可能被改成GET导致原本POST的请求体丢失。如果你发现登录接口返回了200但后续请求还是没有登录态很可能就是重定向过程中POST变成了GET。解决办法是用allow_redirectsFalse先看看原始响应手动处理跳转逻辑。还有一个是超时设置。POST请求因为要传输请求体在网络状况不好时更容易卡住。我习惯给每个请求加上timeout参数一般设10到15秒避免程序无限期等待。这个习惯在批量爬取时尤其重要一个卡住的请求可能拖垮整个任务。3. Cookies的本质服务端凭什么认出你3.1 从Set-Cookie到Cookie请求头的完整链路HTTP协议本身是无状态的意思是服务端处理完一个请求就忘了你是谁下一个请求来了它完全不认识。那为什么你登录一次之后后续操作服务端都知道是你靠的就是Cookies这套机制。具体流程是这样的你第一次发送登录请求服务端验证通过后在响应头里加一个Set-Cookie字段里面包含一个或多个键值对比如sessionidabc123; Path/; HttpOnly。浏览器收到后把这些Cookie存起来。之后每次向同一个域名发请求浏览器会自动在请求头里带上Cookie: sessionidabc123。服务端读到这个sessionid去自己的存储里查对应的用户信息就知道你是谁了。用requests写爬虫时这套机制需要你自己维护。最直接的方式是用requests.Session()它会自动帮你管理Cookie的存取效果和浏览器一样。如果不想用Session也可以手动从响应里提取Cookie再在后续请求的headers里带上。手动方式更灵活但更容易出错比如忘了更新过期的Cookie或者Cookie的domain和path没匹配对导致没带上。3.2 手动管理Cookies的适用场景与操作细节虽然Session很方便但有些场景下手动管理Cookie反而更合适。比如你需要同时维护多个不同账号的登录态或者需要把Cookie持久化到文件里以便下次程序启动时复用又或者需要精细控制哪些请求带哪些Cookie。手动提取Cookie的基本操作是发送登录请求后通过response.cookies拿到CookieJar对象可以遍历它获取每个Cookie的name和value。也可以直接从响应头的Set-Cookie字段里用正则提取。拿到之后构造一个字典在后续请求的headers里设置Cookie字段。这里有个实操细节response.cookies拿到的是本次响应设置的Cookie但一个完整的登录流程可能涉及多次跳转每次响应都可能设置新的Cookie。如果你只取了最后一次的可能漏掉前面设置的。稳妥的做法是用Session自动管理或者在每次响应后都把Cookie合并到一个统一的字典里。还有一个关于Cookie过期的问题。Cookie有会话Cookie和持久Cookie之分会话Cookie在浏览器关闭后就失效持久Cookie有明确的过期时间。用requests时Session对象在程序运行期间会一直保留Cookie但程序重启后就没了。如果你需要跨程序运行保持登录态就得把Cookie序列化存到文件或数据库里下次启动时再加载回来。3.3 Cookie失效的几种典型表现与排查思路Cookie失效是爬虫里最常见的故障之一表现有好几种。最明显的是请求返回401或403服务端直接拒绝。还有一种更隐蔽的返回200但内容是登录页而不是目标数据说明服务端认为你没登录把你重定向到了登录页。第三种是返回的数据不完整比如只给了第一页后续分页请求拿不到因为分页接口需要登录态。排查时我一般按这个顺序走先确认Cookie有没有正确带上打印一下请求头看看再确认Cookie有没有过期对比一下登录时拿到的和当前用的然后确认请求的域名和路径是否和Cookie的domain、path匹配最后检查是不是服务端加了额外的校验比如User-Agent、Referer这些请求头也需要配套设置。有时候Cookie本身没问题但服务端会绑定IP或者User-Agent。比如你登录时用的User-Agent是A后续请求换成了B服务端可能就认为会话异常。所以我的习惯是登录时用什么请求头后续请求就保持一致不要随意改动。4. Session会话保持让爬虫像浏览器一样记住状态4.1 Session对象的工作原理与自动管理机制requests.Session()是处理登录态最省心的方案。它内部维护了一个CookieJar每次请求后自动把响应里的Set-Cookie存进去下次请求时自动带上。你不需要手动做任何Cookie的提取和设置只要用同一个Session对象发请求它就帮你把会话维持住。用Session的代码结构很清晰先创建session requests.Session()然后可以设置一些公共的请求头比如User-Agent这样每个请求都会自动带上。接着用session.post()发登录请求再用session.get()发后续请求登录态自动保持。这里有个容易被忽略的点Session设置的请求头是持久的但如果你在某次请求里单独传了headers参数它会和Session的默认headers合并同名的以单次请求的为准。这个特性可以用来做精细控制比如大部分请求用默认User-Agent某个特殊接口需要换一个就单独传。Session还有一个好处是连接复用。它底层用了urllib3的连接池对同一个host的多次请求会复用TCP连接省去了反复握手的时间。在批量爬取同一站点的数据时这个优化带来的速度提升相当明显。我实测过一个场景用Session比每次新建请求快了将近三成。4.2 登录流程的完整拆解从表单提交到会话验证用一个典型的登录场景来串一下完整流程。假设目标站点有一个登录接口需要提交用户名、密码可能还有验证码。第一步是GET登录页面这一步的目的是拿到页面里的隐藏字段比如CSRF token很多站点会要求登录请求里带上这个token。用Session发这个GET请求token和初始Cookie就都拿到了。第二步是从页面HTML里解析出token同时确认Session里已经有了初始Cookie。第三步是构造登录的POST请求参数包括用户名、密码、token可能还有验证码。用同一个Session发送。第四步是检查登录结果可以通过响应内容判断也可以看响应状态码和跳转。第五步是用同一个Session访问一个需要登录才能看到的页面验证登录态是否生效。这个流程里CSRF token是最容易出问题的环节。token通常藏在页面的隐藏input里或者某个script标签的JS变量里。解析时要注意token可能包含特殊字符用正则提取时要写对模式。另外token有时效性从拿到token到提交登录不能间隔太久否则token过期登录会失败。4.3 Session与手动Cookie方案的取舍对比Session虽然方便但也不是所有场景都适用。我整理了一个对比表方便根据实际情况选择。对比维度Session方案手动Cookie方案使用便捷性高自动管理低需手动提取设置多账号支持需多个Session对象灵活切换Cookie字典持久化能力需额外序列化可直接存文件调试透明度较低Cookie隐式管理高每步可见连接复用支持不支持适用场景单账号常规爬取多账号、需持久化、精细控制我的经验是常规的单账号爬取优先用Session省心且性能好。需要同时维护多个账号时可以创建多个Session对象每个账号一个这样也能兼顾便捷性和隔离性。只有在需要把登录态存下来跨程序使用时才考虑手动方案把Session的Cookie序列化到文件下次加载回来。5. 实战中那些文档不会告诉你的坑5.1 请求头缺失导致的假失败很多人调试POST请求时参数、Cookie、Session都检查了没问题但就是返回异常。这种情况十有八九是请求头缺失。服务端除了验证登录态还会检查一些常规请求头来判断请求是否来自正常浏览器。最常见的几个是User-Agent、Referer、Origin。User-Agent缺失或明显是脚本特征比如默认的python-requests/2.x很多站点会直接拒绝。Referer缺失在某些有防盗链的接口上也会被拦。Origin在跨域POST请求里经常被校验。我的做法是从浏览器开发者工具里把登录请求的完整请求头复制下来挑出这几个关键的在代码里配上。不用全部照搬但User-Agent和Referer基本是必带的。还有一个细节是请求头的顺序。虽然HTTP规范里请求头顺序不重要但有些站点的风控系统会检查顺序是否符合浏览器习惯。这个属于比较极端的案例一般遇到再处理不用一开始就纠结。5.2 验证码与风控的应对思路验证码是爬虫绕不过去的坎。简单的图形验证码可以用OCR识别但准确率有限复杂一点的滑块、点选、行为验证码就需要更专业的方案。我的建议是如果目标站点的验证码很复杂优先考虑是否有其他不需要验证码的接口或者是否有移动端接口验证更宽松。实在没有替代方案再考虑接入打码服务或者用自动化工具模拟浏览器操作。风控方面频繁请求同一接口很容易触发限制。应对思路包括控制请求频率加随机延时轮换User-Agent使用多个Session或Cookie轮换必要时使用代理池。这些手段的核心目的是让请求看起来更像正常用户的行为而不是机器在批量抓取。5.3 编码问题引发的乱码与解析失败POST请求返回的内容如果出现乱码通常是编码判断错了。requests会根据响应头的Content-Type里的charset来判断编码如果服务端没正确设置requests可能猜错。解决办法是手动指定response.encoding比如设成utf-8或gbk再读取response.text。还有一种情况是响应内容是压缩的比如gzip或deflate。requests默认会自动解压但如果你手动设置了Accept-Encoding请求头却没处理好解压就可能拿到乱码。一般不建议手动设这个头让requests自己处理就好。JSON解析失败也是常见问题。有时候服务端返回的JSON前面带了一些额外字符比如防JSON劫持的前缀直接response.json()会报错。这时候需要先做字符串处理去掉前缀再解析。或者用json.loads()配合正则提取出纯JSON部分。6. 一套可复用的登录爬取代码骨架把前面讲的东西整合成一个可复用的骨架方便你直接拿去改。这个骨架涵盖了Session创建、请求头设置、登录流程、异常处理和重试机制。import requests import time import random from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def create_session(): session requests.Session() retry Retry(total3, backoff_factor0.5, status_forcelist[500, 502, 503, 504]) adapter HTTPAdapter(max_retriesretry) session.mount(http://, adapter) session.mount(https://, adapter) session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://example.com/, }) return session def login(session, login_url, payload): try: resp session.post(login_url, datapayload, timeout15, allow_redirectsTrue) if resp.status_code 200: return True return False except requests.RequestException as e: print(f登录请求异常: {e}) return False def fetch_with_delay(session, url, delay_range(1, 3)): time.sleep(random.uniform(*delay_range)) try: resp session.get(url, timeout15) resp.encoding resp.apparent_encoding return resp.text except requests.RequestException as e: print(f请求异常: {e}) return None这个骨架里几个设计点解释一下。重试机制用的是urllib3的Retry针对5xx错误自动重试配合backoff_factor做退避避免短时间内反复冲击服务端。请求头里预设了User-Agent和Referer登录和后续请求都会带上。fetch_with_delay里加了随机延时模拟人工操作的间隔降低被风控的概率。编码用apparent_encoding自动判断比默认的猜测更准一些。实际使用时把login_url和payload换成目标站点的登录成功后用同一个session去请求数据接口。如果遇到需要CSRF token的在login之前先GET一次登录页解析出token加到payload里。7. 关于会话保持这件事我踩过的最深的一个坑说一个我印象最深的经历。有一次爬一个数据平台登录接口用Session跑通了后续请求也能拿到数据一切看起来很正常。但跑了大概十几分钟后所有请求突然开始返回登录页。我一开始以为是Cookie过期了重新登录后继续跑结果又是十几分钟就失效。排查了很久才发现这个平台的服务端会在每次请求后更新sessionid也就是响应头里会带一个新的Set-Cookie。用Session的话requests会自动更新理论上没问题。但问题出在我中间有几次请求用了allow_redirectsFalse手动处理了跳转结果跳转过程中的Set-Cookie被我漏掉了导致Session里的sessionid还是旧的。服务端那边旧的sessionid已经失效新的又没收到自然就认为我掉线了。这个坑的教训是用Session时尽量让它自动处理重定向不要轻易手动干预除非你很清楚每一步的Cookie变化。如果确实需要手动处理那就要在每一步都检查响应头里的Set-Cookie并手动更新到Session里。后来我改回让Session自动跟随重定向问题就再没出现过。还有一个相关的经验是有些站点的sessionid更新是滑动过期的也就是每次请求都会刷新有效期。这种情况下只要你保持一定的请求频率登录态就能一直维持。但如果中间停了太久sessionid就真的过期了需要重新登录。所以批量爬取时控制好请求节奏别让间隔太长反而比频繁重新登录更省事。