在進(jìn)行Python爬蟲開發(fā)時(shí)共螺,遇到需要從Youdao翻譯網(wǎng)站抓取數(shù)據(jù)的情況,由于此翻譯網(wǎng)站對(duì)其API請(qǐng)求進(jìn)行了加密情竹,并且對(duì)返回的數(shù)據(jù)也采用了加密措施藐不,因此直接的HTTP請(qǐng)求抓取不能直接獲取到翻譯結(jié)果。這就需要了解其加密和解密機(jī)制秦效,進(jìn)而在Python代碼中模擬這一過程雏蛮,以實(shí)現(xiàn)數(shù)據(jù)的有效抓取和解密。
以下是一個(gè)實(shí)踐過程的描述:首先阱州,訪問網(wǎng)站底扳,使用鼠標(biāo)右鍵打開開發(fā)者工具,并切換到網(wǎng)絡(luò)(Network)
面板贡耽,然后選擇Fetch/XHR
過濾器衷模。在翻譯輸入框中輸入待翻譯的文本鹊汛,例如中午好,回車執(zhí)行翻譯操作阱冶。在這個(gè)過程中刁憋,可以清晰地觀察到“中午好”被翻譯為“good afternoon”的過程涉及了三個(gè)網(wǎng)絡(luò)接口請(qǐng)求:
- 對(duì) https://dict.youdao.com/webtranslate/key 的
GET
請(qǐng)求; - 對(duì) https://dict.youdao.com/webtranslate 和 https://dict.youdao.com/keyword/key 的
POST
請(qǐng)求木蹬。
第一個(gè)接口的請(qǐng)求載荷(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
第二個(gè)webtranslate接口里的請(qǐng)求載荷如下:
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
多次刷新镊叁,觀察到在兩個(gè)接口請(qǐng)求的載荷中尘颓,變化的參數(shù)主要是sign
、mysticTime
晦譬,其中mysticTime
代表的是時(shí)間戳疤苹,而
sign`看起來像是經(jīng)過某種加密算法處理的結(jié)果。
為了深入了解sign
參數(shù)的生成機(jī)制敛腌,可以采取全局搜索的方式卧土,在瀏覽器開發(fā)者工具中通過快捷鍵Shift+Ctrl+F打開全局搜索功能,輸入sign
進(jìn)行搜索像樊。在搜索結(jié)果中尤莺,可能會(huì)出現(xiàn)大量與sign
相關(guān)的匹配項(xiàng),這時(shí)需要細(xì)心地篩選生棍,尋找與sign
生成邏輯相關(guān)的代碼片段颤霎。
通過這種方法,即便面對(duì)眾多的搜索結(jié)果涂滴,也能有目的地縮小范圍捷绑,逐步接近用于生成sign
值的加密算法的實(shí)現(xiàn)代碼。這個(gè)過程需要耐心和一定的運(yùn)氣氢妈,因?yàn)檎_的代碼片段可能隱藏在大量的匹配結(jié)果之中。找到這些關(guān)鍵代碼后段多,就可以進(jìn)一步分析其邏輯首量,理解sign是如何根據(jù)時(shí)間戳mysticTime以及可能的其他因素生成的。
當(dāng)在瀏覽器的開發(fā)者工具中全局搜索sign參數(shù)并注意到有key:value的組合形式进苍,如
sign: k(o,e)
加缘,這表明你可能已經(jīng)找到了生成sign值的關(guān)鍵代碼片段。這個(gè)k(o,e)
很可能是一個(gè)函數(shù)調(diào)用觉啊,其中k是一個(gè)函數(shù)拣宏,而o
和e
是傳入該函數(shù)的參數(shù),這個(gè)函數(shù)負(fù)責(zé)生成sign的值杠人。從JS代碼中可得勋乾,k(o宋下,e)
方法里,o
參數(shù)是時(shí)間戳辑莫,e
為asdjnjfenknafdfsdfsd
学歧,暫時(shí)不確定其是否固定。
client=${u}&mysticTime=${e}&product=$2uamsum&key=${t}
,e
是時(shí)間戳各吨,u
和d
是常量枝笨,t
為asdjnjfenknafdfsdfsd
。
const u = "fanyideskweb"
, d = "webfanyi"
j
函數(shù)主要用于進(jìn)行MD5加密揭蜒,并將加密結(jié)果轉(zhuǎn)換為十六進(jìn)制(hex)格式横浑。
function j(e) {
return c.a.createHash("md5").update(e.toString()).digest("hex")
}
使用快捷鍵F8繼續(xù)執(zhí)行腳本,發(fā)現(xiàn)再次跑到斷點(diǎn)這里屉更,又執(zhí)行了一次k
函數(shù)徙融,但是key對(duì)應(yīng)的t值發(fā)生了變化,此時(shí)為fsdsogkndfokasodnaso
偶垮。
看起來兩個(gè)接口的
sign
值都是通過同一個(gè)函數(shù)k生成的张咳,但關(guān)鍵在于它們使用key不同。第一次請(qǐng)求時(shí)使用的key是asdjnjfenknafdfsdfsd
似舵,而第二次請(qǐng)求使用的key是fsdsogkndfokasodnaso
脚猾,而且這個(gè)第二次使用的key是從第一次接口請(qǐng)求的返回?cái)?shù)據(jù)中獲得的。翻譯結(jié)果返回的是密文砚哗,這意味著翻譯服務(wù)還采用了某種形式的響應(yīng)加密龙助。這是一個(gè)額外的安全措施,用于保護(hù)數(shù)據(jù)在傳輸過程中的安全蛛芥,防止未經(jīng)授權(quán)的訪問者直接讀取響應(yīng)內(nèi)容提鸟。要解密這些響應(yīng),需要了解加密和解密的具體機(jī)制仅淑。
Z21kD9ZK1ke6ugku2ccWu4n6eLnvoDT0YgGi0y3g-v0B9sYqg8L9D6UERNozYOHqqXyAEo6co8ruGELvtq19adBTgmgtq9XKmTb3RUrbqN9QTNj_RBof8RxaKuaSRS63DlaZVeSgjC6HDrIjQM2yVqVOY1GtO-Re0xcRZML_FmM_6JKN9W6IDSn4K_5-Kfx3SUOxAZ90lJG8iBReRkH8OxCAPaKK2lG6DJlyoHkMHul1MJiAWkni2JX_FiRkypw7KdwvveOaJYsrwRQEIt2GJq8QjqNC8r2oluEzx36x0V20Pdj1HUleZ4uH0-AU8xNW2OmAnLOC7limxtYMKzdwx6GJz0ZqqEmhrmnMw-x1Xz2CFQ4XSJ09L1fsDYsX6uoidgRIq3CWRXIWkBh_9I0EA2D-hhk8m5JYOdLYPY3Pb5ncayIPXGfwFvdkooQYQuO41tfBeOitzdU0cz2z4g6_4A==
有點(diǎn)懵称勋,因?yàn)轫憫?yīng)結(jié)果中并沒有關(guān)鍵字,難以使用關(guān)鍵字去搜索涯竟。此時(shí)驀然回首赡鲜,會(huì)發(fā)現(xiàn)第一個(gè)接口請(qǐng)求返回的結(jié)果:
在此接口請(qǐng)求響應(yīng)中發(fā)現(xiàn)了aesIv和aesKey這樣的關(guān)鍵字,這是否暗示了可能使用了AES加密算法呢庐船?在AES加密中银酬,aesKey用作加密和解密的密鑰,而aesIv(初始化向量)用于確保即使多次使用相同的密鑰加密相同的文本筐钟,加密結(jié)果也會(huì)不同揩瞪,從而增加了加密數(shù)據(jù)的安全性。
進(jìn)行全局搜索aes
關(guān)鍵字是一個(gè)直接且有效的方法來尋找解密邏輯的實(shí)現(xiàn)代碼篓冲,容易發(fā)現(xiàn):
直接雙擊進(jìn)入源碼李破,打上斷點(diǎn)單步調(diào)試:
function y(e) {
return c.a.createHash("md5").update(e).digest() #進(jìn)行MD5加密
}
從上圖中宠哄,可看出R函數(shù)中的t
就是翻譯結(jié)果的加密數(shù)據(jù),o
和n
就是第一次請(qǐng)求返回的aesKey
和aesIv
喷屋。
aes-128-cbc
這個(gè)加密算法琳拨,我也不懂,直接借助于chatgpt屯曹,讓其給出的解密示例:
const crypto = require('crypto');
// 你的初始化向量 (IV)
const iv = '1234567812345678';
// 你的密鑰 (Key)
const key = '1234567812345678';
// 加密后的數(shù)據(jù) (CipherText)狱庇,這里使用的是Base64編碼的字符串示例
const cipherText = '加密數(shù)據(jù)的Base64編碼字符串';
// 創(chuàng)建一個(gè)解密器實(shí)例
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
// 將加密數(shù)據(jù)轉(zhuǎn)換為解密后的明文,使用了'base64'作為輸入編碼恶耽,'utf8'作為輸出編碼
let decrypted = decipher.update(cipherText, 'base64', 'utf8');
// 最后調(diào)用final方法完成解密過程密任,并獲取剩余的解密內(nèi)容
decrypted += decipher.final('utf8');
console.log(decrypted);
仔細(xì)對(duì)照一下并發(fā)現(xiàn),a
Unit8Arry(16)就是密鑰偷俭,i
Unit8Arry(16)是初始化向量浪讳。創(chuàng)建有道翻譯.js
文件,粘貼涌萤、改寫源碼中摳出來的JS代碼:
var crypto = require('crypto');//內(nèi)置模塊
// 第一次請(qǐng)求淹遵,key1是'asdjnjfenknafdfsdfsd',
// 第二次請(qǐng)求,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=$s4gayoo&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)));
// 創(chuàng)建一個(gè)解密器實(shí)例
const decipher = crypto.createDecipheriv('aes-128-cbc', uint8Array_key, uint8Array_iv);
// 將加密數(shù)據(jù)轉(zhuǎn)換為解密后的明文负溪,使用了'base64'作為輸入編碼透揣,'utf8'作為輸出編碼
let decrypted = decipher.update(cipherText, 'base64', 'utf8');
// 最后調(diào)用final方法完成解密過程,并獲取剩余的解密內(nèi)容
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}]翻譯的結(jié)果是:{translated_text}')
except Exception as e:
print(f"Error decrypting translation: {e}")
if __name__ == '__main__':
YouDaoTranslate().main('中午好')
執(zhí)行結(jié)果:[中午好]翻譯的結(jié)果是:good afternoon.
假設(shè)有成百上千個(gè)文本需要翻譯川抡,可使用進(jìn)程池按此方法進(jìn)行快速翻譯辐真。改天使用chatgpt隨機(jī)生成100個(gè)復(fù)雜單詞進(jìn)行驗(yàn)證一下。