Python爬蟲:JS逆向初學習

在進行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)絡接口請求:

  1. https://dict.youdao.com/webtranslate/keyGET請求;
  2. https://dict.youdao.com/webtranslatehttps://dict.youdao.com/keyword/keyPOST請求。

第一個接口的請求載荷(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ù)主要是signmysticTime,其中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ù),而oe是傳入該函數(shù)的參數(shù),這個函數(shù)負責生成sign的值。

從JS代碼中可得,k(o,e)方法里,o參數(shù)是時間戳,easdjnjfenknafdfsdfsd,暫時不確定其是否固定。
client=${u}&mysticTime=${e}&product=$ri4ftqe&key=${t},e是時間戳,ud是常量,tasdjnjfenknafdfsdfsd

  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ù),on就是第一次請求返回的aesKeyaesIv
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)就是密鑰,iUnit8Arry(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個復雜單詞進行驗證一下。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,250評論 6 530
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 97,923評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,041評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,475評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,253評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,801評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,882評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,023評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,530評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,494評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,639評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,177評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,890評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,289評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,552評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,242評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,626評論 2 370

推薦閱讀更多精彩內(nèi)容