逆向

页面 URL:https://fanyi.youdao.com/index.html?keyfrom=baiduvipdc#/TextTranslate

image.png

翻译的内容不一样,sign 就不一样,还有 mysticTime 也不一样。由于还没有逆向,所以这里的 mysticTime 可以猜测是标准时间。

解密

接下来看调用栈:

image.png

image.png

搜一下 URL 就可以找到地方了,这里看起来像个 lambda 表达式,我不知道这个在 JS 里叫啥,暂且这么叫他把,我把他写成一个函数,这里有很多逗号表达式,只要取后面的值就好了:

function fn(e, t){  

    n.H("https://dict.youdao.com/webtranslate", o.A(o.A({}, e), k(t)), {  
            headers: {"Content-Type": "application/x-www-form-urlencoded"}  
        }  
    )  
}

然后需要把缺少的函数补全:

image.png

n.H 函数如上图,这里可以看到是一个 axios 的 post 请求。这里就是一个标准的 Promise 的异步写法:

n = new Promise(((resolve, reject) => {
    post().then(
        ... // 这里就是resolve,表示请求成功执行
    )
    catch(...){
        ... // 这里就是reject,表示请求失败执行
    }
}))

有了上面的 axios 前置知识,就可以知道,请求成功了会走 o(e.data),所以这里大概率就是解密的地方:

image.png

这里其实是请求来的两个值。

加密

在之前请求的地方,组装 post 请求,可以看到组装 payload 的地方,组装的函数中有 sign 的生成函数:

image.png

sign 的生成函数如下,注意这里的 _ 其实是个函数:

image.png

挺好找的,就在上面:

image.png

那么这里就可以来实现一下发送请求的 payload:

image.png

尝试发送请求,可以得到结果:

image.png

有道很多东西都是写死的,最后 js 如下:

const crypto = require('crypto');  

// 加密  

function _(e) {  
    return crypto.createHash("md5").update(e.toString()).digest("hex")  
}  
function S(e) {  
    return _(`client=fanyideskweb&mysticTime=${e}&product=webfanyi&key=Vy4EQ1uwPkUoqvcP1nIu6WiAjxFeA3Y2`)  
}  

function k(e, t) {  
    const a = (new Date).getTime();  

    return {  
        sign: S(a, e),  
        client: 'fanyideskweb',  
        product: 'webfanyi',  
        appVersion: '1.0.0',  
        vendor: 'web',  
        pointParam: 'client,mysticTime,product',  
        mysticTime: a,  
        keyfrom: 'fanyi.web',  
        mid: 1,  
        screen: 1,  
        model: 1,  
        network: 'wifi',  
        abtest: 0,  
        yduuid: t || "abcdefg"  
    }  
}  

function a(e, t, n) {  
    return t = o(t),  
    t in e ? Object.defineProperty(e, t, {  
        value: n,  
        enumerable: !0,  
        configurable: !0,  
        writable: !0  
    }) : e[t] = n,  
    e  
}  

function i(e, t) {  
    var n = Object.keys(e);  
    if (Object.getOwnPropertySymbols) {  
        var r = Object.getOwnPropertySymbols(e);  
        t && (r = r.filter((function(t) {  
            return Object.getOwnPropertyDescriptor(e, t).enumerable  
        }  
        ))),  
        n.push.apply(n, r)  
    }  
    return n  
}  

function o(e) {  
    for (var t = 1; t < arguments.length; t++) {  
        var n = null != arguments[t] ? arguments[t] : {};  
        t % 2 ? i(Object(n), !0).forEach((function(t) {  
            a(e, t, n[t])  
        }  
        )) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n)) : i(Object(n)).forEach((function(t) {  
            Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t))  
        }  
        ))  
    }  
    return e  
}  

function fn(word) {  
    e = {  
        "dictResult": true,  
        "from": "auto",  
        "i": word,  
        "keyid": "webfanyi",  
        "to": "",  
        "useTerm": false  
    }  

    return o(o({}, e), k("Vy4EQ1uwPkUoqvcP1nIu6WiAjxFeA3Y2"))  
}  

//  jiemi  

function T(e) {  
    return crypto.createHash("md5").update(e).digest()  
}  

function _jiemi(e,t,a){  
    if (!e)  
        return null;
    // 这里查阅官方文档可以知道 createDecipheriv接收的参数,所以可以用Buffer初始化
    const o = Buffer.alloc(16, T(t))  
      , n = Buffer.alloc(16, T(a))  
      , r = crypto.createDecipheriv("aes-128-cbc", o, n);  
    let s = r.update(e, "base64", "utf-8");  
    return s += r.final("utf-8"), s  
}  

function jiemi(o) {  
    // da.A.cancelLastGpt();  
    const a = _jiemi(o, 'ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl',  
        'ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4')  
      , word = a ? JSON.parse(a) : {};  
    return a;  
}

爬虫

import json  
import requests  
import execjs  

f = open("youdao.js", "r", encoding="utf-8")  
js_code = f.read()  
f.close()  

js = execjs.compile(js_code)  

headers = {  
    "Accept": "application/json, text/plain, */*",  
    "Accept-Encoding": "gzip, deflate, br",  
    "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",  
    "Cache-Control": "no-cache",  
    "Connection": "keep-alive",  
    "Content-Length": "314",  
    "Content-Type": "application/x-www-form-urlencoded",  
    "Cookie": "hb_MA-B0D8-94CBE089C042_source=www.baidu.com; OUTFOX_SEARCH_USER_ID=-414266408@39.185.201.22; OUTFOX_SEARCH_USER_ID_NCOO=1106084277.0425632; _uetsid=bba76df0032311f0bfb71bca4d641cc2; _uetvid=bba77a20032311f08d2cd70d07ef4757; DICT_DOCTRANS_SESSION_ID=YzdjYmE4MmItY2I5NC00MDQ5LWI0NzUtOTRmMTIwNGZhYzQ0",  
    "Host": "dict.youdao.com",  
    "Origin": "https://fanyi.youdao.com",  
    "Pragma": "no-cache",  
    "Referer": "https://fanyi.youdao.com/",  
    "Sec-Ch-Ua": "\"Chromium\";v=\"122\", \"Not(A:Brand\";v=\"24\", \"Google Chrome\";v=\"122\"",  
    "Sec-Ch-Ua-Mobile": "?0",  
    "Sec-Ch-Ua-Platform": "\"Windows\"",  
    "Sec-Fetch-Dest": "empty",  
    "Sec-Fetch-Mode": "cors",  
    "Sec-Fetch-Site": "same-site",  
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.95 Safari/537.36"  
}  

word = input("输入需要搜索的单词>>")  

resp = requests.post("https://dict.youdao.com/webtranslate", headers=headers, data=js.call("fn", word))  

res = js.call("jiemi", resp.text)  

print(res)