思路:
1. 抓包
- 作用:找到接口,参数
- 方法:(1)搜索方式快速定位到数据接口(2)接口数量少,选择最大的(3)接口数量多
2. 调试
- 作用:调试抓包获取到的链接、数据、找到想要的明文数据、密文数据以及加密方法
- 方法:(1)关键字搜索(2)xhr断点(3)路径搜索(4)引入器引导
3. 扣取js
4. 改写
作用:对扣取到的js进行改写
5.本地运行出值
调试js方法以及例子
方法一:关键字搜索
关键字:
问题:当使用关键字搜索,出现多个js,我们无法确定数据接口时,可以使用交互式搜索(搜索路径)
案例二:https://www.yuque.com/u1046159/erg6ec/on17rw#dUpIP
方法二:xhr断点
使用场景:一般填充的是路径
方法三:路径搜索
路径是域名后面的路径,如上述接口地址:
[http://www.whggzy.com/front/search/category](http://www.whggzy.com/front/search/category)
,那么路径是:front/search/category
请求头参数
requests请求步骤
案例一:搜索方式快速定位到接口
网站:http://www.whggzy.com/PoliciesAndRegulations/index.html?utm=sites_group_front.4dd516b0.0.0.0aeeb380992811ec85e0b5fac8c3351d
抓包
- 浏览器使用F12,爬虫主要使用以下四个:
- 元素:不是源代码页面、不是HTML页面、不是js页面,而是渲染后的页面,也叫元素页面
- 控制台:交互页面
- 源代码:包含静态资源文件
- 网络:(经常会用,抓包工具)截获浏览器与服务器数据交互
如何寻找数据接口
方式一:搜索方式快速定位到接口此方式有两个弊端:
- 当数据加密时,数据接口获取不到
- 页面没有加载
- 比如我们需要乌海市财政局
复制要寻找的数据,然后在F12窗口,按下Ctrl + F搜索
- 可以发现该接口是post请求,对于post请求,我们需要传入请求头headers和请求体
我们从上面得到了浏览器显示的请求头和请求体:
如果我们此时将请求头和请求体组装成json,使用requests,会发现报错:
import requests
url = 'http://www.whggzy.com/front/search/category'
headers = {
'Accept': "*/*",
'Content-Type': "application/json",
'X-Requested-With': "XMLHttpRequest",
# 请求头最基本的两个
'Referer': 'http://www.whggzy.com/PoliciesAndRegulations/index.html?utm=sites_group_front.4dd516b0.0.0.0aeeb380992811ec85e0b5fac8c3351d',
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36',
}
# 表单内容,在F12的"载荷"里面,点击查看源代码,整理成JSON格式
data = {"utm": "sites_group_front.4dd516b0.0.0.0aeeb380992811ec85e0b5fac8c3351d","categoryCode": "GovernmentProcurement","pageSize": 15,"pageNo": 1}
response = requests.post(url,headers=headers, data=data).text
print(response) # 系统出错,请稍后重试
问题:
- 出现这样的问题是什么原因呢?是因为请求头和请求体并没有被js验证正确,也就是请求头或者请求体格式和数据不正确
- 对于post方法的请求头headers和请求体data会被前端验证——js验证(一般不会从服务器端验证,容易造成服务器压力或者出现问题),
那我们需要通过js来验证是否是headers的问题还是data的问题,那就需要进行js的调试(对于这个问题我们使用路径搜索+xhr断点组合方式)
调试js:路径搜索+xhr断点组合方式
如上述接口地址:
[http://www.whggzy.com/front/search/category](http://www.whggzy.com/front/search/category)
,那么路径是:front/search/category
我们在源代码一栏,找到XHR/提取断点,将上述路劲添加到XHR/提取断点下面
- 然后刷新页面,会出现以下内容,我们需要关注XHR断点调试出现的作用域一栏
- 现在我们做的是接口验证,需要2步骤中的作用域出现我们的路径front/search/category,上图发现作用域里面没有出现,如果没有出现,我们需要进行XHR调试:
当作用域中出现路径时,我们可以找到对应的requestHeaders(请求头)以及请求体中的data
- 我们将上面找到的请求头和请求体传入Python的requests方法中(注意:请求头组装成JSON格式,而请求体data是一个string),如果不容易看,我们可以在控制台中输入options以及requestHeaders
发现接口已经能请求到数据
# 网址:http://www.whggzy.com/PoliciesAndRegulations/index.html?utm=sites_group_front.4dd516b0.0.0.0aeeb380992811ec85e0b5fac8c3351d
import requests
url = 'http://www.whggzy.com/front/search/category'
headers = {
'Accept': "*/*",
'Content-Type': "application/json",
'X-Requested-With': "XMLHttpRequest",
# 请求头最基本的两个
'Referer': 'http://www.whggzy.com/PoliciesAndRegulations/index.html?utm=sites_group_front.4dd516b0.0.0.0aeeb380992811ec85e0b5fac8c3351d',
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36',
}
# post方法会传递一个表单
# 表单内容,在F12的"载荷"里面,点击查看源代码,整理成JSON格式
# data = {"utm": "sites_group_front.4dd516b0.0.0.0aeeb380992811ec85e0b5fac8c3351d","categoryCode": "GovernmentProcurement","pageSize": 15,"pageNo": 1}
data = "{\"utm\":\"sites_group_front.4dd516b0.0.0.0aeeb380992811ec85e0b5fac8c3351d\",\"categoryCode\":\"GovernmentProcurement\",\"pageSize\":15,\"pageNo\":1}"
# headers和data会被前端验证——js验证(一般不会从服务器端验证)
response = requests.post(url,headers=headers, data=data).text
print(response) # 结果正确
问题:
当我们遇到数据加密时或者页面没加载时候,无法进行js调试,那么该怎么办?请看案例二:https://www.yuque.com/u1046159/erg6ec/on17rw#dUpIP
找到请求头和请求体后,结束xhr调试
案例二:数据加密
爬取的网站:https://www.qimingpian.cn/finosda/project/pinvestment
当我们按照案例一的方法获取接口时,发现数据没有找到对应的匹配项
遇到这种情况我们先尝试刷新一下页面,然后再进行搜索,看有没有找到接口地址,如果还没有,基本确定数据被加密(常用的反爬手段)
抓包
当我们刷新页面时,发现数据前后加载顺序不一样,说明数据是动态加载的 — > ajax —> 对应浏览器调试中的XHR
寻找接口
方式一:接口数量少,选择最大的
我们会发现,有两个接口,我们选择最大的接口:
方式二:接口数量多
我们发现数据被加密,而且接口数量多,那么该如何选择接口?
当这种网站有分页功能,我们可以通过点击分页来发现接口,我们点击第二页,发现多了个接口,那么这个接口就是我们想要的:
点击第三页、第四页,发现加载不同的接口,而且接口名称有规律:
调试js
- 我们回到案例二,现在我们通过方式一方法:https://www.yuque.com/u1046159/erg6ec/on17rw#X8Dsk寻找到了想要的接口,发现里面含有加密数据,其关键字时encrypt_data:
- 这时我们可以通过调试js方法中的方法一:关键字搜索,在网络一栏,使用Ctrl + F,搜索encrypt_data:
- 我们会发现有多个文件都包含关键字encrypt_data,但是我们的目标文件是js文件
- 找到js文件,然后右键在“来源”面板中打开,打开之后会跳转到指定js,然后选择格式化,格式化后Ctrl + F,搜索关键字encrypt_data
搜索关键字之后,发现有多个匹配项,那么如何确定哪个匹配项才是我们想要的?我们从步骤1中发现该接口是post请求
搜索后,浏览匹配项发现,有四个encrypt_data后面跟的是img_url,很明显与图片上传有关,不符合我们接口形式
只有最后两个才可能与接口数据有关,然后在该处打上断点:
然后刷新浏览器,此时会在断点处停止,同时在xhr调试栏中作用域一栏,发现加密数据encrypt_data
- 接下来我们要看加密数据encrypt_data传递给了谁,我们需要找到主体解密函数(最主要解密函数或者方法),为什么找到解密函数就可以解析数据呢,浏览器上面展示的是明文数据,一般js会对服务器返回来的加密数据做解析操作,解析成明文数据。 那我们只需要找到这个加密数据传递给了哪个解密函数或者方法。
- 我们进行单步调试,同时观察加密数据有没有变化
- 当进行若干步调试后,我们发现加密数据传递给了变量t
我们将左边对应位置的主体解密函数扣下来:
function o(t) {
// 主体解密函数
return JSON.parse(s("5e5062e82f15fe4ca9d24bc5", a.a.decode(t), 0, 0, "012345677890123", 1))
}
- 变量t是传递过来的加密值
- JSON.parse() 方法用来解析JSON字符串,在Python中对应:json.dumps(): 对数据进行编码;json.loads(): 对数据进行解码。
- s 暂时不知道是什么
- a.a.decode(t)一看就是方法,对象.方法.方法 / 对象.方法.属性
- 那么接下来我们需要补全上述js代码 —> s 和a.a.decode(t)
鼠标光标选中s,浏览器会出现s对应的位置:
点击上述位置,可以发现s是一个方法:
我们将方法s全部复制下来:
function s(t, e, i, n, a, s) {
//省略
}
- 一定要粘贴完整,否则会报错
- 可以使用辅助工具notepad++,将页面上所有js代码复制下来,在notepad++中创建javascript文件,然后可以清楚的看出该方法起始位置:
- 用同样的方法,找到a.a.decode(t)
鼠标光标选中a.a.decode(t),进入相应的方法中,然后格式化js
我们将上述代码复制下来:
decode: function (t) {
//省略
}
- 发现上述代码中
decode:
是一个对象(只有对象才有:
)
但a.a.decode( )通过分析是一个方法,但是上述复制下来的代码很明显不是一个方法,我们需要改造—>补变量,我们需要将上面对象改成一个方法:
function decode1 (t) {
//省略
}
同时将function _o_(t) {}
方法中对decode引用进行修改:
修改前:
function o(t) {
// 主体解密函数
return JSON.parse(s("5e5062e82f15fe4ca9d24bc5", a.a.decode1(t), 0, 0, "012345677890123", 1))
}
修改后:
function o(t) {
// 主体解密函数
return JSON.parse(s("5e5062e82f15fe4ca9d24bc5", decode1(t), 0, 0, "012345677890123", 1))
}
a.a.decode1(t)
—>decode1(t)
- 测试解密函数
我们在js中定义一个data变量,值为加密值(变量t的值):
data = ' '
然后测试:
console.log(o(data))
- 调用o方法,传递变量data
运行js文件(需要在专业版pycharm的插件中下载node.js,同时电脑需要安装有node.js)
运行报错:
查看错误位置:
发现方法中变量f并没有定义,我们需要定义变量f,但是变量f的值是什么呢?我们在浏览器方法decode1
的末尾return o
打上断点,然后调试:
然后在控制台中查看f的值:
现在知道f的值,那我们在方法decode1中定义f:
再次运行js后发现仍然报错:
用同样的方法,再次定义变量c:
再次运行,出现结果:
- 为了方便使用,我们可以使用Python处理结果
首先我们将function _o_
进行改造:去掉JSON.parse
function o(t) {
// 主体解密函数
return (s("5e5062e82f15fe4ca9d24bc5", decode1(t), 0, 0, "012345677890123", 1))
}
这时运行js会发现是union值:
我们新建一个py文件
# 执行js的,需要先下载:pip install PyExecJS
import execjs
import json
with open('./123.js','r',encoding='utf-8') as f:
jscode = f.read()
data = 'bOnqtWHqs4t32kZeWEzfoNqIA+aTiXXJK0WUl33PSRHRdOP1Ra6hXvpyOuayBpv/+8PWp6dcAdfLjA5wHhtnmvzviUI8HD7smK1pHMdWEBEpAV0tcEa77aQ7isTpWf2gkv1Zwl9Q6qhtArZahpWrqd8pFZfCVTJr1fGP1MAOWaU7VWL6aSfR1H4aoW/AuJm6mYpFza91XazvbiQVwqL2I7dgj9cMMqITU4KOF+uDw0If7gnaPzn9ZHWCzKZXsHkyx09hbz8xfHJGOGerfZ/3UTBFc1VP9luB8PZHArc4s97Ck7cjXmlc9s1SNnh9/0IyMVxVHT45FHMSHkfbRWOrZzJD/7NwnCGGBExFM1EaUsqYnhIZCt9iCxC3YUxQcc/YyBynN4yeMy54mZGw1YdnfjBLfZsZcQGJHD5plYuZtZzGtT5axTGEc+wFJIOBM9KqAVDP9EjXQbLx0CzDP4mU22ZXuJ8VI2WFomyKq0c1TmXoRIx/YMaDDW732YTvJ5ip9OWZPtfoIxhY3dsgFcXMXJc99avJFFdP8k0WMT2PMz+ir5MJ0eiN6lfXU79AAZgxWgRh5YEpzuhzzIV/hz+44gp6xGS55YblDp1bgfyVHbCq+cJHDHSmxvzviUI8HD7s0iqkrrOZBiSEBMmc+FtEcUNvYUESaJdbOknxv9zQYXQBWXAjhVlAVUuVy/jXrjzbqlhd3bt1v1suwhm+Kz2exc5+hS9LZfPmxnbMKNWSz5vsVCgRqAGpQXs9q9JWuqWTZ5z6syydbD6EBMmc+FtEcThqfYm+nJjcrjlayLajflRw0tRpl9AnyJHN/nJ5ELnF+20gvOmiAz6xIqPbqriBiwp6xGS55YblzJfV4qeU8G93/WUWBMPCkaxKCZlGaFwMFCp8heQuc6iRb1jA68WpXJP6vp74NfYCgxD5WsD5jQgknVUG044HfFesgIYec2QzhrWDvykmb0tYLCPPX67TWi9edT8CdDLRrlqFzRy5W/ZK6XTlbdtONX6hLB2OFGq9+HIqseypV7KXHoBO7n7dTBMrdxLOhJQGSPoL5pj/LI6myDxwsHGYOoX2buu8NvBiL3DQCNKjNj3DcLycA4oSv1dXsk8qipYTCIObMXgn4EYPCZ2IBLLpnCZ5Z1Obg1uUNLDyb68uEl8loUzY7NcsGOMwPFTHxPFm9o+t084ivyeYIWrwLKWa7wALlee5m92J'
ctx = execjs.compile(jscode).call('o',data)
ddata = json.loads(ctx)
print(ddata)
结果打印出来了,但是结果不正确
{'list': ['企业服务', '医疗健康', '生活服务', '人工智能', '大数据', '区块链', '教育培训', '文娱传媒', '金融', '电子商务', 'VR/AR', '旅游户外', '餐饮业', '房产家居', '汽车交通', '体育健身', '食品饮料', '物联网', '硬件', '游戏', '生产制造', '物流运输', '农业', '批发零售', '先进制造', '社交社区', '工具软件', '服装纺织', '建筑', '开采', '环保', '能源电力', '政务及公共服务', '科研及技术服务', '消费升级', '新基建', '硬科技']}
这是因为我们传递的加密前的值不对,应该是变量encrypt_data的值,我们先将断点全部取消
然后重新打开调试工具,找到接口,复制encrypt_data的值:
然后替换Python代码中data的值:
结果正确:
网站上:
案例三:请求头加密
待爬取网址:https://www.oklink.com/zh-cn/btc/tx-list?limit=20&pageNum=1
抓包
通过搜索关键字方式快速定位到接口:
我们发现接口请求方式是get请求,但是含有请求参数,我们查看请求头,发现请求头中含有一些不一样的参数devId和x-apiKey(这里我们先分析x-apiKey)
调试js
- 这里我们使用路径搜索,路径为:
[https://www.oklink.com/api/explorer/v1/btc/transactionsNoRestrict?t=1653542831111&limit=20&offset=0](https://www.oklink.com/api/explorer/v1/btc/transactionsNoRestrict?t=1653542831111&limit=20&offset=0)
,搜索路径关键字transactionsNoRestrict
找到对应的js文件,然后右键在来源面板中打开,格式化js代码,搜索之前请求头中的关键字x-apiKey
- 搜索后,我们可以断定getApiKey( )是主要的加密函数/方法,在此处打一个断点,然后刷新页面,断点调试,知道所在位置是我们刚才打断点的位置,进入方法getApiKey( )中:
- 上述方法/函数就是我们的主体加密函数,将其复制下来:
value: function () {
var e = (new Date).getTime()
, t = this.encryptApiKey();
return e = this.encryptTime(e),
this.comb(t, e)
}
- 但是上面复制下来的js存在问题:
- 上面是一个对象,我们需要将其改为方法
- this指代的方法没有填充,不知道是什么内容,我们可以通过搜索填充对应的方法
- 修改以及补充(找到对应的对象同时将其改为方法)3步骤复制下来的js代码:
```javascript
function getApiKey() {
// 有3个this,表示有三个方法
var e = (new Date).getTime() // 时间戳
return e = encryptTime(e),, t = encryptApiKey();
}comb(t, e)
function encryptApiKey() { var e = this.API_KEY , t = e.split(“”) , o = t.splice(0, 8); return t.concat(o).join(“”) }
function encryptTime(e) { var t = (1 e + 1111111111111).toString().split(“”) , o = parseInt(10 Math.random(), 10) , r = parseInt(10 Math.random(), 10) , n = parseInt(10 Math.random(), 10); return t.concat([o, r, n]).join(“”) }
function comb(e, t) { var o = “”.concat(e, “|”).concat(t); return window.btoa(o) }
- 去掉之前的this
- 将对象改为方法,方法名对照网页上对象中的key名
我们再次检查上面代码,查看是否还有this,如果有,查看this所指代的类型(是方法还是变量还是函数),去掉this,补充对应的(方法、变量)(发现有一个,this指代的是变量):<br /><br />搜索**API_KEY:**<br /><br />再次补充:<br /><br />填充后的js:
```javascript
function getApiKey() {
// 有3个this,表示有三个方法
var e = (new Date).getTime() // 时间戳
, t = encryptApiKey();
return e = encryptTime(e),
comb(t, e)
}
function encryptApiKey() {
var API_KEY = "a2c903cc-b31e-4547-9299-b6d07b7631ab";
var e = API_KEY
, t = e.split("")
, o = t.splice(0, 8);
return t.concat(o).join("")
}
function encryptTime(e) {
var t = (1 * e + 1111111111111).toString().split("")
, o = parseInt(10 * Math.random(), 10)
, r = parseInt(10 * Math.random(), 10)
, n = parseInt(10 * Math.random(), 10);
return t.concat([o, r, n]).join("")
}
function comb(e, t) {
var o = "".concat(e, "|").concat(t);
return window.btoa(o)
}
调用主函数,进行测试:
console.log(getApiKey());
运行后报错:
定位到报错位置:
通过搜索:
但是对于我们爬虫来说不需要浏览器对象,所以可以直接删除,删掉后有一个问题,还剩window.btoa,我们不知道btoa是什么东西,继续查:
所以我们可以继续删掉btoa,然后最后结果再处理base-64编码:function comb(e, t) { var o = "".concat(e, "|").concat(t); // return window.btoa(o) return o // base-64处理 }
再次运行:
我们使用base64编码验证输出结果是否正确:
网页上返回值:
会发现结果正确
- 然后用Python补充后的js代码返回的结果进行base64编码 ```python import base64 import execjs
with open(‘./234.js’, ‘r’, encoding=’utf-8’) as f: jscode = f.read()
ctx = execjs.compile(jscode).call(‘getApiKey’) apikey = base64.b64encode(ctx.encode()).decode() print(apikey)
<a name="FqZpU"></a>
## 调用解析
使用requests从接口获取数据
```python
import requests
from 爬虫.js逆向解析.day01.demo234 import apikey
url = 'https://www.oklink.com/api/explorer/v1/btc/transactionsNoRestrict'
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36",
"Referer":"https://www.oklink.com/zh-cn/btc/tx-list?limit=20&pageNum=1",
"devId":"890366d6-1b4c-49ca-8a0c-c1fc34cd8d59",
"x-apiKey":apikey
}
data = {
"t":1653548944234,
"limit":20,
"offset":0
}
response = requests.get(url,params=data,headers=headers).text
print(response)
案例四:美团
待爬取网址: