python爬虫从0到1(第十三天)——本地js调试与参数生成

通过上一篇文章之后相信大家对于web js逆向已经有了一个初步的认识,可能都认为逆向也不过如此嘛,如果有这种想法那就大错特错!要知道在上篇文章案例中的网站是使用md5加密算法来对参数进行加密,所以要改写成Python的话还是非常容易的,那如果别人网站使用的是自己实现的算法或者魔改后的常规算法呢?并且这还算好的,至少还能看,如果遇到混淆的话,那代码简直是不堪入目。所以,这种情况下,与其改写代码,不如直接让其算法在我们的本地运行,通过Python获取到本地运行的结果来实现参数的生成,接下来我们就正式进入本篇文章的内容。

一、环境配置

本地要想执行js代码的话,在本地就必须存在着执行js代码的环境,就像Python代码要想执行的话环境中就需要存在python解释器一样。本篇文章中以windows系统为例。

1.1 Node.js下载

下载地址:https://nodejs.org/en/download

在这里插入图片描述

此处直接下载的是18.16.0的版本,如果需要其他版本则在当前页面向下翻动后点击自行选择Previous Releases

1.2 node.js安装

下载完成之后,直接双击打开安装包。

在这里插入图片描述

看到欢迎界面后直接点击next。然后进入到协议页面选择接受然后点击next。如下图。

在这里插入图片描述

然后来到安装目录选择页面,在这里可以自定义安装目录,但是切忌,目录中最好不要存在中文字符包括一些特殊符号也不要存在。

在这里插入图片描述

安装目录选择之后一直点击next直到看到install下载按钮。

在这里插入图片描述

点击install,等待安装结束。安装结束后直接点击finish即可。

接着打开终端输入node -v、npm -v`测试安装是否成功。

在这里插入图片描述

如此便证明安装是成功了。接下来便是在pycharm中配置js的执行环境了,当然,也可以使用文本然后更改扩展为js,例如下方输出hello world的代码。

在这里插入图片描述

代码编辑完成之后另存为js文件。这里一定要注意文件扩展名不要写错。

在这里插入图片描述

然后在终端找到文件所在目录,使用node 文件名命令执行。

在这里插入图片描述

但是呢很明显的可以感受到这样的话非常不方便,所以最好还是有相应的编辑器,直接使用jet全家桶之一的WebStorm或者用VSCode都可以,看自己喜欢使用什么,当然直接用pycharm也行,但是pycharm的话必须是专业版的pycharm,社区版是无法安装nodejs插件的,接下来我们就来看pycharm中配置js执行环境的过程。

1.3 pycharm配置js执行环境

首先打开pycharm设置

在这里插入图片描述

选择plugins,然后选中marketplace,再在搜索框输入nodejs

在这里插入图片描述

此处我的是已经下载好了,没有下载过的话是绿色的可点击样式,直接点击下载,在下载的过程中先去配置其他选项。

选择Languages & Frameworks然后选择Node.js

在这里插入图片描述

在右侧配置nodejs与其包管理工具npm的路径,当然pycharm版本较新并且nodejs的环境变量配置无误的话此处会自动识别,如果没有自动识别出来的话只需要手动配置即可,注意查看图中的路径。

到此,pycharm中的配置结束,等待插件下载安装结束之后重新打开pycharm便可以直接在pycharm中编写并执行js代码了。注意创建代码文件的时候是创建JavaScript文件,不要创建成py文件了。

在这里插入图片描述

二、pyexecjs

python要执行js代码的话那么必须要有python执行js的依赖,也就是说需要安装第三方包。而类似的第三方包其实有很多,比较常用的就有js2pypyexecjs等,前者适用于js代码较少的情况,也就意味着如果遇到了很长的混淆代码的话,转译就可能出现报错,因此更多的还是选择pyexecjs进行使用。

2.1 安装

通过包管理工具进行安装:pip install pyexecjs

2.2 使用方法

内容较简单且为固定用法,不再截图展示。提供代码如下。

import execjs   # 导入execjs模块
from execjs.runtime_names import Node   # 导入NodeJS环境

ndjs = execjs.get(Node)     # 修改为NodeJS执行环境,除了Nodejs之外还有其他的环境也能够执行js代码,自行了解即可
print(ndjs.name)    # 输出结果为Node.js (V8)

# 方式1,使用eval方法直接执行
r1 = ndjs.eval("'learning net spider by quanmou!'.split(' ')")
print(r1)   # ['learning', 'net', 'spider', 'by', 'quanmou!']

# 方式2,先加载再执行
# 同级目录下新建一个js文件,名为prac.js,内容为function add(a, b) {
#     return a+b
# }
jscode = open('prac.js', encoding='utf-8').read()   # 读取出js文件中的js代码
js_compile = ndjs.compile(jscode)   # 加载器先加载js代码,注意此时代码并没有执行
a = 1
b = 2
r2 = js_compile.call('add', a, b)      # 通过加载器call方法调用已加载的js代码中的函数,在call方法中第一个参数为要调用的函数的函数名,
# 后面的参数为调用函数是需要传递的参数,注意参数要一一对应
print(r2)   # 输出3

三、百度翻译案例

接下来我们以百度翻译为例进行简单实战。老规矩进入到百度翻译页面后直接打开开发者工具进行分析。

在这里插入图片描述

毫无疑问是异步加载的,所以直接定位到XHR进行抓包。在输入框输入需要翻译的内容就能抓到翻译时请求的接口。

在这里插入图片描述

定位到包后打开查看详情,直接预览,在trans_result节点中明显看到了翻译的结果,所以直接就从这个包入手就OK。

在这里插入图片描述

到这里,数据包已经找到了,那么接下来只要去请求这个包所在的URL就能够拿到对应的响应,其中就会包含我们需要的翻译结果。

那么现在来实现第一部分的代码。

import requests

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36',
    'Acs-Token': '替换为自己的token',
    'Cookie': '替换为自己的Cookie'
}

url = 'https://fanyi.baidu.com/v2transapi?from=zh&to=en'

data = {
    "from": "zh",
    "to": "en",
    "query": "美女",  # 需要翻译的内容
    "transtype": "realtime",
    "simple_means_flag": "3",
    "sign": "551517.821612",
    "token": "替换为自己的token",
    "domain": "common"
}   # 构建表单数据

response = requests.post(url, headers=headers, data=data)
print(response.json())

注意:该接口存在token认证机制以及cookie认证,所以代码中需要在请求头添加token和cookie参数值。在浏览器中查看翻译接口返回的数据包的详情信息中,在请求头里面直接复制即可。

在这里插入图片描述

此时输出结果如下

在这里插入图片描述

但是,当我们修改表单中query的值后,便会发现无法获取到翻译结果了。

在这里插入图片描述

那么具体原因是什么呢?回到浏览器,我们翻译帅哥然后抓包看一下有什么不同的地方呢?

在这里插入图片描述

对比美女传输的表单数据来看的话,翻译帅哥传书的表单数据的sign值是不同于美女的。

那是是否就是这个值影响了我们翻译的结果呢,直接来测试一下,将代码中的sign值修改为此处看到的sign值。然后执行代码看结果。

在这里插入图片描述

的确,结果已经出来了,至此,我们就知道了在token与cookie不变的情况下,由query和sign控制我们要翻译的内容与结果,所以接下来只需要找到sign参数的生成位置即可。sign是表单数据,所以可以通过搜索源代码的方式来定位,快捷键为:ctrl+shift+f。

在这里插入图片描述

为了避免出现过多的无效搜索结果,我们可以根据参数的格式添加特殊符号,以减少搜索到的无效结果的数量,例如此处我们就可以添加冒号来实现。

在这里插入图片描述

搜索到三个包,挨个进行查看。此处第一个就是目标。点击即可进入到源代码之中。

在这里插入图片描述

进入之后在该js文件中继续搜索signde 位置

在这里插入图片描述

可以看到一共有7个结果,不熟练的情况下我们就挨个打上断点然后重新抓包,当代码被断下则证明断点处于我们的sign有关。

在这里插入图片描述

定位到第四个sign时重新翻译的时候代码执行在此处停下,那么也就意味着此处就是我们需要的sign的内容。很明显的可以看到,sign调用b方法返回的结果,传入的参数e就是我们要翻译的内容,那么接下来我们进入到b方法看一下这个参数是如何生成的呢。

在这里插入图片描述

定位b方法过来之后,我们可以知道b方法就是图中箭头所指的函数,阅读这个函数的代码可以发现,改写这个代码的话难度还是有的,所以完全没有必要对其进行改写,直接将这整个函数复制到本地的js文件中执行。函数名不好看的话我们可以稍微改写一下这个函数的格式。如下:

function inno(t) {
    var o, i = t.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);
    if (null === i) {
        var a = t.length;
        a > 30 && (t = "".concat(t.substr(0, 10)).concat(t.substr(Math.floor(a / 2) - 5, 10)).concat(t.substr(-10, 10)))
    } else {
        for (var s = t.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/), c = 0, l = s.length, u = []; c < l; c++)
            "" !== s[c] && u.push.apply(u, function (t) {
                if (Array.isArray(t))
                    return e(t)
            }(o = s[c].split("")) || function (t) {
                if ("undefined" != typeof Symbol && null != t[Symbol.iterator] || null != t["@@iterator"])
                    return Array.from(t)
            }(o) || function (t, n) {
                if (t) {
                    if ("string" == typeof t)
                        return e(t, n);
                    var r = Object.prototype.toString.call(t).slice(8, -1);
                    return "Object" === r && t.constructor && (r = t.constructor.name),
                        "Map" === r || "Set" === r ? Array.from(t) : "Arguments" === r || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r) ? e(t, n) : void 0
                }
            }(o) || function () {
                throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")
            }()),
            c !== l - 1 && u.push(i[c]);
        var p = u.length;
        p > 30 && (t = u.slice(0, 10).join("") + u.slice(Math.floor(p / 2) - 5, Math.floor(p / 2) + 5).join("") + u.slice(-10).join(""))
    }
    for (var d = "".concat(String.fromCharCode(103)).concat(String.fromCharCode(116)).concat(String.fromCharCode(107)), h = (null !== r ? r : (r = window[d] || "") || "").split("."), f = Number(h[0]) || 0, m = Number(h[1]) || 0, g = [], y = 0, v = 0; v < t.length; v++) {
        var _ = t.charCodeAt(v);
        _ < 128 ? g[y++] = _ : (_ < 2048 ? g[y++] = _ >> 6 | 192 : (55296 == (64512 & _) && v + 1 < t.length && 56320 == (64512 & t.charCodeAt(v + 1)) ? (_ = 65536 + ((1023 & _) << 10) + (1023 & t.charCodeAt(++v)),
            g[y++] = _ >> 18 | 240,
            g[y++] = _ >> 12 & 63 | 128) : g[y++] = _ >> 12 | 224,
            g[y++] = _ >> 6 & 63 | 128),
            g[y++] = 63 & _ | 128)
    }
    for (var b = f, w = "".concat(String.fromCharCode(43)).concat(String.fromCharCode(45)).concat(String.fromCharCode(97)) + "".concat(String.fromCharCode(94)).concat(String.fromCharCode(43)).concat(String.fromCharCode(54)), k = "".concat(String.fromCharCode(43)).concat(String.fromCharCode(45)).concat(String.fromCharCode(51)) + "".concat(String.fromCharCode(94)).concat(String.fromCharCode(43)).concat(String.fromCharCode(98)) + "".concat(String.fromCharCode(43)).concat(String.fromCharCode(45)).concat(String.fromCharCode(102)), x = 0; x < g.length; x++)
        b = n(b += g[x], w);
    return b = n(b, k),
    (b ^= m) < 0 && (b = 2147483648 + (2147483647 & b)),
        "".concat((b %= 1e6).toString(), ".").concat(b ^ f)
}

将函数命名为inno,接下来就是直接调用inno函数传入翻译内容然后执行代码查看结果。

在这里插入图片描述

报错r没有定义,那么就近一步去查找r的位置,结合代码中报的未定义的r的位置来到浏览器中对应位置打上断点

在这里插入图片描述

然后执行代码到下一个断点,便可以看到代码断在了此处,那么此时就可以查看r的具体情况。

在这里插入图片描述

将鼠标放在r上可以看到r的具体值如上所示,同时,在函数邻近上方可以看到定义的r此时的值也为上图所示情况。

在这里插入图片描述

而对于r有关的赋值的话仅存在一个r=window[d],所以能够直接猜测出r的值来自于window对象,那么我们就来看一下这个值是如何生成的。

将页面左右滚动直到r=window[d]打上断点然后执行点击执行到下一个断点处

在这里插入图片描述

此时将鼠标放在d上,可以看到d的值是一个字符串gtk,那么至此我们就知道了,r的值就是window对象的gtk节点对应的值或者方法调用的返回结果,具体什么情况我们再进一步观察。

在console(控制台)输入window然后回车查看window对象,找到gtk节点对应的位置。

在这里插入图片描述

很明显了,是一个固定值,那么也就是说r其实也是一个固定值,所以回到我们扣下来的js代码中,定义一个r并且赋值即可。执行代码如下:

在这里插入图片描述

可以看到此时已经没有报r没有定义了,但是报出了n没有定义,n很明显是一个方法,那么我们以同样的方式去找到这个n方法的位置将整个n方法扣下来放到代码中运行即可。

在这里插入图片描述

在这里插入图片描述

将n函数粘贴到本地js后执行结果如下:

在这里插入图片描述

至此,js代码就全部搞定,该结果与浏览器中翻译美女时的sign也完全相同。

那么接下来只需要在python中调用js中的inno函数就可以获取拿到这个参数然后携带着去进行请求了。

代码如下:

from execjs.runtime_names import Node
...

ndjs = execjs.get(Node)
jscode = open('baidutrans.js', encoding='utf-8').read()
word = input('请输入您要翻译的内容:')
sign = ndjs.compile(jscode).call('inno', word)
data = {
    "from": "zh",
    "to": "en",
    "query": word,  # 需要翻译的内容
    "transtype": "realtime",
    "simple_means_flag": "3",
    "sign": sign,
    "token": "替换为自己的token",
    "domain": "common"
}   # 构建表单数据

response = requests.post(url, headers=headers, data=data)   # 注意请求方式是post请求,所以不能使用get方法进行请求发送,data为表单数据
print(response.json())

执行结果:

在这里插入图片描述

至此,本案例告一段落,同时对于pyexecjs也有了初步的认知。后面为完整的py代码与js代码。

四、完整代码

4.1 完整JS代码
function n(t, e) {
            for (var n = 0; n < e.length - 2; n += 3) {
                var r = e.charAt(n + 2);
                r = "a" <= r ? r.charCodeAt(0) - 87 : Number(r),
                r = "+" === e.charAt(n + 1) ? t >>> r : t << r,
                t = "+" === e.charAt(n) ? t + r & 4294967295 : t ^ r
            }
            return t
        }
var r = '320305.131321201'
function inno(t) {
    var o, i = t.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);
    if (null === i) {
        var a = t.length;
        a > 30 && (t = "".concat(t.substr(0, 10)).concat(t.substr(Math.floor(a / 2) - 5, 10)).concat(t.substr(-10, 10)))
    } else {
        for (var s = t.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/), c = 0, l = s.length, u = []; c < l; c++)
            "" !== s[c] && u.push.apply(u, function (t) {
                if (Array.isArray(t))
                    return e(t)
            }(o = s[c].split("")) || function (t) {
                if ("undefined" != typeof Symbol && null != t[Symbol.iterator] || null != t["@@iterator"])
                    return Array.from(t)
            }(o) || function (t, n) {
                if (t) {
                    if ("string" == typeof t)
                        return e(t, n);
                    var r = Object.prototype.toString.call(t).slice(8, -1);
                    return "Object" === r && t.constructor && (r = t.constructor.name),
                        "Map" === r || "Set" === r ? Array.from(t) : "Arguments" === r || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r) ? e(t, n) : void 0
                }
            }(o) || function () {
                throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")
            }()),
            c !== l - 1 && u.push(i[c]);
        var p = u.length;
        p > 30 && (t = u.slice(0, 10).join("") + u.slice(Math.floor(p / 2) - 5, Math.floor(p / 2) + 5).join("") + u.slice(-10).join(""))
    }
    for (var d = "".concat(String.fromCharCode(103)).concat(String.fromCharCode(116)).concat(String.fromCharCode(107)), h = (null !== r ? r : (r = window[d] || "") || "").split("."), f = Number(h[0]) || 0, m = Number(h[1]) || 0, g = [], y = 0, v = 0; v < t.length; v++) {
        var _ = t.charCodeAt(v);
        _ < 128 ? g[y++] = _ : (_ < 2048 ? g[y++] = _ >> 6 | 192 : (55296 == (64512 & _) && v + 1 < t.length && 56320 == (64512 & t.charCodeAt(v + 1)) ? (_ = 65536 + ((1023 & _) << 10) + (1023 & t.charCodeAt(++v)),
            g[y++] = _ >> 18 | 240,
            g[y++] = _ >> 12 & 63 | 128) : g[y++] = _ >> 12 | 224,
            g[y++] = _ >> 6 & 63 | 128),
            g[y++] = 63 & _ | 128)
    }
    for (var b = f, w = "".concat(String.fromCharCode(43)).concat(String.fromCharCode(45)).concat(String.fromCharCode(97)) + "".concat(String.fromCharCode(94)).concat(String.fromCharCode(43)).concat(String.fromCharCode(54)), k = "".concat(String.fromCharCode(43)).concat(String.fromCharCode(45)).concat(String.fromCharCode(51)) + "".concat(String.fromCharCode(94)).concat(String.fromCharCode(43)).concat(String.fromCharCode(98)) + "".concat(String.fromCharCode(43)).concat(String.fromCharCode(45)).concat(String.fromCharCode(102)), x = 0; x < g.length; x++)
        b = n(b += g[x], w);
    return b = n(b, k),
    (b ^= m) < 0 && (b = 2147483648 + (2147483647 & b)),
        "".concat((b %= 1e6).toString(), ".").concat(b ^ f)
}

console.log(inno('美女'))
4.2 完整python代码
import requests
import execjs
from execjs.runtime_names import Node

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36',
    'Acs-Token': '替换为自己的token',
    'Cookie': '替换为自己的Cookie'
}

url = 'https://fanyi.baidu.com/v2transapi?from=zh&to=en'

ndjs = execjs.get(Node)
jscode = open('baidutrans.js', encoding='utf-8').read()
word = input('请输入您要翻译的内容:')
sign = ndjs.compile(jscode).call('inno', word)
data = {
    "from": "zh",
    "to": "en",
    "query": word,  # 需要翻译的内容
    "transtype": "realtime",
    "simple_means_flag": "3",
    "sign": sign,
    "token": "替换为自己的token",
    "domain": "common"
}   # 构建表单数据

response = requests.post(url, headers=headers, data=data)   # 注意请求方式是post请求,所以不能使用get方法进行请求发送,data为表单数据
print(response.json())
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

quanmoupy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值