

分析有道翻译 Web 版(https://fanyi.youdao.com)的 API 接口,通过逆向工程破解其加密机制,实现完整的翻译功能 Python 实现。


这种响应看不懂的肯定是加密了
通过浏览器开发者工具抓包分析,发现主要涉及两个关键接口:
tips:sign参数和响应结果需要解密
小技巧快速生成py代码

右键对应的包,复制,curl格式复制
爬虫工具网:https://www.spidertools.cn/#/curl2Request

翻译请求的主要参数结构:
data = {
"i": "苹果", # 需要翻译的文本
"from": "auto", # 源语言(自动检测)
"to": "", # 目标语言
"useTerm": "false",
"domain": "0",
"dictResult": "true", # 是否返回字典结果
"keyid": "webfanyi", # 固定值
"sign": "d50df236da2f8fb02aa35ced782a9e0e", # 动态签名
"client": "fanyideskweb", # 固定值
"product": "webfanyi", # 固定值
"appVersion": "1.0.0", # 固定值
"vendor": "web", # 固定值
"pointParam": "client,mysticTime,product", # 固定值
"mysticTime": "1763003045609", # 时间戳
"keyfrom": "fanyi.web", # 固定值
"mid": "1", # 固定值
"screen": "1", # 固定值
"model": "1", # 固定值
"network": "wifi", # 固定值
"abtest": "0", # 固定值
"yduuid": "abcdefg" # 固定值
}快速定位:

打开搜索框,搜索网址后面的定位这个包发起http协议的位置,
没混淆,再对应js文件搜索如sign一般不是sign=,就是sign:有的.sign有的要打引号之类的大致就那些类型,这个就看经验了
正常是打断点定位,这种直接有的就不用,指导找到,然后看下对应的值

通过 JavaScript 逆向分析,发现签名生成逻辑:
python
def _generate_sign(self, mystic_time: int, key: str) -> str:
"""生成请求签名"""
raw = f"client={CLIENT}&mysticTime={mystic_time}&product={PRODUCT}&key={key}"
return hashlib.md5(raw.encode('utf-8')).hexdigest()
打上点调试,鼠标放在上面就显示对应的值
mystictime是时间戳的d,u都是固定的 就这个t要看下,这个t咋一看有点熟悉,发现前面抓包的时候在这个请求前还有一个包

secretKey这个跟这个t不说10分相似起码也有个9分相似,加上返回了aesiv和aeskey我猜测那个响应加密是不是就是aes加密.接下来就是继续跟或者找个在线解密的测试一下,我继续调试,很快就找到了密文的位置


跟进去看看确实是AES加密算法
在翻译请求之前,需要先获取动态加密密钥:
python
def fetch_aes_keys(self) -> dict:
"""从服务器获取动态加密密钥"""
url = "https://dict.youdao.com/webtranslate/key"
mystic_time = str(int(time.time() * 1000))
sign = self._generate_sign(mystic_time, DEFAULT_KEY)
# 构建请求参数...
response = self.session.get(url, params=params)
data = response.json().get("data", {})
return {
"secret_key": data["secretKey"], # 用于签名
"aes_key": data["aesKey"], # 用于响应解密
"aes_iv": data["aesIv"] # 用于响应解密
}服务器返回的响应数据使用 AES-CBC 加密,解密流程:
python
def decrypt_response(self, ciphertext_b64: str, aes_key: str, aes_iv: str) -> str:
"""解密 AES-CBC 加密的响应数据"""
# Base64 解码
cipher_bytes = urlsafe_b64decode(ciphertext_b64)
# 从字符串生成 AES 密钥和 IV
key = hashlib.md5(aes_key.encode('utf-8')).digest()
iv = hashlib.md5(aes_iv.encode('utf-8')).digest()
# AES 解密
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted_padded = cipher.decrypt(cipher_bytes)
# PKCS7 去填充
decrypted = unpad(decrypted_padded, block_size=16, style='pkcs7')
return decrypted.decode('utf-8')
"""
Youdao Web Translate API 封装模块
支持:自动获取密钥、AES解密、结果解析、格式化输出
"""
import time
import hashlib
import requests
import json
from base64 import urlsafe_b64decode
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
# ==================== 配置 ====================
HEADERS = {
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Content-Type": "application/x-www-form-urlencoded",
"Origin": "https://fanyi.youdao.com",
"Pragma": "no-cache",
"Referer": "https://fanyi.youdao.com/",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-site",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
"sec-ch-ua": '"Google Chrome";v="141", "Not?A_Brand";v="8", "Chromium";v="141"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"'
}
COOKIES = {
"OUTFOX_SEARCH_USER_ID": "-959393723@218.1.219.199",
"OUTFOX_SEARCH_USER_ID_NCOO": "106810021.57668559",
"DICT_DOCTRANS_SESSION_ID": "MGFmMjNlMGMtYzhhMS00MzlmLWE3ODktNTc3MGRiNTgwNWJj",
"_uetsid": "ff8d75d0b3a711f0bcc755bc4bf023ab",
"_uetvid": "7be93100979311f0bc83d941597f7661"
}
CLIENT = "fanyideskweb"
PRODUCT = "webfanyi"
DEFAULT_KEY = "yU5nT5dK3eZ1pI4j" # 初始key,用于获取 secretKey
# ==================== 工具函数 ====================
def _md5_hex(text: str) -> str:
"""计算字符串的 MD5 哈希值(十六进制小写)"""
return hashlib.md5(text.encode('utf-8')).hexdigest()
def _md5_bytes(data: str) -> bytes:
"""返回 MD5 哈希的原始字节"""
return hashlib.md5(data.encode('utf-8')).digest()
# ==================== 核心接口类 ====================
class YouDaoTranslator:
def __init__(self):
self.session = requests.Session()
self.session.headers.update(HEADERS)
self.session.cookies.update(COOKIES)
def _generate_sign(self, mystic_time: int, key: str) -> str:
"""生成请求签名"""
raw = f"client={CLIENT}&mysticTime={mystic_time}&product={PRODUCT}&key={key}"
return _md5_hex(raw)
def _generate_request_headers(self, sign: str, mystic_time: int) -> dict:
"""构建包含认证信息的请求头"""
return {
"client": CLIENT,
"product": PRODUCT,
"appVersion": "1.0.0",
"vendor": "web",
"pointParam": "client,mysticTime,product",
"mysticTime": str(mystic_time),
"keyfrom": "fanyi.web",
"mid": "1",
"screen": "1",
"model": "1",
"network": "wifi",
"abtest": "0",
"yduuid": "abcdefg",
"sign": sign
}
def fetch_aes_keys(self) -> dict:
"""从服务器获取动态加密密钥(secretKey, aesKey, aesIv)"""
url = "https://dict.youdao.com/webtranslate/key"
mystic_time = str(int(time.time() * 1000))
sign = self._generate_sign(mystic_time, DEFAULT_KEY)
params = {
"keyid": "webfanyi-key-getter-2025",
"sign": sign,
"client": CLIENT,
"product": PRODUCT,
"appVersion": "1.0.0",
"vendor": "web",
"pointParam": "client,mysticTime,product",
"mysticTime": mystic_time,
"keyfrom": "fanyi.web",
"mid": "1",
"screen": "1",
"model": "1",
"network": "wifi",
"abtest": "0",
"yduuid": "abcdefg"
}
try:
resp = self.session.get(url, params=params)
resp.raise_for_status()
data = resp.json().get("data", {})
return {
"secret_key": data["secretKey"],
"aes_key": data["aesKey"],
"aes_iv": data["aesIv"]
}
except Exception as e:
raise RuntimeError(f"获取密钥失败: {e}")
def translate(self, word: str) -> str:
"""发送翻译请求(返回可能是密文或明文)"""
keys = self.fetch_aes_keys()
mystic_time = int(time.time() * 1000)
sign = self._generate_sign(mystic_time, keys["secret_key"])
full_headers = {**HEADERS, **self._generate_request_headers(sign, mystic_time)}
data = {
"i": word,
"from": "auto",
"to": "",
"useTerm": "false",
"dictResult": "true",
"keyid": "webfanyi",
"sign": sign,
"client": CLIENT,
"product": PRODUCT,
"appVersion": "1.0.0",
"vendor": "web",
"pointParam": "client,mysticTime,product",
"mysticTime": mystic_time,
"keyfrom": "fanyi.web",
"mid": "1",
"screen": "1",
"model": "1",
"network": "wifi",
"abtest": "0",
"yduuid": "abcdefg"
}
try:
resp = self.session.post("https://dict.youdao.com/webtranslate", headers=full_headers, data=data)
resp.raise_for_status()
return resp.text.strip()
except Exception as e:
raise RuntimeError(f"翻译请求失败: {e}")
def decrypt_response(self, ciphertext_b64: str, aes_key: str, aes_iv: str) -> str:
"""解密 AES-CBC 加密的响应数据"""
try:
# 补齐 Base64 padding
missing = len(ciphertext_b64) % 4
if missing:
ciphertext_b64 += '=' * (4 - missing)
cipher_bytes = urlsafe_b64decode(ciphertext_b64)
key = _md5_bytes(aes_key)
iv = _md5_bytes(aes_iv)
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted_padded = cipher.decrypt(cipher_bytes)
decrypted = unpad(decrypted_padded, block_size=16, style='pkcs7')
return decrypted.decode('utf-8')
except Exception as e:
raise RuntimeError(f"解密失败: {e}")
# ==================== 结果解析器 ====================
class YouDaoParser:
@staticmethod
def parse(result: str) -> dict:
"""将 JSON 字符串解析为 Python 字典"""
if isinstance(result, dict):
return result
return json.loads(result)
@staticmethod
def format_meaning(data: dict) -> None:
"""格式化并打印单词详细信息"""
ec = data.get("dictResult", {}).get("ec", {}) or {}
word_info = ec.get("word", {})
trs = word_info.get("trs", [])
wfs = word_info.get("wfs", [])
exam_types = ec.get("exam_type", [])
src = data.get("translateResult", [[{"src": "", "tgt": ""}]])[0][0]["src"]
tgt = data.get("translateResult", [[{"src": "", "tgt": ""}]])[0][0]["tgt"]
print(f"📘 单词: {src} → {tgt}")
print()
usphone = word_info.get("usphone", "")
ukphone = word_info.get("ukphone", "")
if usphone or ukphone:
print(f"🇬🇧 英音: /{ukphone}/")
print(f"🇺🇸 美音: /{usphone}/")
print()
for item in trs:
print(f"{item.get('pos', '')} {item.get('tran', '')}")
print()
if exam_types:
print("🔖 考试标签:", " / ".join(exam_types))
print()
if wfs:
print("🔄 词形变化:")
for wf_item in wfs:
name = wf_item["wf"]["name"]
value = wf_item["wf"]["value"]
print(f" • {name}: {value}")
print()
# ==================== 简化接口 ====================
def lookup(word: str) -> None:
"""
快速查词接口
示例: lookup("append")
"""
translator = YouDaoTranslator()
try:
raw_response = translator.translate(word)
# 尝试判断是否为加密数据(Base64)
if raw_response.startswith('{') and 'code' in raw_response:
# 是明文 JSON
parsed = json.loads(raw_response)
else:
keys = translator.fetch_aes_keys()
decrypted = translator.decrypt_response(raw_response, keys["aes_key"], keys["aes_iv"])
parsed = json.loads(decrypted)
YouDaoParser.format_meaning(parsed)
except Exception as e:
print(f"❌ 查询失败: {e}")
# ==================== 使用示例 ====================
if __name__ == "__main__":
lookup("眼睛")
