在進行Python爬蟲開發(fā)時,遇到需要從Youdao翻譯網(wǎng)站抓取數(shù)據(jù)的情況,由于此翻譯網(wǎng)站對其API請求進行了加密,并且對返回的數(shù)據(jù)也采用了加密措施,因此直接的HTTP請求抓取不能直接獲取到翻譯結(jié)果。這就需要了解其加密和解密機制,進而在Python代碼中模擬這一過程,以實現(xiàn)數(shù)據(jù)的有效抓取和解密。
以下是一個實踐過程的描述:首先,訪問網(wǎng)站,使用鼠標右鍵打開開發(fā)者工具,并切換到網(wǎng)絡(Network)
面板,然后選擇Fetch/XHR
過濾器。在翻譯輸入框中輸入待翻譯的文本,例如中午好,回車執(zhí)行翻譯操作。在這個過程中,可以清晰地觀察到“中午好”被翻譯為“good afternoon”的過程涉及了三個網(wǎng)絡接口請求:
- 對 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
多次刷新,觀察到在兩個接口請求的載荷中,變化的參數(shù)主要是sign
、mysticTime
,其中mysticTime
代表的是時間戳,而
sign`看起來像是經(jīng)過某種加密算法處理的結(jié)果。
為了深入了解sign
參數(shù)的生成機制,可以采取全局搜索的方式,在瀏覽器開發(fā)者工具中通過快捷鍵Shift+Ctrl+F打開全局搜索功能,輸入sign
進行搜索。在搜索結(jié)果中,可能會出現(xiàn)大量與sign
相關(guān)的匹配項,這時需要細心地篩選,尋找與sign
生成邏輯相關(guān)的代碼片段。
通過這種方法,即便面對眾多的搜索結(jié)果,也能有目的地縮小范圍,逐步接近用于生成sign
值的加密算法的實現(xiàn)代碼。這個過程需要耐心和一定的運氣,因為正確的代碼片段可能隱藏在大量的匹配結(jié)果之中。找到這些關(guān)鍵代碼后,就可以進一步分析其邏輯,理解sign是如何根據(jù)時間戳mysticTime以及可能的其他因素生成的。
當在瀏覽器的開發(fā)者工具中全局搜索sign參數(shù)并注意到有key:value的組合形式,如
sign: k(o,e)
,這表明你可能已經(jīng)找到了生成sign值的關(guān)鍵代碼片段。這個k(o,e)
很可能是一個函數(shù)調(diào)用,其中k是一個函數(shù),而o
和e
是傳入該函數(shù)的參數(shù),這個函數(shù)負責生成sign的值。從JS代碼中可得,k(o,e)
方法里,o
參數(shù)是時間戳,e
為asdjnjfenknafdfsdfsd
,暫時不確定其是否固定。
client=${u}&mysticTime=${e}&product=$ri4ftqe&key=${t}
,e
是時間戳,u
和d
是常量,t
為asdjnjfenknafdfsdfsd
。
const u = "fanyideskweb"
, d = "webfanyi"
j
函數(shù)主要用于進行MD5加密,并將加密結(jié)果轉(zhuǎn)換為十六進制(hex)格式。
function j(e) {
return c.a.createHash("md5").update(e.toString()).digest("hex")
}
使用快捷鍵F8繼續(xù)執(zhí)行腳本,發(fā)現(xiàn)再次跑到斷點這里,又執(zhí)行了一次k
函數(shù),但是key對應的t值發(fā)生了變化,此時為fsdsogkndfokasodnaso
。
看起來兩個接口的
sign
值都是通過同一個函數(shù)k生成的,但關(guān)鍵在于它們使用key不同。第一次請求時使用的key是asdjnjfenknafdfsdfsd
,而第二次請求使用的key是fsdsogkndfokasodnaso
,而且這個第二次使用的key是從第一次接口請求的返回數(shù)據(jù)中獲得的。翻譯結(jié)果返回的是密文,這意味著翻譯服務還采用了某種形式的響應加密。這是一個額外的安全措施,用于保護數(shù)據(jù)在傳輸過程中的安全,防止未經(jīng)授權(quán)的訪問者直接讀取響應內(nèi)容。要解密這些響應,需要了解加密和解密的具體機制。
Z21kD9ZK1ke6ugku2ccWu4n6eLnvoDT0YgGi0y3g-v0B9sYqg8L9D6UERNozYOHqqXyAEo6co8ruGELvtq19adBTgmgtq9XKmTb3RUrbqN9QTNj_RBof8RxaKuaSRS63DlaZVeSgjC6HDrIjQM2yVqVOY1GtO-Re0xcRZML_FmM_6JKN9W6IDSn4K_5-Kfx3SUOxAZ90lJG8iBReRkH8OxCAPaKK2lG6DJlyoHkMHul1MJiAWkni2JX_FiRkypw7KdwvveOaJYsrwRQEIt2GJq8QjqNC8r2oluEzx36x0V20Pdj1HUleZ4uH0-AU8xNW2OmAnLOC7limxtYMKzdwx6GJz0ZqqEmhrmnMw-x1Xz2CFQ4XSJ09L1fsDYsX6uoidgRIq3CWRXIWkBh_9I0EA2D-hhk8m5JYOdLYPY3Pb5ncayIPXGfwFvdkooQYQuO41tfBeOitzdU0cz2z4g6_4A==
有點懵,因為響應結(jié)果中并沒有關(guān)鍵字,難以使用關(guān)鍵字去搜索。此時驀然回首,會發(fā)現(xiàn)第一個接口請求返回的結(jié)果:
在此接口請求響應中發(fā)現(xiàn)了aesIv和aesKey這樣的關(guān)鍵字,這是否暗示了可能使用了AES加密算法呢?在AES加密中,aesKey用作加密和解密的密鑰,而aesIv(初始化向量)用于確保即使多次使用相同的密鑰加密相同的文本,加密結(jié)果也會不同,從而增加了加密數(shù)據(jù)的安全性。
進行全局搜索aes
關(guān)鍵字是一個直接且有效的方法來尋找解密邏輯的實現(xiàn)代碼,容易發(fā)現(xiàn):
直接雙擊進入源碼,打上斷點單步調(diào)試:
function y(e) {
return c.a.createHash("md5").update(e).digest() #進行MD5加密
}
從上圖中,可看出R函數(shù)中的t
就是翻譯結(jié)果的加密數(shù)據(jù),o
和n
就是第一次請求返回的aesKey
和aesIv
。
aes-128-cbc
這個加密算法,我也不懂,直接借助于chatgpt,讓其給出的解密示例:
const crypto = require('crypto');
// 你的初始化向量 (IV)
const iv = '1234567812345678';
// 你的密鑰 (Key)
const key = '1234567812345678';
// 加密后的數(shù)據(jù) (CipherText),這里使用的是Base64編碼的字符串示例
const cipherText = '加密數(shù)據(jù)的Base64編碼字符串';
// 創(chuàng)建一個解密器實例
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);
仔細對照一下并發(fā)現(xiàn),a
Unit8Arry(16)就是密鑰,i
Unit8Arry(16)是初始化向量。創(chuàng)建有道翻譯.js
文件,粘貼、改寫源碼中摳出來的JS代碼:
var crypto = require('crypto');//內(nèi)置模塊
// 第一次請求,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=$mrfdrbh&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)建一個解密器實例
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.
假設有成百上千個文本需要翻譯,可使用進程池按此方法進行快速翻譯。改天使用chatgpt隨機生成100個復雜單詞進行驗證一下。