
你肯定遇到过这种场景在浏览器里打开有道翻译输入一行英文页面根本没跳转译文就刷刷地出现在下面。很多刚开始学爬虫的读者都会卡在这一步地址栏里的URL动也没动浏览器到底做了什么答案就是它发出了一次POST请求。前两天我收到一条私信说有个人想写一个Python脚本把自己技术文档里的术语批量翻译成中文卡就卡在不知道怎么构造一个和浏览器一模一样的POST请求。这篇文章我就拿有道翻译当作教学例子把“浏览器控制台抓包→拆解请求参数→Python模拟POST请求→解析返回结果”这条链路完整走一遍。做完这个案例你以后再遇到翻译、搜索、登录、加载更多这类交互接口都能用同一套方法自己啃下来。1. 翻译这种“点了就刷新内容”的页面为什么绕不开POST1.1 GET和POST的真实差别决定了你该用哪个很多新手学爬虫第一课都是requests.get()一个URL然后拿HTML去解析。这个思路本身没错但它只覆盖了最静态的那类页面。你细想一下GET请求的每个参数都堂而皇之地写在URL问号后面你把一个待翻译的长句子拼进去URL会变得又臭又长而且很多浏览器和服务器对URL长度是有限制的。POST就不一样它的参数放在HTTP请求的body里url保持干净参数可以很长。翻译这个场景天然需要把用户输入的完整文本送到服务器POST是更合理的载体。我习惯用一个类比GET像是把请求内容直接写在快递单的表面上从广州发到北京中途每一个分拣站都能看得见POST则像是把请求内容塞进快递纸箱里再打包发货只有收件人拆开才知道里面是什么。当然本质上POST不是什么绝对保密抓包照样能看见但它在HTTP语义上更匹配“提交数据、返回结果”这个动作。所以翻译接口、登录接口、搜索接口绝大多数都用POST。1.2 翻译接口为什么是完美的POST入门案例选有道翻译做案例不是因为它特殊恰恰因为它足够普通。它不需要你用session维持复杂登录态不需要处理滑块验证码请求结构很典型一个表单格式的body、几项请求头、返回一段标准JSON。你把它拆明白就等于把市面上百分之六七十的Ajax接口都拆明白了一半。更有意思的是这个接口里既有静态参数又有动态参数还藏着一个MD5签名逻辑。翻译页面上你每点一次请求体里就会多出一串32位的sign值。如果不处理这个值很多版本的接口会直接拒绝你。这和真正的生产环境里那些表单防伪、签名校验的设计思路是一样的。用它练手是真的能学到东西的。2. 浏览器控制台抓包三步把翻译请求从网络面板里捞出来2.1 准备工作打开开发者工具的正确姿势我平时用Edge和Chrome比较多两者操作几乎一样。第一步用浏览器打开有道翻译的网页版随便找一个有输入框的页面。第二步按F12或者右键点击页面任意位置选“检查”。第三步切到顶部那个叫Network网络的面板。很多人到了这一步就走了弯路喜欢直接看Sources面板或者Elements面板找半天也找不到接口。记住你要看的是网络面板不是元素面板。Network面板打开后先在左上角勾选Preserve log保留日志这个选项的作用是防止页面跳转时把网络请求记录清空。虽然这个翻译页面本身不跳转但勾上永远是保险的。接着在下面的过滤器里点一下Fetch/XHR或者直接在筛选框里输入XHR。这样设置完面板里只会剩下异步接口请求不会刷出一堆图片和CSS文件干扰视线。2.2 抓包实操输入一句话盯着Network面板看一切就绪后在左侧的输入框里随便敲一个词比如hello world然后点击翻译按钮。这时候把目光移到Network面板你会看到列表里多出了一两条请求。真正干活的往往是名字里带translate的那个。如果你刚才清了列表这里应该非常干净不会有别的干扰项。点击那条请求右侧会展开一列信息。你要重点看三个地方Headers请求标头下方的General区域这里写着Request Method你会看到是POST。Request Headers请求标头这里能看到User-Agent、Referer、Origin、Content-Type这些关键字段。Payload负载或Form Data表单数据这里是POST请求真正传递的参数列表。我建议你第一次做的时候把这些字段从头到尾抄一遍。这张表是后面写Python代码的原材料。不用试图一开始就理解每一项先抄下来对照代码慢慢就明白了。2.3 从抓包结果里我们能提炼出哪些必抄的信息以下这些字段是我在教学里经常强调的“必抄清单”位置字段作用Request HeadersUser-Agent告诉服务器你用的是哪个浏览器伪装的第一步Request HeadersReferer告诉服务器请求从哪里来很多反爬用它做校验Request HeadersX-Requested-With标记这是一个Ajax异步请求Form Datai待翻译的文本Form Datafrom / to源语言和目标语言AUTO表示自动检测Form Dataclient / keyfrom客户端标识Form Datadoctype期望返回的格式这里是jsonForm Datasalt / sign / bv动态签名相关参数后面专门讲很多新手只复制URL就开始写代码结果怎么调都不对就是因为漏了这些“藏在暗处”的参数。记住浏览器能成功你和浏览器的请求长得一模一样你也能成功。3. 参数拆解salt、sign、bv这三个“防伪标记”是如何算出来的3.1 哪些参数是固定的哪些参数是会变的把Form Data里的参数分成两类一类是每次点击翻译都一模一样的比如client、doctype、version、keyfrom、action这些是写死在接口里的常量直接照抄。另一类是每次点击都在变的最明显的是salt、sign、bv再加上i不同文本当然不同。怎么验证哪些在变你连续翻译两次把两次的Form Data放在一起对比。固定参数在任何一次请求里都一样动态参数每次都会换。这个方法不光学翻译接口有用以后分析任何接口都是同一个套路。你很快会发现规律salt像一串数字后面跟着一位0到9的数lts是一串更长的数字看起来和salt很像sign和bv都是32位的十六进制字符串。这种长度固定、字符范围固定的串基本就是某个Hash函数的输出。这里就需要你去JS源码里找线索了。3.2 在JS源码里找到签名拼接规则这是一个标准的JS逆向过程但别被“逆向”两个字吓到思路很简单网页上的JS代码把参数处理完才发出去的请求那么生成这个sign的逻辑一定就藏在某个JS文件里。你可以直接在Sources源代码面板里搜索sign或者在上面那个翻译请求的Initiator启动器标签里看调用链。搜出来的常见代码长这样var salt (new Date).getTime() parseInt(10 * Math.random(), 10); var sign md5(某个固定字符串 待翻译文本 salt 另一个固定字符串);这个“固定字符串”业内一般叫盐值salt key是服务端和前端约定好的密钥。它的作用是防止别人直接用简单参数伪造请求。在我写作时的一个常见版本里sign的拼接规则是sign md5(fanyideskweb 待翻译文本 salt 一段固定盐值) bv md5(浏览器的User-Agent字符串)注意这段盐值字符串随时可能更新所以我不建议你直接背它。真正要掌握的是定位方法搜salt、搜sign、搜md5找到拼接语句然后把它翻译成Python。这才是别人换盐之后你还能继续跑的能力。3.3 用Python复刻MD5签名算法的代码实现MD5这个东西可以理解成一个“把任意长度的字符串经过计算变成固定32位十六进制串”的哈希算法。它本身不是为了加密而存在更多是给请求内容生成一个指纹让服务器发现参数被改动。你在Python里写签名算法核心就三行import hashlib raw fanyideskweb text salt 固定盐值 sign hashlib.md5(raw.encode(utf-8)).hexdigest()加上日期时间这部分没有魔法就是下面的逻辑salt当前毫秒时间戳再往后拼一位0-9的随机数。lts当前毫秒时间戳其实就是salt去掉最后一位随机数。bv对User-Agent字符串做MD5。有了这三个规律和固定的拼接规则Python端就能在每一次请求时生成和浏览器行为一致的参数。这也是整篇爬虫里最有含金量的一步。4. 用requests库还原POST请求从第一版代码到可复用函数4.1 写一个最小可用版本先把流程跑通前面抓包拆解的所有信息现在开始翻译成代码。我习惯先写最小版本不着急封装确认能跑通再慢慢优化。你先按下面的结构把代码敲一遍import requests import time import random import hashlib url https://fanyi.youdao.com/translate_o?smartresultdictsmartresultrule ua Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 headlers { User-Agent: ua, Referer: https://fanyi.youdao.com/, Origin: https://fanyi.youdao.com, Content-Type: application/x-www-form-urlencoded; charsetUTF-8, X-Requested-With: XMLHttpRequest, Accept: application/json, text/javascript, */*; q0.01, Cookie: 这里填你从浏览器复制的Cookie, } salt str(int(time.time() * 1000)) str(random.randint(0, 9)) payload { i: hello world, from: AUTO, to: AUTO, smartresult: dict, client: fanyideskweb, salt: salt, sign: hashlib.md5((fanyideskweb hello world salt 固定盐值).encode(utf-8)).hexdigest(), lts: salt[:-1], bv: hashlib.md5(ua.encode(utf-8)).hexdigest(), doctype: json, version: 2.1, keyfrom: fanyi.web, action: FY_BY_REALTlME, } resp requests.post(url, headersheadlers, datapayload) print(resp.json())这里有两个细节要先说明白。第一requests库的data参数传字典时会自动帮我们把内容编码成application/x-www-form-urlencoded格式所以你不用手动去拼接字符串。第二payload里的i字段直接写中文也完全没问题requests会自动做URL编码。4.2 解析嵌套JSON响应拿到最终译文上面代码如果一切顺利控制台会打出整个JSON响应。它通常长这样{ type: en2zh, errorCode: 0, elapsedTime: 1, translateResult: [ [ { src: hello world, tgt: 你好世界 } ] ] }你没有看错这个JSON确实是多层的。translateResult是个二维数组最内部每个元素里有src和tgt两个字段src是原文tgt是译文。服务器为什么要设计得这么嵌套因为它要考虑一个句子可能对应多段翻译、多个候选结果的情况二维数组是它内部通用的数据结构。那么要取译文就是一层一层往下走result resp.json() translated result[translateResult][0][0][tgt] print(translated)顺便说一句有时候你会在控制台看到类似\u4f60\u597d这样的东西这是JSON标准的Unicode转义。requests调用json()方法解析后返回的是真正的Python字符串print出来会自动显示中文不用你自己去解码。4.3 封装成函数参数、异常、重复利用跑通最小版本之后下一步就是把它变成能反复调用的函数。否则每翻译一个新单词你都要复制粘贴一大段代码没人受得了。def translate(text, cookie): if not text: return salt str(int(time.time() * 1000)) str(random.randint(0, 9)) sign_raw fanyideskweb text salt 固定盐值 sign hashlib.md5(sign_raw.encode(utf-8)).hexdigest() payload { i: text, from: AUTO, to: AUTO, smartresult: dict, client: fanyideskweb, salt: salt, sign: sign, lts: salt[:-1], bv: hashlib.md5(ua.encode(utf-8)).hexdigest(), doctype: json, version: 2.1, keyfrom: fanyi.web, action: FY_BY_REALTlME, } headers { User-Agent: ua, Referer: https://fanyi.youdao.com/, Origin: https://fanyi.youdao.com, Content-Type: application/x-www-form-urlencoded; charsetUTF-8, X-Requested-With: XMLHttpRequest, Cookie: cookie, } try: resp requests.post(url, headersheaders, datapayload, timeout10) resp.raise_for_status() data resp.json() return data[translateResult][0][0][tgt] except Exception as e: return f翻译失败{e}把这个函数放进你的脚本里再配合一个列表循环就能批量翻译。phrases [hello world, python crawler, network request] for phrase in phrases: print(phrase, , translate(phrase, cookie)) time.sleep(1)我在这个函数里加了timeout参数这是爬虫里的一个基础但非常容易忽略的习惯。很多网络接口偶尔会慢如果服务器一直不响应requests默认会一直等着脚本就卡死了。设置timeout等于告诉它超过10秒直接报错别干等。5. 实战踩坑Cookie缺失、请求头不全、请求太频繁都长什么样5.1 errorCode 50和“非法请求”多半是Cookie的问题跑代码时遇到的第一座大山通常是返回JSON里出现errorCode: 50或者错误提示类似“网页链接有误”。这时候先别急着怀疑签名算错了先检查请求头里的Cookie。在我写的这版代码里使用了translate_o这个接口路径。这个路径的特点是相对“内部”一点服务器会校验Cookie字段尤其是OUTFOX_SEARCH_USER_ID开头的几个cookie值。解决办法有两个方向第一从浏览器开发者工具里在任意一个翻译请求的Request Headers里找到Cookie整行复制到代码里第二如果不想被Cookie折磨把接口URL从translate_o改成translate也就是“https://fanyi.youdao.com/translate?smartresultdictsmartresultrule”有些版本的这个接口不用带Cookie也能通过。这里我要强调一下Cookie不是永久有效的有的几个小时就过期了。脚本跑得多了某天突然开始报错第一反应不是去检查代码逻辑而是重新复制一遍Cookie试试。5.2 返回HTML而不是JSON请求头伪装不到位另一个很常见的翻车现场是请求状态码是200但resp.json()直接报错被迫用resp.text一看返回了一整个HTML页面。出现这个现象说明服务器压根没把你当成正常的页面请求直接给你丢了一个默认页面。问题基本出在请求头不完整。我见过太多人只写了User-Agent然后问为什么不行。对于翻译接口下面这几个字段缺了都不行Referer表示你是从翻译页面跳过来的必须带着。Origin跨域安全策略会校验来源必须带着。X-Requested-With值为XMLHttpRequest服务器靠这个判断你是Ajax请求而不是浏览器直接访问。我自己的测试结论是User-Agent Referer Origin X-Requested-With这四件套凑齐了返回的才是干净JSON。你可以在代码里逐个加每加一个打印一次响应看变化体会一下“服务器校验请求头”到底是什么感觉。5.3 请求频率过高被限制加随机延时批量翻译的需求一旦跑起来你就会犯一个几乎所有爬虫新手都会犯的错误用for循环一口气发几百个请求中间一个sleep都不写。没跑一会儿你会发现响应开始变得奇怪返回的可能是验证码页面、错误码51、或者干脆请求超时。翻译这个接口是实时带签名校验的服务端自然会有频率限制。应对方式并不复杂就是给每个请求之间加上随机延时。随机很重要如果你固定sleep(1)程序就像个精准的计时器反而更显眼。我一般是这么写的for phrase in phrases: print(phrase, , translate(phrase, cookie)) time.sleep(random.uniform(0.5, 1.5))另外单批次任务我也建议控制在几十条以内。跑完一批歇一会儿或者换IP都比从技术层面硬刚更实际。说到底这只是一个学习用的接口没必要给它那么大压力。5.4 接口升级导致算法失效重新走一遍抓包流程最让人沮丧的情况是昨天还能用的代码今天一跑返回errorCode非0或者干脆提示参数错误。你发现自己写的sign规则已经不对了因为网站更新了前端JS换了一个新的盐值字符串或者干脆改了签名算法。这种情况你别慌。回到第2节的抓包流程重新把网络请求导出来对照新的Form Data看sign和bv的算法有没有变。定位的思路永远是同一个先在页面里连续翻译两次拍下前后两组salt和sign的样本然后去Sources面板搜sign 或者md5(关键词找到拼接语句再把旧代码里的固定字符串替换成新值整个项目又活了。我把这个处理过程整理成了一张表方便你直接当排查手册用现象可能原因排查思路errorCode 50 / 非法请求Cookie缺失或过期重新复制浏览器Cookie或改用不带_o的接口路径返回200但内容是HTML请求头伪装不到位补全User-Agent、Referer、Origin、X-Requested-With连续请求几十条后开始报错触发服务端频控加随机延时降低频率单批次限量sign相关参数反复校验失败网站更新了签名算法或盐值重新抓包重新定位JS里的sign拼接逻辑更新代码输出中出现\u4e2d\u6587未正确解析JSON直接用resp.json()不要手动处理resp.text6. 这个爬虫项目带给我的几点思考什么该学什么不该学6.1 学的是方法不是某个网站的脆弱接口先说一个我要强调的边界。这篇文章展示的翻译接口本质上是一个面向普通用户的免费功能抓包分析和低频率的个人自动化调用通常被用于学习HTTP协议和爬虫原理。但如果你打算大规模抓取、商用、或者给目标服务器造成明显压力那就超出了正常学习的范围既不符合网站的服务条款也有可能带来法律风险。所以下面的方法请只用于个人学习和小规模使用。真正有价值的东西不是那个会随时失效的盐值字符串而是你从这一次实操里建立起来的完整链路看到页面交互能想象出背后的POST请求打开Network面板能精准定位到接口面对一串不知所云的参数能通过对比和搜索找到生成规则最后用Python把这些规则复现出来。这套链路放在任何网站上都成立。6.2 我平时会怎么用它以及它还能怎么扩展我自己写这类脚本的场景其实很小就是偶尔翻译一批英文术语表中的难懂名词几十条就停手。跑完以后把脚本留着下一次遇到同样的需求换一批Cookie直接跑。更多的时候我会用同一套方法分析别的接口比如一些资讯页面的“加载更多”按钮一些工具站点的表单提交逻辑。如果你想沿着这个方向继续深入建议按这三个顺序进阶先是学会用requests.Session维持会话状态它能把Cookie自动保存下来省去手动复制的麻烦然后是抓包工具的进阶Charles或Fiddler能拦截手机App的HTTPS请求让你从“只抓网页”升级到“抓App”最后是手写一个简单的JS执行工具遇到纯JS加密的sign可以直接把浏览器里的规则搬到本地执行。6.3 更稳定、更省事的替代方案最后说点掏心窝子的话。如果目的是批量翻译且对稳定性有要求用一个翻译网页接口来刚是性价比最低的做法。你花时间研究签名规则人家后端一更新就全部作废。我会优先考虑官方提供的翻译API通常都有免费额度而且接口稳定、返回结构规范。然后是开源社区维护的第三方翻译聚合库它们替你处理了各种网站的参数变化调用起来一行代码就完事。但即便如此我还是建议你把这篇教程从头到尾动手做一遍。因为那些被封装好的库一定会在文档里写一句话“本库基于逆向分析接口可能失效。”你如果不懂抓包、不懂POST参数构造、不懂签名逻辑这句话对你来说就是天书。现在你亲手走过了这一遍再回去看文档就会恍然大悟哦原来它们做的就是这个事。我个人在实际操作中最深的体会是爬虫这门技术一二三级难度之间的差距往往不是工具用得有多花哨而是你能不能把一个具体的接口请求彻底拆开并重新组装。如果你看完这篇文章能不看任何参考自己通过抓包把一个从来没见过的POST接口复现出来那这个案例的目标就真正达成了。