在进行Python爬虫开发时,遇到需要从Youdao翻译网站抓取数据的情况,由于此翻译网站对其API请求进行了加密,并且对返回的数据也采用了加密措施,因此直接的HTTP请求抓取不能直接获取到翻译结果。这就需要了解其加密和解密机制,进而在Python代码中模拟这一过程,以实现数据的有效抓取和解密。
以下是一个实践过程的描述:首先,访问网站,使用鼠标右键打开开发者工具,并切换到网络(Network)
面板,然后选择Fetch/XHR
过滤器。在翻译输入框中输入待翻译的文本,例如中午好,回车执行翻译操作。在这个过程中,可以清晰地观察到“中午好”被翻译为“good afternoon”的过程涉及了三个网络接口请求:
- 对 https://dict.youdao.com/webtranslate/key 的
GET
请求; - 对 https://dict.youdao.com/webtranslate 和 https://dict.youdao.com/keyword/key 的
POST
请求。
第一个接口的请求载荷(payload)如下,有些非必须,可尝试去除:
keyid: webfanyi-key-getter
sign: b7150d775d0039168fb116052f1f38ad
client: fanyideskweb
product: webfanyi
appVersion: 1.0.0
vendor: web
pointParam: client,mysticTime,product
mysticTime: 1709517202639
keyfrom: fanyi.web
mid: 1
screen: 1
model: 1
network: wifi
abtest: 0
yduuid: abcdefg
第二个webtranslate接口里的请求载荷如下:
i: 中午好
from: zh-CHS
to: en
domain: 0
dictResult: true
keyid: webfanyi
sign: d08172c36481bbce6f1a2bb159ebc981
client: fanyideskweb
product: webfanyi
appVersion: 1.0.0
vendor: web
pointParam: client,mysticTime,product
mysticTime: 1709517203277
keyfrom: fanyi.web
mid: 1
screen: 1
model: 1
network: wifi
abtest: 0
yduuid: abcdefg
多次刷新,观察到在两个接口请求的载荷中,变化的参数主要是sign
、mysticTime
,其中mysticTime
代表的是时间戳,而
sign`看起来像是经过某种加密算法处理的结果。
为了深入了解sign
参数的生成机制,可以采取全局搜索的方式,在浏览器开发者工具中通过快捷键Shift+Ctrl+F打开全局搜索功能,输入sign
进行搜索。在搜索结果中,可能会出现大量与sign
相关的匹配项,这时需要细心地筛选,寻找与sign
生成逻辑相关的代码片段。
通过这种方法,即便面对众多的搜索结果,也能有目的地缩小范围,逐步接近用于生成sign
值的加密算法的实现代码。这个过程需要耐心和一定的运气,因为正确的代码片段可能隐藏在大量的匹配结果之中。找到这些关键代码后,就可以进一步分析其逻辑,理解sign是如何根据时间戳mysticTime以及可能的其他因素生成的。
当在浏览器的开发者工具中全局搜索sign参数并注意到有key:value的组合形式,如
sign: k(o,e)
,这表明你可能已经找到了生成sign值的关键代码片段。这个k(o,e)
很可能是一个函数调用,其中k是一个函数,而o
和e
是传入该函数的参数,这个函数负责生成sign的值。从JS代码中可得,k(o,e)
方法里,o
参数是时间戳,e
为asdjnjfenknafdfsdfsd
,暂时不确定其是否固定。
client=${u}&mysticTime=${e}&product=${d}&key=${t}
,e
是时间戳,u
和d
是常量,t
为asdjnjfenknafdfsdfsd
。
const u = "fanyideskweb"
, d = "webfanyi"
j
函数主要用于进行MD5加密,并将加密结果转换为十六进制(hex)格式。
function j(e) {
return c.a.createHash("md5").update(e.toString()).digest("hex")
}
使用快捷键F8继续执行脚本,发现再次跑到断点这里,又执行了一次k
函数,但是key对应的t值发生了变化,此时为fsdsogkndfokasodnaso
。
看起来两个接口的
sign
值都是通过同一个函数k生成的,但关键在于它们使用key不同。第一次请求时使用的key是asdjnjfenknafdfsdfsd
,而第二次请求使用的key是fsdsogkndfokasodnaso
,而且这个第二次使用的key是从第一次接口请求的返回数据中获得的。翻译结果返回的是密文,这意味着翻译服务还采用了某种形式的响应加密。这是一个额外的安全措施,用于保护数据在传输过程中的安全,防止未经授权的访问者直接读取响应内容。要解密这些响应,需要了解加密和解密的具体机制。
Z21kD9ZK1ke6ugku2ccWu4n6eLnvoDT0YgGi0y3g-v0B9sYqg8L9D6UERNozYOHqqXyAEo6co8ruGELvtq19adBTgmgtq9XKmTb3RUrbqN9QTNj_RBof8RxaKuaSRS63DlaZVeSgjC6HDrIjQM2yVqVOY1GtO-Re0xcRZML_FmM_6JKN9W6IDSn4K_5-Kfx3SUOxAZ90lJG8iBReRkH8OxCAPaKK2lG6DJlyoHkMHul1MJiAWkni2JX_FiRkypw7KdwvveOaJYsrwRQEIt2GJq8QjqNC8r2oluEzx36x0V20Pdj1HUleZ4uH0-AU8xNW2OmAnLOC7limxtYMKzdwx6GJz0ZqqEmhrmnMw-x1Xz2CFQ4XSJ09L1fsDYsX6uoidgRIq3CWRXIWkBh_9I0EA2D-hhk8m5JYOdLYPY3Pb5ncayIPXGfwFvdkooQYQuO41tfBeOitzdU0cz2z4g6_4A==
有点懵,因为响应结果中并没有关键字,难以使用关键字去搜索。此时蓦然回首,会发现第一个接口请求返回的结果:
在此接口请求响应中发现了aesIv和aesKey这样的关键字,这是否暗示了可能使用了AES加密算法呢?在AES加密中,aesKey用作加密和解密的密钥,而aesIv(初始化向量)用于确保即使多次使用相同的密钥加密相同的文本,加密结果也会不同,从而增加了加密数据的安全性。
进行全局搜索aes
关键字是一个直接且有效的方法来寻找解密逻辑的实现代码,容易发现:
直接双击进入源码,打上断点单步调试:
function y(e) {
return c.a.createHash("md5").update(e).digest() #进行MD5加密
}
从上图中,可看出R函数中的t
就是翻译结果的加密数据,o
和n
就是第一次请求返回的aesKey
和aesIv
。
aes-128-cbc
这个加密算法,我也不懂,直接借助于chatgpt,让其给出的解密示例:
const crypto = require('crypto');
// 你的初始化向量 (IV)
const iv = '1234567812345678';
// 你的密钥 (Key)
const key = '1234567812345678';
// 加密后的数据 (CipherText),这里使用的是Base64编码的字符串示例
const cipherText = '加密数据的Base64编码字符串';
// 创建一个解密器实例
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
// 将加密数据转换为解密后的明文,使用了'base64'作为输入编码,'utf8'作为输出编码
let decrypted = decipher.update(cipherText, 'base64', 'utf8');
// 最后调用final方法完成解密过程,并获取剩余的解密内容
decrypted += decipher.final('utf8');
console.log(decrypted);
仔细对照一下并发现,a
Unit8Arry(16)就是密钥,i
Unit8Arry(16)是初始化向量。创建有道翻译.js
文件,粘贴、改写源码中抠出来的JS代码:
var crypto = require('crypto');//内置模块
// 第一次请求,key1是'asdjnjfenknafdfsdfsd',
// 第二次请求,key2是'fsdsogkndfokasodnaso'
const u = 'fanyideskweb';
const d = 'webfanyi';
const key = 'asdjnjfenknafdfsdfsd'
function j(e) {
return crypto.createHash("md5").update(e.toString()).digest("hex")
}
function k(time,key) {
return j(`client=${u}&mysticTime=${time}&product=${d}&key=${key}`)
}
function y(e) {
return crypto.createHash("md5").update(e).digest()
}
function aes_decrypt(cipherText,aesIv,aesKey) {
const uint8Array_iv = new Uint8Array(Buffer.from(y(aesIv)));
const uint8Array_key = new Uint8Array(Buffer.from(y(aesKey)));
// 创建一个解密器实例
const decipher = crypto.createDecipheriv('aes-128-cbc', uint8Array_key, uint8Array_iv);
// 将加密数据转换为解密后的明文,使用了'base64'作为输入编码,'utf8'作为输出编码
let decrypted = decipher.update(cipherText, 'base64', 'utf8');
// 最后调用final方法完成解密过程,并获取剩余的解密内容
decrypted += decipher.final('utf8');
return decrypted
}
Python代码如下:
import json
import time
from random import uniform
import requests
import execjs
class YouDaoTranslate:
def __init__(self):
self.session = requests.Session()
self.session.headers.update({
'Referer': 'https://fanyi.youdao.com/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
})
self.key_get_url = 'https://dict.youdao.com/webtranslate/key'
self.translate_url = 'https://dict.youdao.com/webtranslate'
self.get_cookie_url = 'https://rlogs.youdao.com/rlog.php?'
self.get_cookie()
def get_cookie(self):
params = {
"_npid": "fanyiweb",
"_ncat": "event",
"_ncoo": str(2147483647 * uniform(0, 1)),
"nssn": "NULL",
"_ntms": str(int(time.time() * 1000)),
}
try:
self.session.get(self.get_cookie_url, params=params)
except requests.RequestException as e:
print(f"Error getting cookies: {e}")
def get_sign(self, key='asdjnjfenknafdfsdfsd'):
try:
with open('有道翻译.js', 'r', encoding='utf-8') as f:
context = execjs.compile(f.read())
current_time = str(int(time.time() * 1000))
sign = context.call('k', current_time, key)
return current_time, sign
except Exception as e:
print(f"Error generating sign: {e}")
return None, None
def get_keys(self):
current_time, sign = self.get_sign()
if current_time and sign:
params = {
'keyid': 'webfanyi-key-getter',
'sign': sign,
'client': 'fanyideskweb',
'product': 'webfanyi',
'pointParam': 'client,mysticTime,product',
'mysticTime': current_time,
}
try:
response = self.session.get(self.key_get_url, params=params).json()
return response['data']['secretKey'], response['data']['aesKey'], response['data']['aesIv']
except requests.RequestException as e:
print(f"Error getting keys: {e}")
return None, None, None
def get_translate_data(self, translate_text, cur_time, sign):
data = {
'i': translate_text,
'keyid': 'webfanyi',
'sign': sign,
'client': 'fanyideskweb',
'product': 'webfanyi',
'appVersion': '1.0.0',
'vendor': 'web',
'pointParam': 'client,mysticTime,product',
'mysticTime': cur_time,
'keyfrom': 'fanyi.web'
}
try:
response = self.session.post(self.translate_url, data=data).text
return response
except requests.RequestException as e:
print(f"Error getting translation data: {e}")
return None
def main(self, translate_text):
secretKey, aesKey, aesIv = self.get_keys()
if secretKey and aesKey and aesIv:
current_time, sign = self.get_sign(key=secretKey)
cipherText = self.get_translate_data(translate_text, current_time, sign)
if cipherText:
try:
with open('有道翻译.js', 'r', encoding='utf-8') as f:
context = execjs.compile(f.read())
result = context.call('aes_decrypt', cipherText, aesIv, aesKey)
translated_text = json.loads(result)['translateResult'][0][0]['tgt']
print(f'[{translate_text}]翻译的结果是:{translated_text}')
except Exception as e:
print(f"Error decrypting translation: {e}")
if __name__ == '__main__':
YouDaoTranslate().main('中午好')
执行结果:[中午好]翻译的结果是:good afternoon.
假设有成百上千个文本需要翻译,可使用进程池按此方法进行快速翻译。改天使用chatgpt随机生成100个复杂单词进行验证一下。