【爬虫实战】使用Python和JS逆向拉勾网的XS值

前言

之前的两篇都是分析了webpack技术的网站,今天分析一个特殊webpack的网站——拉勾网。该网站的特点是加载器和功能函数都是在一块的。

一、目标分析

这种招聘网站主要功能就是搜索职位、公司和投简历,所以主要目标如下:

通过关键字搜索到岗位信息。

二、逻辑分析

1. 分析请求

可以看到特殊的接口:

只有三个XS值的字段比较特殊,载荷也是加密的:

响应数据也是密文,所以之后需要解密:

2. 定位加密函数

定位函数一般常用的两种方式,搜索关键字或接口。搜索关键字:

很巧,只有一条记录,并且三个都在一块呢。需要注意这种带逗号的写法,取右边的值。

可以看出这里的T就是重点,但是这个T对象是哪来的呢,肯定离的不远,往前看看有没有?果然就在前面不远的地方,这就是一个webpack的典型写法,下一步就要去找调度器。那么这里的加载器要去找r,断点后需要刷新才可以。

在这里,点进去就看到调度器:

1
2
3
4
5
6
7
8
9
10
 function i(t) {
var e = n[t];
if (void 0 !== e)
return e.exports;
e = n[t] = {
exports: {}
};
return r[t].call(e.exports, e, e.exports, i),
e.exports
}

但是这里的i函数不是一个单独的自执行函数,调用功能的代码和调度器都写在一个文件了。所以,扣代码的时候,就直接把整个文件全部拷走。

3. 补JS

提示ReferenceError: window is not defined

1
window = global;

ReferenceError: XMLHttpRequest is not defined

1
XMLHttpRequest = {};

接下来

1
2
3
4
5
// 最主要的就是i函数
window.xs = i;

T = window.xs(517);
console.log(T.A2('xxxx'))

提示ReferenceError: sessionStorage is not defined,在控制台输入sessionStorage,将获取的对象放入代码中。

提示TypeError: sessionStorage.getItem is not a function,增加个方法:

1
2
3
sessionStorage.getItem = function (key) {
return sessionStorage[key]
}

最后输出:

接下来补X-K-HEADER。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
e = {
"async": true,
"body": "first=true&needAddtionalResult=false&city=%E5%85%A8%E5%9B%BD&fromSearch=true&kd=python&pn=1",
"headers": {
"accept": 'application/json, text/plain, */*',
"content-type": 'application/x-www-form-urlencoded; charset=UTF-8',
"x-anit-forge-token": 'None',
"x-anit-forge-code": '0',
},
"method": "POST",
"password": "undefined",
"url": "https://www.lagou.com/jobs/companyAjax.json",
"user": "undefined",
"withCredentials": true
}
console.log('X-S-HEADER', T.A2(e))
console.log('X-K-HEADER', T.G5())
console.log('X-SS-REQ-HEADER', T.cz())

输出:

1
2
3
X-S-HEADER MaQEHOgLUgyKyipTHUFaqi0WAMaq0+pAXIINKShZb9mALzKIC3T8jU+7lY3aJG/otxW6qvhTRSksDdID1//5zrDwDWTWxFTRS1xHm0aMgZfmhIMA43w5LXt9l1DEd8k8V/LJABIXzOrg9Dw2BjOIoQ==
X-K-HEADER E7HlJlYhdIam2HmJZ/ftSII9yKgPaEQci0kgXNnpDFv2b9WffxNBoNIrZ0Wsa5cZ
X-SS-REQ-HEADER {"secret":"E7HlJlYhdIam2HmJZ/ftSII9yKgPaEQci0kgXNnpDFv2b9WffxNBoNIrZ0Wsa5cZ"}

4. 定位请求体加密函数

虽然能直接通过搜索接口地址的方式找到一条记录,但是这里不是我们想要的,我们需要的是密文,而不是加密前的数据。

所以还是需要用启动器的方式来追踪。

两处都是密文

可以看到之前都是密文,但是之后就没有了,所以关键就出现在倒序最开始密文的地方。

可以这这个断点周围看看,有没有类似data或者code、body的代码,然后打上断点试试。

可以看到case 50这里出现了,关键变量就是w,重点就是前面的代码:

1
2
3
(s = e.body) && (w = (0,
T.q6)(JSON.stringify((0,
A.$Z)("?".concat(s))))

处理成正常格式:

1
w=T.q6(JSON.stringify(A.$Z("?".concat(s))))

5、请求体JS代码

1
2
3
4
5
6
A = window.xs(375)
payload = "first=true&needAddtionalResult=false&city=%E5%85%A8%E5%9B%BD&fromSearch=true&kd=python&pn=1"

w = T.q6(JSON.stringify(A.$Z("?".concat(payload))))
console.log(w)
// 输出 bdyOPAGI8VnLWZIYM6EQYuPt0iE5wOeNuSKWJdG5m+5+AjHqCnCELOkxKVfny4yCQJsGgVDPBepqDSxwZV/ED70hnaZUlGO9vfSP+3JegFv5qVCYKTudDjZxndmPgP0k3M4ajYvxzuB5ix2Yn3/x6w==

三、代码实现

Python部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import json
import time

import requests
import execjs

with open('lagou.js') as f:
js_code = f.read()
js = execjs.compile(js_code)
k, s, ss = js.call("getXS")
print(k, s, ss)

# cookies = {
# 'JSESSIONID': 'ABAAABAABEIABCI29B274903EF00D69D1DC564A62158EE8',
# 'WEBTJ-ID': '20231128100151-18c13a94d01115f-008e604e8aaae2-19525634-2073600-18c13a94d0220c4',
# 'sajssdk_2015_cross_new_user': '1',
# 'sensorsdata2015session': '%7B%7D',
# 'sensorsdata2015jssdkcross': '%7B%22distinct_id%22%3A%2218c13a94dd713c6-01fd442601b775-19525634-2073600-18c13a94dd81ead%22%2C%22first_id%22%3A%22%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%2C%22%24os%22%3A%22MacOS%22%2C%22%24browser%22%3A%22Chrome%22%2C%22%24browser_version%22%3A%22116.0.0.0%22%7D%2C%22%24device_id%22%3A%2218c13a94dd713c6-01fd442601b775-19525634-2073600-18c13a94dd81ead%22%7D',
# }

headers = {
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,la;q=0.7,lv;q=0.6,da;q=0.5,sm;q=0.4',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
# 'Cookie': 'JSESSIONID=ABAAABAABEIABCI29B274903EF00D69D1DC564A62158EE8; WEBTJ-ID=20231128100151-18c13a94d01115f-008e604e8aaae2-19525634-2073600-18c13a94d0220c4; sajssdk_2015_cross_new_user=1; sensorsdata2015session=%7B%7D; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%2218c13a94dd713c6-01fd442601b775-19525634-2073600-18c13a94dd81ead%22%2C%22first_id%22%3A%22%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%2C%22%24os%22%3A%22MacOS%22%2C%22%24browser%22%3A%22Chrome%22%2C%22%24browser_version%22%3A%22116.0.0.0%22%7D%2C%22%24device_id%22%3A%2218c13a94dd713c6-01fd442601b775-19525634-2073600-18c13a94dd81ead%22%7D',
'DNT': '1',
'Origin': 'https://www.lagou.com',
'Pragma': 'no-cache',
'Referer': 'https://www.lagou.com/wn/zhaopin?fromSearch=true&kd=python&pn=1',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36',
'X-K-HEADER': k,
'X-S-HEADER': s,
'X-SS-REQ-HEADER': ss,
'accept': 'application/json, text/plain, */*',
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
'sec-ch-ua': '"Chromium";v="116", "Not)A;Brand";v="24", "Google Chrome";v="116"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"',
# 'traceparent': '00-a818e3305fb35638a7e6e6aa8b1b8d16-94d86c086816904f-01',
'x-anit-forge-code': '0',
'x-anit-forge-token': 'None',
}

payload = "first=true&needAddtionalResult=false&city=%E5%85%A8%E5%9B%BD&fromSearch=true&kd=python&pn=1"
data = js.call("encrypt", payload)
print('payload', data)

# ts = str(time.time()*1000)
response = requests.post('https://www.lagou.com/jobs/positionAjax.json', headers=headers, data=data,
proxies={'http': "http://47.93.16.8:37003"})
print('密文', response.text)
content = response.json()['data']
print(content)

decrypt_data = js.call("Mt", content)
print('解密后数据', decrypt_data)

但是IP受限了,之后再看。