Python爬蟲:JS逆向初學(xué)習(xí)

在進(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)求:

  1. 對(duì) https://dict.youdao.com/webtranslate/keyGET請(qǐng)求;
  2. 對(duì) https://dict.youdao.com/webtranslatehttps://dict.youdao.com/keyword/keyPOST請(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ù)主要是signmysticTime晦譬,其中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ù)拣宏,而oe是傳入該函數(shù)的參數(shù),這個(gè)函數(shù)負(fù)責(zé)生成sign的值杠人。

從JS代碼中可得勋乾,k(o宋下,e)方法里,o參數(shù)是時(shí)間戳辑莫,easdjnjfenknafdfsdfsd学歧,暫時(shí)不確定其是否固定。
client=${u}&mysticTime=${e}&product=$2uamsum&key=${t},e是時(shí)間戳各吨,ud是常量枝笨,tasdjnjfenknafdfsdfsd

  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ù),on就是第一次請(qǐng)求返回的aesKeyaesIv喷屋。
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)就是密鑰偷俭,iUnit8Arry(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)證一下。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末崖堤,一起剝皮案震驚了整個(gè)濱河市侍咱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌密幔,老刑警劉巖楔脯,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異胯甩,居然都是意外死亡昧廷,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門蜡豹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人溉苛,你說我怎么就攤上這事镜廉。” “怎么了愚战?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵娇唯,是天一觀的道長齐遵。 經(jīng)常有香客問我,道長塔插,這世上最難降的妖魔是什么梗摇? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮想许,結(jié)果婚禮上伶授,老公的妹妹穿的比我還像新娘。我一直安慰自己流纹,他們只是感情好糜烹,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著漱凝,像睡著了一般疮蹦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上茸炒,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天愕乎,我揣著相機(jī)與錄音,去河邊找鬼壁公。 笑死感论,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的贮尖。 我是一名探鬼主播笛粘,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼湿硝!你這毒婦竟也來了薪前?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤关斜,失蹤者是張志新(化名)和其女友劉穎示括,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體痢畜,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡垛膝,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了丁稀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吼拥。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖线衫,靈堂內(nèi)的尸體忽然破棺而出凿可,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布枯跑,位于F島的核電站惨驶,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏敛助。R本人自食惡果不足惜粗卜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望纳击。 院中可真熱鬧续扔,春花似錦、人聲如沸评疗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽百匆。三九已至砌些,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間加匈,已是汗流浹背存璃。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留雕拼,地道東北人纵东。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像啥寇,于是被迫代替她去往敵國和親偎球。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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