你有没有遇到过这种情况:想拿某个网页的翻译结果做自动化处理,想着“不就是发个请求吗”,结果打开浏览器调试面板一看,全是看不懂的请求名,点开又不知道看哪一行,最后只能回到手工复制粘贴的老路上去。
这个场景太常见了。我最早学爬虫的时候,就在“发请求”这件事上栽过不少跟头——尤其是POST请求。很多新手习惯用GET,顺手一写就完事,但到了真实网站,尤其是带交互的页面(搜索、翻译、登录、评论提交),后端几乎都是通过POST接收数据的。GET是“取”,POST是“交”,你买东西得把订单“交”给店家,光是喊一嗓子“我要买”是没用的。
这篇文章就用一个最典型的案例——模拟向某在线翻译服务提交翻译请求——来把“浏览器控制台抓包 + POST请求构造 + 请求头伪装”整条链路拆开讲清楚。适合刚学完requests基础、想进阶一点的人,也适合那些已经写过不少代码、但始终觉得抓包调试不系统的朋友。
1. 为什么拿“翻译服务”当练习案例
1.1 拆解这个案例到底难在哪
翻译服务这类页面,是最适合练POST请求的“教科书级”场景。原因有三点:
第一,交互模式天然是POST。你输入一段英文,点“翻译”,前端要把这段文本交给服务器处理,服务器返回结果。这就是典型的“提交数据-返回数据”,对应POST请求。
第二,数据格式够典型。翻译请求的提交参数里,既有文本内容本身,又有源语言、目标语言、翻译类型这类附加字段,非常像你在真实业务里会碰到的表单数据。
第三,响应结构丰富。翻译接口返回的一大坨JSON里,通常包含了翻译结果、词典解释、音标、例句、更多候选翻译等等。解析起来比“返个200就完事”的接口有意思得多,能顺带练JSON解析。
但难点也在这:翻译服务的请求参数往往不只是你填的那个文本。它还带着一堆“额外字段”——签名、时间戳、盐值、客户端类型、版本号之类的。这些字段是怎么来的、作用是什么,是新手最容易一头雾水的地方。
我刚学的时候也犯过傻:直接在代码里发一个只有文本框数据的POST,结果服务器返我一串错误码。后来才明白,服务器端早就不是“你给我一句话,我给你翻一下”那么简单了——它需要验证“这个请求是不是从正规客户端发出来的”。
1.2 这个案例能迁移到什么场景
不要以为学完了就只会“翻译一下”。把“抓包拿到接口→分析参数→构造请求→带Cookie/头信息→解析JSON”这套流程跑通之后,你就能迁移到很多场景:
- 表单提交类:问卷、留言板、注册登录
- 数据查询类:查天气、查快递、查股票、查论文
- 内容生成类:AI对话、语音合成、图片描述
本质上,只要网页上有一个输入框、一个按钮、一次结果刷新,背后基本都是一个接口调用。你只要学会“在控制台里找到那个请求、照葫芦画瓢”,任何带交互的页面你都能自动化。
这也是我写这篇文章想强调的核心:教程里教的“某翻译服务”只是载体,真正要带走的是“抓包-还原-请求-解析”的思维。
2. 浏览器控制台抓包实操,手把手拆给你看
2.1 一套能复用到任意网页的抓包流程
打开浏览器,按F12,切到Network(网络)面板,这一套大家可能都知道。但真正的关键在细节:
- 勾选“Preserve log”(保留日志),这样页面跳转或刷新时请求不会被清空
- 筛选条件选“Fetch/XHR”,把所有图片、CSS、字体请求都过滤掉,只留接口请求
- 在“名称”列表中,一眼扫过去,优先看那些名字里带“translate”“search”“submit”“api”的请求
拿翻译服务举例:在页面上输入一个词,点“翻译”按钮,回到控制台,能看到列表里多了一个新请求。选中它,右边会分几个Tab:Headers(请求头)、Payload(请求体)、Preview(响应预览)、Response(原始响应)。
最常用的组合是:
- Headers里看Request Headers,重点是User-Agent、Referer、Origin、Cookie
- Headers里看Request Payload或者Form Data,重点是表单字段名和值
- Preview里看响应结构,确认返回的JSON长什么样
这是我总结的一套“看接口三板斧”。只要是前端用AJAX/Fetch发起的请求,十有八九都能用这个流程找到它。
2.2 抓包时的两个关键动作
第一步,找到请求之后,别急着一行行看。直接右键这个请求,选“Copy → Copy as cURL(bash)”。你会得到一整段curl命令。
这段命令是宝藏——它把你刚才在浏览器里发的那个请求的完整信息都“翻译”成了文本:URL、请求方法、每个请求头、每个请求体字段。把它保存到一个文本文件里,就等于把真实请求现场完整记录下来了。
第二步,把curl命令和代码结构对应着看。你能看到类似这样的结构:
curl 'https://fanyi.example.com/api/translate_v2' \ -H 'user-agent: Mozilla/5.0 ...' \ -H 'referer: https://fanyi.example.com/' \ -H 'cookie: xxx' \ --data-raw '{"content":"hello","from":"en","to":"zh"}'对应到Python里就是:
import requests url = "https://fanyi.example.com/api/translate_v2" headers = { "user-agent": "Mozilla/5.0 ...", "referer": "https://fanyi.example.com/", "cookie": "xxx" } data = { "content": "hello", "from": "en", "to": "zh" } resp = requests.post(url, headers=headers, data=data) print(resp.text)每一条curl里的-H参数,对应requests里的headers字典的键值对;--data-raw对应的就是请求体。这样一对照,整个请求的“骨架”就出来了。
2.3 从curl到Python的三种常见映射关系
curl命令和Python代码的映射,看起来简单,但有不少细节容易出错。
- curl的-H对应headers字典。但要注意,有些请求头是有“动态性”的,比如Cookie,可能过一段时间就失效了。写代码时不要完全照搬,要理解哪些头是必须的,哪些是“带了更容易通过”的。
- curl的--data-raw表示“原文发送”,对应requests里的data参数。data接受字符串或字典,如果你把字典传进去,requests会自动编码为表单格式;如果你想发JSON格式,需要传一个JSON字符串并用headers里的“content-type: application/json”声明。
- curl的--data-urlencode表示“URL编码后发送”,通常用于表单提交,对应requests里的data字典(默认就会做URL编码)。
理解了这三组关系,你再看任何copy as cURL,都会有一种“我能翻译它”的感觉,而不是对着一段黑压压的命令发呆。
3. Python代码怎么写:从“照抄”到“理解”
3.1 一份能用的POST请求骨架
抓包完成之后,写Python代码就有据可依了。先给一个标准骨架:
import requests import time import hashlib def translate_text(text, from_lang="en", to_lang="zh"): url = "https://fanyi.example.com/api/translate_v2" headers = { "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "referer": "https://fanyi.example.com/", "origin": "https://fanyi.example.com", "content-type": "application/json", "cookie": "你的cookie" } payload = { "content": text, "from": from_lang, "to": to_lang, "timestamp": int(time.time() * 1000) } resp = requests.post(url, headers=headers, json=payload, timeout=10) return resp.json() if __name__ == "__main__": result = translate_text("hello") print(result)这段代码涵盖了POST请求的几大要点:URL、请求头、请求体、超时设置。其中requests.post里的json=参数会自动做两件事:把字典序列化成JSON字符串;自动加上“content-type: application/json”请求头。所以如果在headers里没写content-type,问题也不大。
但我故意在headers里保留了content-type,是为了让你养成习惯——当你在调试时发现“我明明发的是JSON,服务器却报格式错误”,排查思路就是:看看声明的content-type和数据格式是否一致。
3.2 data、json、params到底有什么区别
这是POST请求里最容易混乱的三个参数,我用通俗的话解释一下。
- params:拼在URL问号后面的参数,一般用来做查询条件,比如
?page=1&size=20。 - data:请求体里的数据,默认编码成表单格式(application/x-www-form-urlencoded)。适合那种“在页面填了表单提交”的场景。
- json:请求体里的数据,编码成JSON格式(application/json)。适合接口API、前后端分离的页面。
如果你混着用,比如该用data的地方用了json,服务器就会一脸懵:明明我说的是“把鸡汤面加辣”,你却在“把面条蘸辣酱”,虽然不是不能吃,但不是我要的那份。
实际调试中的一条实用建议:看到页面上是一个输入框、一个按钮、刷新一段文字结果,优先用json试试;看到的是页面跳转、整表提交,优先用data试试。当然,最可靠的办法还是看抓包里请求体的“Content-Type”是什么,一锤定音。
3.3 请求头伪装:不能只带User-Agent
“伪装”是很多爬虫教程爱提的词,但伪装的学问在于“像不像一个正常用户”。
只带一个User-Agent就是典型的“半吊子伪装”。因为服务器不只看你的User-Agent,它还会看:
- Referer:你这个请求是从哪个页面来的。翻译服务自然是从它的首页发起的,如果你不带Referer,服务器会想“这个请求没有来源,可能是程序直接调接口”。
- Origin:类似Referer但更偏重安全校验,部分接口会严格校验。
- Cookie:代表“登录态”和“访问历史”。你每次访问网页都会种下Cookie,服务器用Cookie来识别你是不是一个“回头客”。
我在实操里发现,很多反爬拦截都是先从“缺头信息”开始的。带齐Referer和Origin之后,接口被拒的概率会明显下降。
但我必须强调,Cookie是双刃剑。Cookie会过期,会绑定IP,会受登录状态影响。真实项目里需要单独处理“Cookie失效后自动更新”的逻辑,不是塞一个固定字符串就一劳永逸。
4. 深入一步:加密参数是怎么回事
4.1 加密参数的常见套路
真实的翻译服务,请求体里往往不止有“content、from、to”这种明文字段,还会有一些看起来不知道干吗用的参数,比如salt、sign、time。
拿经典的时间戳+签名组合来说:
import time import hashlib timestamp = str(int(time.time() * 1000)) salt = timestamp + "某个随机数" sign = hashlib.md5(("固定字符串" + salt + "额外盐值").encode()).hexdigest()这里的逻辑是:客户端把“当前时间戳 + 一些固定字符串”拼接成一个字符串,做一次MD5(或SHA256)哈希,生成一个签名sign。服务器收到请求后,会用自己的算法重新计算一遍签名,比对一致才认为请求是合法的客户端发来的。
时间是拿来干嘛的?拿来限制请求“新鲜度”的。如果你的签名是30分钟前生成的,服务器一比对时间戳发现太老,就直接拒绝。这就防止了别人把请求录制下来反复重放攻击。
这段知识很重要。很多人觉得“加密参数就是抄下来写死就完事”,但时间戳变了,签名就失效。这是POST请求从基础走向进阶的第一道门槛。
4.2 怎么判断参数是“写死”的还是“动态生成”的
看网页源码和抓包时,有个判断技巧:把同一个请求发两次,对比请求体里的字段值。
- 每次请求都一模一样的字段 → 大概率是写死的数据
- 每次请求都会变的字段 → 大概率是动态生成的,最常见的是时间戳和签名
- 只在特定操作后出现的字段 → 可能是从上一个接口返回里取出来的
拿某在线翻译服务实测:多翻几次,你会看到类似“time”字段一直在变,而“token”字段可能一直不变。这就是典型的组合参数结构——一个动态校验、一个静态标识。
遇到这种情况,正确思路是:把“每次会变的字段”当作函数参数,在代码里动态计算,而不是硬编码。这需要你锁定它的生成逻辑,最常用的手段是看页面的JS源码,找到生成签名的那段代码,然后用Python重新实现一遍。
4.3 一个完整的“带签名”POST请求示例
import requests import time import hashlib class Translator: def __init__(self, token="你的固定token"): self.token = token self.session = requests.Session() self.session.headers.update({ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "referer": "https://fanyi.example.com/" }) def _sign(self, text, timestamp, salt): raw = "某个固定盐值" + text + timestamp + salt + self.token return hashlib.md5(raw.encode()).hexdigest() def translate(self, text, from_lang="en", to_lang="zh"): timestamp = str(int(time.time() * 1000)) salt = str(int(time.time() * 1000) + 100) payload = { "content": text, "from": from_lang, "to": to_lang, "time": timestamp, "salt": salt, "sign": self._sign(text, timestamp, salt) } resp = self.session.post("https://fanyi.example.com/api/translate_v2", json=payload) return resp.json()这段代码是我在很多项目里的通用模板。用类封装是刻意的:翻译请求涉及“签名算法 + 请求头 + 会话保持”三块逻辑,拆成类以后,代码更清晰,也方便在别的地方复用。尤其是session的使用——一个Session对象会把Cookie和请求头自动带到同一次的后续请求里,比每次requests.post独立发请求要稳得多。
5. 常见问题与排查技巧实录
5.1 一个新手最容易踩的坑:返回HTML而不是JSON
症状:代码执行了,但打印出来的不是清爽的JSON,而是一大段HTML。
排查思路:首先看返回状态的Content-Type。如果是“text/html”,说明你请求到的不是API接口,而是普通网页。这种情况常见于:
- URL拼错了,写到了页面地址而非接口地址
- 请求头缺失被反爬系统拦了,服务器返回了一个“验证页面”
- 需要先访问首页拿到Cookie,再访问接口
解决办法也很直接:回到控制台,确认你复制请求时的那个URL是否和代码里的完全一致;带上完整的headers再试;用session先GET一下首页,把Cookie存下来再POST。
5.2 请求成功了但数据不对:一个必须学会的调试技巧
有时候状态码是200,但返回的数据里没有翻译结果,只有一个错误描述。这时候别急着改代码,先在浏览器里手动操作一次,把控制台里“那个请求”的响应和Python里拿到的响应做对比。
如果浏览器里响应正常、Python响应异常,说明请求构造出了问题。这时候最有效的招是:把Python的请求体和浏览器抓包里的Request Payload逐字段比对,一个字母一个字母地对,包括引号、括号、结尾的逗号。我遇到过很多次,以为完全一样,后来发现少了一个字段,或者字段名大小写差了一个字母。
5.3 被反爬机制识别的几道防线
反爬不是一道墙,是好几道。常见的拦截点按顺序排:
| 拦截维度 | 典型特征 | 应对方式 |
|---|---|---|
| 请求头 | 缺少User-Agent/Referer/Cookie | 带齐关键头,用Session维持状态 |
| 频率 | 单IP短时间大量请求 | 加延时,随机间隔,控制QPS |
| 签名 | 请求体含动态sign字段 | 分析并动态生成签名 |
| 行为 | 请求轨迹不像真人 | 请求顺序、鼠标事件等模拟 |
很多人只关注第一道,以为带上User-Agent就安全了。实际上,真实开发里签名才是大头。特别是翻译服务这类接口,签名几乎必带。
5.4 日常调试时值得养成的几个习惯
先curl后代码。每次都从控制台复制curl,保存起来再翻译成代码,能减少“凭记忆写”的错误。
写代码时把返回结果格式化输出。用json.dumps(resp.json(), ensure_ascii=False, indent=2)代替直接print,一眼就能看到数据层级结构。
加延时。翻译服务这类接口,对频率异常敏感。写死每次请求间隔1-3秒随机延时,才算是一个“有礼貌”的客户端。
6. 实用小技巧:几个我一直用的“加速包”
抓包这件事,多练几次手就熟了。但有几个习惯是我长期实践后才养成的,写在这里给你做个参考。
一是利用控制台的“Search”功能。如果你不知道一个请求叫什么名字,但知道返回结果的某个关键词(比如翻译结果“你好”),可以在Network面板按Ctrl+F,输入“你好”,浏览器会直接帮你定位到包含这个关键词的请求。这个功能在接口较多的时候极其好用。
二是学会看Initiator(发起者)列。每次请求都会显示它是由哪段JS触发的。顺着Initiator往上看,能找到生成请求参数的JS文件,对分析加密参数很有帮助。
三是别忽略Preview和Response的差异。Preview是浏览器帮你格式化好的数据树,Response是原始字符串。有时候Preview里看不到某些隐藏字段,但Response里能看到。解析数据时,以Response里的原始JSON为准。
四是用Python写一个通用的“抓包转代码”的小工具思路。把每次复制的curl命令保存成一个文本,写一段代码解析这个文本里的-H和--data-raw,自动生成字典结构。这个工具会随着你抓包越来越多而越用越顺手。
我在实际项目中,很多POST请求都是这样一步一步“抄”出来的。抄得多了,你会发现很多网站的参数规则大同小异,总结出几套通用的签名算法模板,新项目上手速度会快很多。
最后再分享一个小技巧:写爬虫代码的时候,别把逻辑全部堆在一个文件里。把“请求封装”“签名生成”“数据解析”“主流程”分开写,后期维护会舒服得多。我早期吃过这个亏——几千行代码揉成一团,改一个参数要翻半天,后来拆开之后就清爽了。这个翻译案例虽小,但如果你认真按模块去写,它会成为你爬虫代码设计能力的一块很好的磨刀石。