当前位置: 首页>后端>正文

Python爬虫:JS逆向初学习

在进行Python爬虫开发时,遇到需要从Youdao翻译网站抓取数据的情况,由于此翻译网站对其API请求进行了加密,并且对返回的数据也采用了加密措施,因此直接的HTTP请求抓取不能直接获取到翻译结果。这就需要了解其加密和解密机制,进而在Python代码中模拟这一过程,以实现数据的有效抓取和解密。

以下是一个实践过程的描述:首先,访问网站,使用鼠标右键打开开发者工具,并切换到网络(Network)面板,然后选择Fetch/XHR过滤器。在翻译输入框中输入待翻译的文本,例如中午好,回车执行翻译操作。在这个过程中,可以清晰地观察到“中午好”被翻译为“good afternoon”的过程涉及了三个网络接口请求:

  1. 对 https://dict.youdao.com/webtranslate/key 的GET请求;
  2. 对 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

多次刷新,观察到在两个接口请求的载荷中,变化的参数主要是signmysticTime,其中mysticTime代表的是时间戳,sign`看起来像是经过某种加密算法处理的结果。

为了深入了解sign参数的生成机制,可以采取全局搜索的方式,在浏览器开发者工具中通过快捷键Shift+Ctrl+F打开全局搜索功能,输入sign进行搜索。在搜索结果中,可能会出现大量与sign相关的匹配项,这时需要细心地筛选,寻找与sign生成逻辑相关的代码片段。

通过这种方法,即便面对众多的搜索结果,也能有目的地缩小范围,逐步接近用于生成sign值的加密算法的实现代码。这个过程需要耐心和一定的运气,因为正确的代码片段可能隐藏在大量的匹配结果之中。找到这些关键代码后,就可以进一步分析其逻辑,理解sign是如何根据时间戳mysticTime以及可能的其他因素生成的。

Python爬虫:JS逆向初学习,第1张

当在浏览器的开发者工具中全局搜索sign参数并注意到有key:value的组合形式,如sign: k(o,e),这表明你可能已经找到了生成sign值的关键代码片段。这个k(o,e)很可能是一个函数调用,其中k是一个函数,而oe是传入该函数的参数,这个函数负责生成sign的值。
Python爬虫:JS逆向初学习,第2张

从JS代码中可得,k(o,e)方法里,o参数是时间戳,easdjnjfenknafdfsdfsd,暂时不确定其是否固定。
client=${u}&mysticTime=${e}&product=${d}&key=${t},e是时间戳,ud是常量,tasdjnjfenknafdfsdfsd

  const u = "fanyideskweb"
              , d = "webfanyi"

j函数主要用于进行MD5加密,并将加密结果转换为十六进制(hex)格式。

 function j(e) {
                return c.a.createHash("md5").update(e.toString()).digest("hex")
            }

使用快捷键F8继续执行脚本,发现再次跑到断点这里,又执行了一次k函数,但是key对应的t值发生了变化,此时为fsdsogkndfokasodnaso

Python爬虫:JS逆向初学习,第3张

看起来两个接口的sign值都是通过同一个函数k生成的,但关键在于它们使用key不同。第一次请求时使用的key是asdjnjfenknafdfsdfsd,而第二次请求使用的key是fsdsogkndfokasodnaso,而且这个第二次使用的key是从第一次接口请求的返回数据中获得的。
Python爬虫:JS逆向初学习,第4张

翻译结果返回的是密文,这意味着翻译服务还采用了某种形式的响应加密。这是一个额外的安全措施,用于保护数据在传输过程中的安全,防止未经授权的访问者直接读取响应内容。要解密这些响应,需要了解加密和解密的具体机制。
Z21kD9ZK1ke6ugku2ccWu4n6eLnvoDT0YgGi0y3g-v0B9sYqg8L9D6UERNozYOHqqXyAEo6co8ruGELvtq19adBTgmgtq9XKmTb3RUrbqN9QTNj_RBof8RxaKuaSRS63DlaZVeSgjC6HDrIjQM2yVqVOY1GtO-Re0xcRZML_FmM_6JKN9W6IDSn4K_5-Kfx3SUOxAZ90lJG8iBReRkH8OxCAPaKK2lG6DJlyoHkMHul1MJiAWkni2JX_FiRkypw7KdwvveOaJYsrwRQEIt2GJq8QjqNC8r2oluEzx36x0V20Pdj1HUleZ4uH0-AU8xNW2OmAnLOC7limxtYMKzdwx6GJz0ZqqEmhrmnMw-x1Xz2CFQ4XSJ09L1fsDYsX6uoidgRIq3CWRXIWkBh_9I0EA2D-hhk8m5JYOdLYPY3Pb5ncayIPXGfwFvdkooQYQuO41tfBeOitzdU0cz2z4g6_4A==

有点懵,因为响应结果中并没有关键字,难以使用关键字去搜索。此时蓦然回首,会发现第一个接口请求返回的结果:


Python爬虫:JS逆向初学习,第5张

在此接口请求响应中发现了aesIv和aesKey这样的关键字,这是否暗示了可能使用了AES加密算法呢?在AES加密中,aesKey用作加密和解密的密钥,而aesIv(初始化向量)用于确保即使多次使用相同的密钥加密相同的文本,加密结果也会不同,从而增加了加密数据的安全性。

进行全局搜索aes关键字是一个直接且有效的方法来寻找解密逻辑的实现代码,容易发现:

Python爬虫:JS逆向初学习,第6张

直接双击进入源码,打上断点单步调试:
Python爬虫:JS逆向初学习,第7张
 function y(e) {
                return c.a.createHash("md5").update(e).digest() #进行MD5加密
            }

从上图中,可看出R函数中的t就是翻译结果的加密数据,on就是第一次请求返回的aesKeyaesIv
aes-128-cbc这个加密算法,我也不懂,直接借助于chatgpt,让其给出的解密示例:

const crypto = require('crypto');

// 你的初始化向量 (IV)
const iv = '1234567812345678';

// 你的密钥 (Key)
const key = '1234567812345678';

// 加密后的数据 (CipherText),这里使用的是Base64编码的字符串示例
const cipherText = '加密数据的Base64编码字符串';

// 创建一个解密器实例
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);

// 将加密数据转换为解密后的明文,使用了'base64'作为输入编码,'utf8'作为输出编码
let decrypted = decipher.update(cipherText, 'base64', 'utf8');

// 最后调用final方法完成解密过程,并获取剩余的解密内容
decrypted += decipher.final('utf8');

console.log(decrypted);

仔细对照一下并发现,a Unit8Arry(16)就是密钥,iUnit8Arry(16)是初始化向量。创建有道翻译.js文件,粘贴、改写源码中抠出来的JS代码:

var crypto = require('crypto');//内置模块

// 第一次请求,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=${d}&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)));

    // 创建一个解密器实例
    const decipher = crypto.createDecipheriv('aes-128-cbc', uint8Array_key, uint8Array_iv);

    // 将加密数据转换为解密后的明文,使用了'base64'作为输入编码,'utf8'作为输出编码
    let decrypted = decipher.update(cipherText, 'base64', 'utf8');

    // 最后调用final方法完成解密过程,并获取剩余的解密内容
    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}]翻译的结果是:{translated_text}')
                except Exception as e:
                    print(f"Error decrypting translation: {e}")

if __name__ == '__main__':
    YouDaoTranslate().main('中午好')


执行结果:[中午好]翻译的结果是:good afternoon.
假设有成百上千个文本需要翻译,可使用进程池按此方法进行快速翻译。改天使用chatgpt随机生成100个复杂单词进行验证一下。


https://www.xamrdz.com/backend/3yc1943844.html

相关文章: