浏览器环境下JavaScript的模拟执行
在前面两节中,我们了解了利用 PyExecJS 和 Nodejs 对 JavaScript 进行模拟执行的方法,但在某些复杂的情况下可能还是有一定的局限性。
分析
比如说:我们在测览器中找到了一个类似的加密算法,其生成逻辑如下:
const token = encrypt(a, b)
我们最终需要获取的就是 token 这个变量究竟是什么。这个 token 模拟出来了,就可以直接拿着去构造请求进行数据爬取了。但这个 token 是由一个 encrypt 方法返回的,参数是 a 和 b。对于参数 a 和 b,我们可能比较容易找到它们是怎么生成的,但是这个 encrypt 方法非常复杂,其内部又关联了许多变量和对象,甚至方法内部的逻辑也进行了混淆等操作,向内追踪非常困难。
这时候如果我们要用 Python 和 Nodejs 来模拟整个调用过程,关键其实就两步:
-
把所有的依赖库都下载到本地;
-
使用 PyExecJS 或 Nodejs 来加载依赖库并模拟调用 encrypt 方法
但在某些情况下可能存在一定的问题,我们分两个方面来进行探讨。
准备工作
本节中,我们使用 playwright 来实现浏览器辅助逆向。首先,安装 playwright,相关命令如下:
pip3 install playwright
playwright install
运行如上两条命令之后,会安装 playwright 库,并安装 Chromjum、Firefox、WebKit 三个内核的浏览器供 playwright 直接使用。具体的安装方法可以参考; https://setup.scrape.center/playwright 。
案例介绍
本节中,我们要分析的目标站点是 https://spa2.scrape.center/ 。可以看到,其 Ajax 请求参数带有一个 token,并且每次都会变化,如图 11-75 所示。
实战
首先,我们来实现 Object(i["a"]) 的全局挂载,只需要将其赋值给 window 对象的一个属性即可,属性名称任意,只要不和现有的属性冲突即可。
比如我们需要在代码:
var a = (this.page-1) * this.limit,e = Object(i["a"])(this.$store.state.url.index,a);
下方添加如下用于挂载全局 window 对象的代码:
window.encrypt = Object(i["a"]);
比如,这里我们将 Object(i["a"]) 挂载给 window 对象的 encrypt 属性。这样只要该行代码执行完毕,我们调用 window.encrypt 方法就相当于调用了 Object["a"] 方法。
接着,我们将修改后的整个 JavaScript 代码文件保存到本地,并将其命名为 chunk.js,如图 11-79 所示。
接下来,我们利用 playwright 启动一个测览器,并使用 Request Interception 将 JavaScript 文件替换,实现如下:
from playwright.sync_api import sync_playwright
BASE_URL = "https://spa2.scrape.center"
context = sync_playwright().start()
browser = context.chromium.launch()
page = browser.new_page()
page.route(
"/js/chunk-10192a00.243cb8b7.js",
lambda route: route.fulfill(path="./chunk.js")
)
page.goto(BASE_URL)
这里首先使用 playwright 创建一个 Chromium 无头测览器,然后利用 new_page 方法创建一个新的页面,并定义了一个关键的路由:
page.route(
"/js/chunk-10192a00.243cb8b7.js",
lambda route: route.fulfill(path="./chunk.js")
)
这里路由的第一个参数是原本加载的文件路径,比如原本加载的 JavaScript 路径为 /j5/chunk-10192a00.243cb8b7.js,如图 11-80 所示。
第二个参数利用 route 的 fulfill 方法指定本地的文件,也就是我们修改后的文件 chunk.js。
这样 playwright 加载 /js/chunk-10192a00.243cb8b7.js 文件的时候,其内容就会被替换为我们本地保存的 chunk.js 文件。当执行之后,Object(i["a"]) 也就被挂载给 window 对象的 encrypt 属性了,所以调用 window.encrypt 方法就相当于调用了 object(i["a"]) 方法了。
怎么模拟调用呢?很简单,只需要在 playwright 环境中额外执行 JavaScript 代码即可,比如可以定义如下的方法:
def get_token(offset):
result = page.evaluate('''() => (
return window.encrypt("%s","%s")
}''' %('/api/movie', offset))
return result
这里我们声明了 get_token 方法,经过上文的分析,模拟执行方法需要传入两个参数,第一个参数是固定值 /api/movie,另一个参数是变值,所以将其当作参数传入。
在模拟执行的过程中,我们直接使用 page 对象的 evaluate 方法,传入 JavaScript 字符串即可,这个 JavaScript 字符串是一个方法,返回的就是 window.encrypt 方法的执行结果。最后将结果赋给 result 变量,并返回。
到此为止,核心代码就说完了。最后,我们只需要完善一下逻辑,将上面的代码串联调用即可。最终整理的代码如下:
from playwright.sync_api import sync_playwright
import time
import requests
BASE_URL = 'https://spa2.scrape.center'
INDEX_URL = BASE_URL + '/api/movie?limit={limit}&offset={offset}&token={token}'
MAX_PAGE = 10
LIMIT = 10
context = sync_playwright().start()
browser = context.chromium.launch(headless=False)
page = browser.new_page()
page.route(
"/js/chunk-10192a00.243cb8b7.js",
lambda route: route.fulfill(path="./chunk.js")
)
page.goto(BASE_URL)
def get_token(offset):
result = page.evaluate('''() => {
return window.encrypt("%s", "%s")
}''' % ('/api/movie', offset))
return result
for i in range(MAX_PAGE):
offset = i * LIMIT
token = get_token(offset)
index_url = INDEX_URL.format(limit=LIMIT, offset=offset, token=token)
response = requests.get(index_url)
print('response', response.json())
time.sleep(10)
这里我们遍历了 10 页,然后构造了 offset 变量,传给 get_token 方法获取 token 即可,最终运行结果如下:
可以看到,每一页的数据就被成功爬取到了,简单方便。