百趣云 百趣云的博客

有道翻译 sign 参数:bv 到 md5 的完整链路

参数全家福:每个都不难,组合起来才是防护

有道翻译 fanyi.youdao.comtranslate_o 接口(注意带 _o,不带 _o 的旧接口不校验签名但时好时坏)需要一组参数:

参数含义算法
lts毫秒时间戳str(now_ms)
saltlts + 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]BjuEUYgy_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 这个固定串是定位锚点——它是拼接模板的第一段,十年没变过,密钥就排在它后面。

精髓:四个实战坑

  1. 长文本截断。sign 参与计算的文本有长度截断逻辑——超过一定长度(历史上是 50 字符)只取前 N 个字符参与签名。长文本 sign 对不上,先查这个。
  2. bv 与 ua 的绑定是隐形的。bv 就是 md5(ua),服务端重新算一遍对比。用 requests 时如果中途改了 headers 里的 ua 而没重算 bv,直接签名失败。建议 ua 定义成全局常量。
  3. salt 的随机数只有 1 位lts + random(0-9),意味着同一毫秒内只有 10 种合法 salt。高并发时别自己造"更随机"的 salt,服务端校验的是格式不是随机性。
  4. OUTFOX_SEARCH_USER_ID cookie 影响返回结构。这个 cookie 是有道的用户标识,带不带、带什么值,返回的 json 结构有差异(比如是否带词典释义)。协议模拟时固定一个合法值,响应解析才稳定。

总结

  1. 四个参数一条依赖链:ua→bv,text+salt→sign,lts→salt
  2. 密钥会换,写死必死,启动时从 JS 提取是正解
  3. 长文本截断是隐蔽坑,sign 对不上先查它
  4. 固定 OUTFOX_SEARCH_USER_ID,保证响应结构稳定
By 百趣云 阅读量:3 On