一.写在前面
前面我们已经了解了一些JS逆向的知识,现在我们就来将其付诸于实践。以XX为例,批量得到某铁道(游戏)的视频。其实我们在之前已经尝试过使用selenium来获取X音上的yuan神(游戏)的视频,并且成功了,但是,这样的弊端很大,因为速度非常慢,而且各种验证码包括滑块点选之类的甩你脸上,所以效果不是很好,而这次我们尝试JS逆向它的有加密的参数,这样就能够非常快的爬取全部视频。下面的一切内容仅供学习交流。
二.分析网络请求
我们来到XXXX用户主页,打开浏览器开发者工具。
刷新一下页面,我们就能够看到很多网络请求。首先我们应该明确一点,现在的很多网页都不是一下子把所有的内容给你加载出来的,而是随着用户操作才发送网络请求(Ajax请求或者其他一些请求),然后再渲染给你看到,而这些请求往往可以在浏览器开发工具的Fetch/XHR面板下看到,我们切换到这个面板下,查看这些请求,就能够看到一个非常可疑的请求:
点开预览我们就能够看到很多似乎是视频描述的文本,然后继续往里面寻找,打开一个aweme_id,就能够发现里面有一些URL,点开,我们就能够看到,非常幸运,就是视频的URL。
我们回到标头,查看该URL的特点,然后在筛选器里面筛选这种类型的网络请求。
每个请求里面都包含了大概十多个视频的信息,随着我们鼠标往下滑,我们就能看到更多这样的请求被拦截。所以我们有理由这样设想:假如我们能够构造这些请求,不就能够获取全部视频的地址了么?进而就可以全部爬取了。
三.加密参数分析
于是我们点开一个请求,看看它都携带了哪些查询字符串。我们重点关注的是那些变化的参数,不变的参数对我们没有太大意义。经过我们的观察,看起来像是加密的参数的只有这两个:msToken和一个XB参数
这个msToken参数经过我们的一番寻找,原来他就是cookie里面的一个值,
所以这个参数我们就可以暂且不管了,重点要看看这个XB参数,我们没有找到它是哪里冒出来的,所以我们有理由相信,它应该是在JS代码运行过程中被加密了。回到这个请求的标头,我们复制一小段URL的内容,到源代码上打上一个XHR断点。
然后刷新页面,我们就能够看到代码被卡住了,说明有这样的URL被发送。
我们将鼠标放到这个this上,或者是在控制台页面上面打印。
我们发现里面好像确实是包含了一个URL,而且带有XB参数(太长了,截屏没有截全)。所以我们猜想XB参数在之前就被加密装好了。所以我们把目光集中到旁边的调用堆栈,往上跟栈。来到了一个被混淆的函数。
这里的代码全部是看不懂的十六进制,但是我们不用管,既然跟栈到了这里来,说明确实是有经过这一步的,所以我们在这里打上一个断点,并把之前那个XHR断点取消掉,再次刷新页面。页面再次进入调试状态。
我们观察这行代码,它似乎是把什么值赋给了左边那一坨,所以我们不妨在控制台打印一下右边那个函数到底返回了什么,以及它的参数。
我们看到这个函数返回了一个很像URL的东西,函数的两个参数一个是空,一个有点东西。我们仔细观察这个URL,发现它和我们之前那个URL不太一样,这个是/aweme/v1/web/app/installed,而我们之前那个URL应该是/aweme/v1/web/aweme/post/的。到这里,我们心里面不禁起了疑问:明明我们跟栈到了上一步,为什么这个玩意儿的好像跟我们预想的不太一样?于是,我们心里有了一个想法,难道这个函数(右边那一坨)被执行了很多次,而且每次执行的结果不一定相同?所以我们尝试在这里打上一个日志断点,再次刷新页面。
来到控制台,我们发现,果然这个函数被执行了很多次,而且每次的结果都不太一样。
我们怀着侥幸的心理,在筛选器里面搜索看看有没有加密参数XB的信息,因为在这一步,XB参数很可能也已经被加密完成了的。
我们发现居然还真有,而且有的就是单独一个XB参数。这样我们就大概理清了:原来每次这个方法都会被执行很多次,每次的结果都不一样,而且有时候它的返回值就是一个XB加密参数。
那么XB参数究竟是在哪里被加密的呢?既然日志断点能够打印出XB值,那么我们完全有能力让其在是XB值的时候断住,然后再往上跟栈,找到加密的地方。没错,我们试着打上一个条件断点,而XB值的一个很大的特点就是长度是28,所以我们就以这个为条件。
再次刷新页面,进入到了调试模式,说明我们捕获到了XB值,我们再控制台上打印,发现确实是XB值。
这下就非常好了。我们把鼠标移动到这个函数上面,看看它到底是在哪里定义的:
于是我们就来到了它定义的地方。
那么它会是加密XB参数的地方么?我们不妨打上断点,再次刷新试试,在控制台上打印出这个函数的返回值。
非常完美,我们终于找到了XB加密的地方,就是这个函数。而且我们也能够知道它的两个参数是什么东西了。
四.模拟执行JS代码
既然找到了JS代码里面加密XB的地方,那么我们该怎么将其为我们所用呢?难道要去读懂里面的逻辑么?肯定不是,我们只要用它就行了。首先我们肯定要一个能够运行JS代码的地方,这里我就选择了VSCODE,并安装了node.js环境。最开始我尝试只把这个加密的函数抠出来,然后尝试运行,发现缺了很多东西,而且补了很久加了很多东西都还是报错缺东西。所以后来我干脆直接把这一整个JS代码复制下来,单独保存为一个JS文件,然后只补了几个东西,就没有报错了。
window=global
document={}
document.addEventListener=function(){}
// 创建一个新的 Navigator 对象
const customNavigator = {
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0'
};
// 将新的 Navigator 对象赋值给 window.navigator
if (typeof window !== 'undefined') {
Object.defineProperty(window, 'navigator', {
value: customNavigator,
configurable: true,
enumerable: true,
writable: false // 这里设为 false,以防止后续再次尝试修改
});
} else {
console.error("Window object is not defined. This code may not work in non-browser environments.");
}
上面的代码是写在整个JS文件的最上头的,补的是window和UA还有document,因为我不懂JS代码,所以有些是问的GPT,然后又到处百度改的(能用就行)。
这个圈起来的代码意思是把这个加密XB的关键函数赋给window,这样我们就能在外面调用了。
let key1='device_platform=webapp&aid=6383&channel=channel_pc_web&sec_user_id=MS4wLjABAAAAsWXxS-KIWEuFJf2oy9E5RwDFzqIElxMsYccLGHCTh-J7z4WhTCehpsQwkv0n_8pl&max_cursor=1707386400000&locate_query=false&show_live_replay_strategy=1&need_time_list=0&time_list_query=0&whale_cut_token=&cut_version=1&count=18&publish_video_strategy_type=2&pc_client_type=1&version_code=170400&version_name=17.4.0&cookie_enabled=true&screen_width=1536&screen_height=864&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=121.0.0.0&browser_online=true&engine_name=Blink&engine_version=121.0.0.0&os_name=Windows&os_version=10&cpu_core_num=8&device_memory=8&platform=PC&downlink=6.6&effective_type=4g&round_trip_time=150&webid=7214703537787274789&msToken=TH_OPzj7n4OKMM8fqvvJRTCyfjziCOGnsex9yTR0HbFsqUeG6RPrRFM-V6r5dFVzSbqQP88GjZHNgAFDbGqqcru8rxOc07-1R9uvhsz9kYJuSrnaMEnazZg='
let key2=null
console.log(window.encodeXB(key1,key2))
我们传入两个参数,试着调用一下,打印结果。
JS代码成功打印出了加密后的XB值。
五.建立服务,与python代码联动
现在又出现了一个新的问题,虽然我们能成功计算出XB值,但是我们要写爬虫啊,JS代码我可不会,怎么办呢?其实有这样两个解决办法,第一种就是利用python的一个执行JS代码的库,这样我们就可以利用python来调用这些JS代码,第二种方法就是建立一个简单的JS服务端,就像你发送requests请求到别人的服务器,然后返回响应一样,这里我们使用第二种办法。用JS代码建立一个服务端,接受一个post请求,返回我们计算的值。
const express = require('express');
const app = express();
const port = 3000; // 选择一个你喜欢的端口
// 使用body-parser中间件来解析POST请求的请求体
app.use(express.json());
// 处理POST请求
app.post('/', (req, res) => {
// 从请求体中获取数据
const requestData = req.body;
// 检查 requestData 是否为 JSON 数据
if (typeof requestData === 'object' && requestData !== null) {
// 获取 requestData 的所有键
const keys = Object.keys(requestData);
// 检查是否有至少一个键
if (keys.length > 0) {
// 获取第一个键
const firstKey = keys[0];
// 获取第一个键对应的值
const firstValue = requestData[firstKey];
//console.log(`First Key: ${firstKey}, First Value: ${firstValue}`);
// 在这里可以进行任何你想要的处理
const responseData = window.encodeXB(firstValue,null);
res.send(responseData);
} else {
console.error('Empty JSON data in the request body');
}
} else {
console.error('Invalid JSON data in the request body');
}
});
// 启动服务器
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
这个服务端代码是和我们加密XB参数的代码放在一起的,我们运行服务端代码,它就会监听一个端口,这样当我们携带一个post的http请求去访问的时候,服务端就会解析我们的请求体的数据,然后调用加密XB参数的方法,并返回给我们值。我们先在VSCODE里面运行服务端的代码,可以看到成功运行了。
然后呢,我们就可以在pycharm里面编写我们的爬虫代码了。
import requests
header = {...}
def getXB(key):
"""
这个函数用来发送一个http请求向服务端获取对应的XB值,注意需要先启动VSCODE里面的服务端
:param key: 加密用的字符串,需要除了XB值的前面的查询字符串
:return: 返回XB字符串
"""
data={
'key':key
}
response=requests.post(url="http://localhost:3000",json=data)
return response.text
def getData(data):
"""
这个函数传入一段字符串,然后构造url请求,并返回内容。注意传入的字符串是从?后面开始到XB参数之前,XB之前那个&不要加
:param data:
:return:
"""
baseurl="https://www.douyin.com/aweme/v1/web/aweme/post/?"
#data="device_platform=webapp&aid=6383&channel=channel_pc_web&sec_user_id=MS4wLjABAAAAsWXxS-KIWEuFJf2oy9E5RwDFzqIElxMsYccLGHCTh-J7z4WhTCehpsQwkv0n_8pl&max_cursor=0&locate_query=false&show_live_replay_strategy=1&need_time_list=1&time_list_query=0&whale_cut_token=&cut_version=1&count=18&publish_video_strategy_type=2&pc_client_type=1&version_code=170400&version_name=17.4.0&cookie_enabled=true&screen_width=1536&screen_height=864&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=121.0.0.0&browser_online=true&engine_name=Blink&engine_version=121.0.0.0&os_name=Windows&os_version=10&cpu_core_num=8&device_memory=8&platform=PC&downlink=10&effective_type=4g&round_trip_time=150&webid=7214703537787274789&msToken=VFu2Zu4uX3Ej6Gzcs4moEbx_B-vRZudzOgZXbQOce7zmotHxJhDw8VWwRhlccIeoSoZ9zJ__kEIbsnAmJx4fkY246GZS8e07ls1L6dem9oOTBMrq_qakzsgB0BikFAYI"
XB=getXB(data)
url=baseurl+data+'&X-Bogus='+XB
#print(url)
response=requests.get(url=url,headers=header)
return response.text
if __name__=="__main__":
data="device_platform=webapp&aid=6383&channel=channel_pc_web&sec_user_id=MS4wLjABAAAAsWXxS-KIWEuFJf2oy9E5RwDFzqIElxMsYccLGHCTh-J7z4WhTCehpsQwkv0n_8pl&max_cursor=1707537600000&locate_query=false&show_live_replay_strategy=1&need_time_list=0&time_list_query=0&whale_cut_token=&cut_version=1&count=18&publish_video_strategy_type=2&pc_client_type=1&version_code=170400&version_name=17.4.0&cookie_enabled=true&screen_width=1536&screen_height=864&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=121.0.0.0&browser_online=true&engine_name=Blink&engine_version=121.0.0.0&os_name=Windows&os_version=10&cpu_core_num=8&device_memory=8&platform=PC&downlink=10&effective_type=4g&round_trip_time=200&webid=7214703537787274789&msToken=mrmatvU-fZ10rREVP9M106uwYtZoFeuUhylNVqN7gzGGRUwoXECAh_ip05nF4gKJLaeluP9HfEJzRCrUu0AJVhSpXf13kQ-TPoqsWpjp7LLLP5JmQH8UG1-GO8qA019B"
print(getData(data))
然后我们尝试运行一下:
可以看到,pycharm与VSCODE成功联系了,而且返回了预期的数据。至于后面你是要用多线程去爬取所有视频还是异步爬虫,那都不是很重要了。