百趣云 百趣云的博客

百度翻译 sign 与 token 生成还原

两个参数,两种获取方式

百度翻译 fanyi.baidu.comv2transapi 接口需要 signtoken 两个参数,获取方式完全不同:

  • token页面里写死的,正则一把抠:token: '([0-9a-f]+)'
  • sign由待翻译文本实时计算,算法在页面 JS 里,种子是页面里的 gtk

这个"gtk 种子 + 逐字符位运算"的算法结构,是前端签名里最经典的设计之一,值得完整拆一遍。

gtk:算法的种子

页面源码里有 window.gtk = '320305.131321201' 之类的定义,两个数字用点分隔。它是 sign 计算的初始状态,会不定期更新——所以工程上 gtk 要和 token 一样启动时从页面提取,绝不写死

sign 算法完整还原

算法核心是一个"指令驱动的位运算器"函数(混淆后常命名为 na):

def n(r, ops):
    # 按指令串对 r 做位运算。指令格式:每3字符一组 [操作][方向][位数]
    for t in range(0, len(ops) - 2, 3):
        a = ops[t + 2]
        a = ord(a) - 87 if a >= 'a' else int(a)   # 'a'=10, 'b'=11... 字母表位移
        a = (r >> a) if ops[t + 1] == '+' else (r << a)
        r = ((r + a) & 0xFFFFFFFF) if ops[t] == '+' else (r ^ a)
    return r

def utf16_codes(text):
    # JS charCodeAt 语义:emoji 等代理对字符拆成两个码元
    codes = []
    for ch in text:
        cp = ord(ch)
        if cp > 0xFFFF:
            codes.extend([0xD800 + ((cp - 0x10000) >> 10),
                          0xDC00 + ((cp - 0x10000) & 0x3FF)])
        else:
            codes.append(cp)
    return codes

def baidu_sign(text, gtk):
    a, b = (int(x) for x in gtk.split('.'))
    r = a
    for code in utf16_codes(text):
        r += code
        r = n(r, "+-a^+6")      # 第一轮:每个字符后执行
    r = n(r, "+-3^+b+-f")       # 第二轮:收尾执行
    r ^= b
    if r < 0:
        r = (r & 0x7FFFFFFF) + 0x80000000
    r %= 1000000
    return f"{r}.{r ^ a}"

指令串 "+-a^+6" 的读法:+ - a 一组、^ + 6 一组……每组三字符,分别表示"累加/异或、左移/右移、位数"。这套"操作串当程序执行"的设计很精巧——百度想改防护只需换操作串,算法骨架不变。

完整请求流程

import requests, re, hashlib

class BaiduFanyi:
    def __init__(self):
        self.s = requests.Session()
        self.s.headers['User-Agent'] = 'Mozilla/5.0 ...'
        self._refresh()

    def _refresh(self):
        html = self.s.get('https://fanyi.baidu.com/').text
        self.token = re.search(r"token: '([0-9a-f]+)'", html).group(1)
        self.gtk = re.search(r"window\.gtk = '([^']+)'", html).group(1)

    def translate(self, text, to='en'):
        data = {
            'from': 'zh', 'to': to, 'query': text,
            'transtype': 'realtime', 'simple_means_flag': '3',
            'sign': baidu_sign(text, self.gtk), 'token': self.token,
        }
        r = self.s.post('https://fanyi.baidu.com/v2transapi', data=data)
        return r.json()

精髓:四个实测坑

  1. 代理对字符(emoji)是签名对不上的头号原因。Python 的 ord() 对 emoji 返回完整码点,JS 的 charCodeAt 返回两个 UTF-16 码元——不做代理对拆分,含 emoji 的文本 sign 必错。上面的 utf16_codes 就是标准解法。
  2. token 和 session 绑定。token 从哪个 session 的页面里抠的,就得用哪个 session 发请求,跨 session 使用返回 {"errno": 997}(token 校验失败)。
  3. 新版加了 Acs-Token 头。2023 年后部分请求还校验 Acs-Token 请求头,由页面里的 acs 安全脚本生成。裸请求遇到 998/999 错误码时,检查是不是缺这个头——它的生成有混淆,建议直接用浏览器自动化补。
  4. 频率限制按 token 计。单 token 高频翻译会触发 {"errno": 998} 或滑块,批量场景要轮换 session(每轮换都重拿 token+gtk)。

总结

  1. token 页面抠、gtk 页面取、sign 实时算,三者同源同 session
  2. 算法核心是指令驱动的位运算器,"+-a^+6" 这类操作串是灵魂
  3. emoji 代理对拆分是 Python 复现的最大坑
  4. 997=token 问题,998=频率问题,错误码即排查地图
By 百趣云 阅读量:1 On