参数全家福:每个都不难,组合起来才是防护
有道翻译 fanyi.youdao.com 的 translate_o 接口(注意带 _o,不带 _o 的旧接口不校验签名但时好时坏)需要一组参数:
| 参数 | 含义 | 算法 |
|---|---|---|
lts | 毫秒时间戳 | str(now_ms) |
salt | 盐 | lts + 1位随机数 |
bv | 浏览器标识 | md5(User-Agent) |
sign | 核心签名 | md5(固定串 + 文本 + salt + 密钥) |
单个看都是 md5 一把梭,但它们的依赖链才是设计意图:bv 绑 ua,sign 绑文本和 salt,salt 绑时间——改任何一个输入,链条上所有参数都要重算。
算法实现
import hashlib, time, random
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..."
def make_params(text):
lts = str(int(time.time() * 1000))
salt = lts + str(random.randint(0, 9))
bv = hashlib.md5(UA.encode()).hexdigest()
# 密钥历史上换过多次,当前版本以页面 JS 为准
key = "Ygy_4c=r#e#4EX^NUGUc5"
sign = hashlib.md5(f"fanyideskweb{text}{salt}{key}".encode()).hexdigest()
return {'lts': lts, 'salt': salt, 'bv': bv, 'sign': sign}密钥的提取方法(以不变应万变)
有道的签名密钥换过好几次(mmbP%A-r6U1Nw(n]BjuEU、Ygy_4c=r#e#4EX^NUGUc5 等),写死密钥是新手最常踩的坑。正确的工程姿势是启动时从 JS 里提取:
import re, requests
def get_current_key(session):
# 1. 从页面找到 fanyi.min.js 的地址(带 hash 的版本化文件名)
html = session.get('https://fanyi.youdao.com/').text
js_url = re.search(r'src="(https://shared-https.ydstatic.com/fanyi/newweb/[^"]+\.js)"', html).group(1)
# 2. 在 JS 里搜特征字符串 "fanyideskweb",密钥就在它附近的拼接表达式里
js = session.get(js_url).text
m = re.search(r'"fanyideskweb"\+.*?\+([a-z])\+?"([^"]{16,24})"', js)
return m.group(2)搜 fanyideskweb 这个固定串是定位锚点——它是拼接模板的第一段,十年没变过,密钥就排在它后面。
精髓:四个实战坑
- 长文本截断。sign 参与计算的文本有长度截断逻辑——超过一定长度(历史上是 50 字符)只取前 N 个字符参与签名。长文本 sign 对不上,先查这个。
- bv 与 ua 的绑定是隐形的。bv 就是 md5(ua),服务端重新算一遍对比。用 requests 时如果中途改了 headers 里的 ua 而没重算 bv,直接签名失败。建议 ua 定义成全局常量。
- salt 的随机数只有 1 位。
lts + random(0-9),意味着同一毫秒内只有 10 种合法 salt。高并发时别自己造"更随机"的 salt,服务端校验的是格式不是随机性。 - OUTFOX_SEARCH_USER_ID cookie 影响返回结构。这个 cookie 是有道的用户标识,带不带、带什么值,返回的 json 结构有差异(比如是否带词典释义)。协议模拟时固定一个合法值,响应解析才稳定。
总结
- 四个参数一条依赖链:ua→bv,text+salt→sign,lts→salt
- 密钥会换,写死必死,启动时从 JS 提取是正解
- 长文本截断是隐蔽坑,sign 对不上先查它
- 固定 OUTFOX_SEARCH_USER_ID,保证响应结构稳定