1. 从翻译接口说起为什么这个需求一直有人在问做数据采集这行的朋友多多少少都碰过翻译类接口。不管是做多语言内容聚合、跨境电商的评论分析还是给一批外文资料做批量预处理翻译能力往往是绕不开的一环。而市面上能提供稳定翻译结果的公开服务里翻译类网页接口因为响应快、语种全、结果质量还不错一直是很多人第一个想到的目标。但真正动过手的人都知道这类接口远没有看上去那么友好。你打开浏览器开发者工具看着一次翻译请求干干净净地发出去、结果顺顺利利地回来心里想着这不就是个 POST 嘛然后兴冲冲地用脚本一模拟——返回的要么是一串看不懂的报错要么干脆给你一个空结果。问题出在哪出在这类接口背后有一整套请求签名机制也就是大家常说的加密参数。这篇内容我想聊的就是围绕翻译接口采集这件事把整个链路拆开讲清楚请求到底带了哪些参数、哪些是明文的、哪些是动态计算的、这些动态参数是怎么生成的、以及在实际写采集脚本时有哪些坑。需要先说明的是我讨论的是技术原理层面的分析与学习目的是理解现代 Web 接口的签名设计思路帮助大家在合规前提下做技术研究。任何采集行为都应该遵守目标站点的服务条款和相关法律法规控制请求频率、尊重 robots 协议这是底线。适合读这篇的人有一定 Python 基础、了解 HTTP 协议、写过简单爬虫但一遇到加密参数就卡壳的朋友。如果你正好卡在请求发出去了但拿不到数据这一步那接下来的内容应该能帮你把思路理顺。2. 抓包看到的请求长什么样参数逐个体检在动手写任何代码之前第一步永远是把真实请求完整地看一遍。这一步偷懒后面全是白费功夫。我用浏览器的开发者工具F12 打开 Network 面板把语言切到目标语种随便翻译一句话然后找到那个返回翻译结果的 XHR 请求。2.1 请求的骨架URL、方法、请求头先看请求行和请求头。这类翻译接口通常是 POST 请求Content-Type 一般是application/x-www-form-urlencoded也就是说参数是表单形式提交的不是 JSON。这一点很关键因为很多人习惯性用json去发结果服务端根本解析不到。请求头里有几个字段值得留意User-Agent这个不用多说脚本里最好带一个正常的浏览器 UA否则容易被识别。Referer通常会带上来源页面地址有些接口会校验这个字段缺失可能被拒。Cookie这是重头戏。很多签名参数的计算会依赖 Cookie 里的某些值比如会话标识、时间戳种子等。如果你在脚本里完全不带 Cookie签名大概率算不对。我一般会先把整个请求以 cURL 的形式复制出来开发者工具里右键请求 → Copy → Copy as cURL这样请求头、Cookie、表单参数全都原封不动地保留下来方便后面逐项分析。2.2 表单参数拆解哪些是死的哪些是活的把 cURL 里的表单参数拉出来看大致能分成三类第一类业务参数。比如待翻译的文本query、源语言from、目标语言to、翻译类型标识等。这些是你自己控制的想翻译什么就填什么属于死参数。第二类固定标识参数。比如appid、product、client这类通常是前端写死的常量或者从页面某个全局变量里读出来的。它们在一段时间内不变属于半死参数。第三类动态签名参数。比如常见的sign、token、ts时间戳、salt之类。这些参数每次请求都不一样是前端用 JavaScript 现算出来的。这一类就是整个采集工作的核心难点。我实测下来很多人失败的原因就是把第一、二类参数照抄了第三类参数要么漏了、要么写死了一个旧值服务端一校验签名对不上直接返回错误码。2.3 一个容易被忽略的细节参数顺序还有一个特别隐蔽的坑——部分接口对表单参数的拼接顺序敏感。也就是说签名计算时用的字符串是按特定顺序把参数拼起来的如果你在脚本里提交参数时顺序乱了虽然参数值都对但服务端重新计算签名时用的顺序和你提交的不一致照样校验失败。这个坑我踩过。当时参数值反复核对都没问题就是过不了最后发现是字典遍历顺序的问题。解决办法是严格按照抓包里看到的参数顺序来构造请求体或者干脆用有序字典Python 3.7 的普通 dict 已经保序但显式用列表构造更稳妥。3. 签名参数是怎么算出来的定位加密逻辑参数拆完了接下来就是最硬核的部分——找到签名是怎么生成的。这一步的核心工具是开发者工具的 Sources 面板配合断点和搜索。3.1 用全局搜索快速定位签名函数打开 Sources 面板按CtrlShiftFMac 上是CmdOptF调出全局搜索直接搜签名参数的字段名比如sign。搜索结果会列出所有包含这个字符串的 JS 文件。通常签名逻辑会出现在某个被打包压缩过的 JS 文件里。找到之后在疑似计算签名的那一行打个断点然后回到页面重新触发一次翻译。断点命中后你就能看到当时的调用栈Call Stack一层层往上翻就能找到签名函数的入口。3.2 读懂混淆代码别被变量名吓到打包后的 JS 通常变量名都是a、b、c、_0x1234这种看着头大。但别慌签名逻辑本身往往不复杂无非是字符串拼接 哈希运算。你要做的是在断点处把参与运算的中间变量值都打印出来鼠标悬停或加到 Watch 里。记录下输入是什么、输出是什么。反推中间的运算步骤。举个常见的模式签名 MD5(某固定串 query salt 时间戳)。你在断点处看到query、salt、ts的值又看到最终sign的值就可以拿这几个值去本地用 MD5 试算对上了就说明逻辑找对了。3.3 常见签名算法就那么几种根据我的经验这类接口的签名算法绝大多数落在下面这几类里算法类型特征处理方式MD532位或16位十六进制串Python 用 hashlib.md5SHA-1 / SHA-25640位 / 64位十六进制串hashlib.sha1 / sha256简单拼接无明显哈希特征是几个字段拼起来直接字符串拼接时间戳参与每次请求都变且和当前时间接近用 int(time.time()) 生成随机盐每次请求都变无规律需要复现随机逻辑或用固定值试探我遇到过的多数情况签名就是固定盐 业务参数 时间戳做一次 MD5。找到这个规律用 Python 复现就是几行代码的事。注意有些接口的签名里会混入 Cookie 中的某个值所以你在本地复现时必须保证脚本里的 Cookie 和计算签名时用的一致。这也是为什么我前面强调要完整保留 Cookie。4. 用 Python 把整条链路复现出来原理搞清楚了接下来落地成代码。我下面给的是一套通用的骨架具体参数名和算法要按你实际分析的结果替换。4.1 环境与依赖准备pip install requests就这一个库基本够了。如果你需要处理更复杂的加密比如 AES、RSA可能还要装pycryptodome但翻译类接口一般用不到那么重。4.2 构造签名与请求的完整流程import hashlib import time import requests # 这些常量从抓包和 JS 分析里得到 APPID 从页面或抓包里拿到的固定值 SECRET_SALT 从 JS 里分析出的固定盐值 def build_sign(query, salt, ts): # 这里的拼接顺序必须和 JS 里完全一致 raw APPID query salt str(ts) SECRET_SALT return hashlib.md5(raw.encode(utf-8)).hexdigest() def translate(query, from_langauto, to_langen): ts int(time.time() * 1000) # 毫秒时间戳看 JS 里用的是秒还是毫秒 salt str(ts) # 有些接口 salt 就是时间戳有些是随机数 sign build_sign(query, salt, ts) url 接口地址 headers { User-Agent: Mozilla/5.0 (...) Chrome/xxx, Referer: 来源页面, Cookie: 从浏览器复制过来的完整 Cookie, } data { query: query, from: from_lang, to: to_lang, appid: APPID, salt: salt, ts: ts, sign: sign, } resp requests.post(url, headersheaders, datadata, timeout10) return resp.json() if __name__ __main__: print(translate(你好世界))这段代码的关键点有三个时间戳的精度秒还是毫秒一定要和 JS 对齐、拼接顺序和 JS 完全一致、Cookie 的完整性别漏字段。4.3 验证签名是否算对怎么判断签名算对了最直接的办法是拿浏览器里那次成功请求的参数用你的函数重新算一遍 sign看结果是否一致。如果一致说明算法复现成功如果不一致就回到断点处把中间变量一个个对比找出差异点。我一般会准备一组已知输入 已知输出的测试用例每次改完代码先跑测试用例通过了再去发真实请求。这样能快速定位是算法问题还是请求构造问题。4.4 处理返回结果的编码问题翻译接口返回的通常是 JSON但有时候中文会以 Unicode 转义的形式出现\u4f60\u597d。requests的.json()会自动处理一般不用管。但如果你拿到的是非标准 JSON可能需要手动json.loads(resp.text)并注意编码。另外返回结构里翻译结果通常在某个嵌套字段里比如data.trans_result[0].dst具体路径看实际返回。建议先把一次完整返回打印出来看清楚结构再写解析逻辑别凭猜测写。5. 那些让我卡了半天的坑排查链路复盘光讲顺利的流程没意思真正有价值的是踩坑经验。下面这几个问题我每一个都花了不少时间才搞明白。5.1 签名对了但依然报错Cookie 过期有一次我算法明明复现对了测试用例也过了但真实请求就是返回鉴权失败。排查了半天最后发现是Cookie 过期了。这类接口的 Cookie 里往往带会话标识有效期不长。解决办法是定期从浏览器更新 Cookie或者分析 Cookie 是怎么下发的用脚本自动获取。排查思路先确认签名对不对用测试用例再确认 Cookie 是否新鲜换一个刚复制的试试最后确认请求头是否完整。5.2 时间戳精度搞错秒 vs 毫秒JS 里Date.now()返回的是毫秒Math.floor(Date.now()/1000)才是秒。我一开始没注意用了秒级时间戳结果签名怎么算都不对。后来在断点处一看ts是 13 位数字才反应过来是毫秒。判断方法很简单看时间戳的位数。10 位是秒13 位是毫秒。5.3 参数里混入了看不见的字符还有一个特别阴间的坑从 JS 里复制出来的盐值字符串末尾可能带了一个不可见的空格或换行。你肉眼看着一样但 MD5 结果就是不同。解决办法是用repr()把字符串打印出来看看有没有多余的转义字符。print(repr(SECRET_SALT)) # 如果输出是 abc 而不是 abc那就是多了个空格5.4 请求频率过高被限流这个不算破解层面的坑但很现实。短时间内发大量请求服务端会返回限流错误或者直接封 IP。我的做法是加随机延时每次请求间隔 1~3 秒随机并且控制单次会话的总请求量。这不是为了绕过什么而是基本的礼貌——任何公开服务都不欢迎被高频轰炸。坑点现象根因解决Cookie 过期鉴权失败会话失效更新 Cookie 或自动获取时间戳精度签名不匹配秒/毫秒混淆看位数对齐 JS隐藏字符签名不匹配复制带入空格repr 检查参数顺序签名不匹配拼接顺序错严格按抓包顺序请求过频限流/封禁频率过高加随机延时6. 从能跑到跑得稳工程化的一些想法把单次请求跑通只是第一步如果你要处理的是批量文本还得考虑稳定性和可维护性。6.1 把签名逻辑和请求逻辑分开我习惯把签名计算单独封装成一个纯函数输入是业务参数输出是签名字符串。这样做的好处是签名逻辑可以单独测试不依赖网络而且如果哪天接口改了算法你只需要改这一个函数。6.2 异常重试与降级网络请求总会失败超时、连接重置都是常事。加一个简单的重试机制比如失败重试 2 次每次间隔递增能显著提升批量任务的完成率。但要注意鉴权类错误不要盲目重试那通常是参数问题重试多少次都没用只会白白增加请求量。6.3 结果缓存如果你要翻译的文本里有大量重复内容加一层本地缓存比如用字典或 SQLite 存原文 → 译文的映射能省下大量请求。这个优化在批量处理场景下效果非常明显。6.4 关于合规的再提醒技术本身是中性的但使用方式有边界。我在做这类分析时始终坚持几条原则只用于个人学习和技术研究控制请求频率不影响服务正常运行不将采集结果用于商业用途不绕过任何明确的访问限制。如果目标服务提供了官方 API优先用官方 API那才是最稳妥、最省心的路径。很多时候我们折腾签名只是因为没注意到官方其实有开放的接口。7. 写在最后的一点个人体会回过头看这类接口签名分析的活儿难点从来不在代码本身而在于耐心地观察和验证。工具就那几个——开发者工具、断点、搜索、对比测试真正拉开差距的是你愿不愿意把每一个参数都搞清楚、把每一次失败都追到根因。我见过太多人一上来就想着有没有现成的库能不能直接抄一段代码结果遇到一点变化就束手无策。反倒是那些愿意老老实实抓包、打断点、做测试用例的人最后不仅把当前这个接口搞定了下次遇到新的签名机制也能快速上手。因为套路是相通的找参数、定位函数、复现算法、验证结果这四步走下来绝大多数接口都能拿下。最后分享一个小习惯每次分析完一个接口我都会把参数说明、算法逻辑、踩过的坑整理成一份笔记。下次再遇到类似的翻出来对照一下能省下大量时间。技术这东西积累的就是这些看似琐碎的经验。