YuanFang0w0 发表于 2022-5-15 22:46

Python基于Selenium控制Chrome捕获Ajax请求(省去解析网页的繁琐)

# 主题

Python基于Selenium控制Chrome捕获Ajax请求(不使用代{过}{滤}理服务器,不解析网页元素,直接捕获)
你还觉得Selenium爬取效率低下吗?

## 一、前言

### 1.需求以及问题

有需求需要爬取目标网站(国外网站)的用户信息,但是这个网站反爬机制还是比较强的,直接去用requests请求ajax接口直接会失败,请求到的页面将是一个用于人机校验的谷歌验证。

因此通过我不断地试验(伪装请求头,除了必要的cookies等参数,我其他的一个个试验),最终还是没有突破它的限制,我又去GitHub上找了找相关的项目,同样的问题并没有得到解决。最终,我将目光投向了Selenium。

### 2.Selenium分析

使用Selenium的时候,依然不能使用无头模式(即使加上了规避检测的内容依然如此),但是对于我这个项目需求来说,无头与否影响不大,因此我只做了浏览器的最小化即可。

使用Selenium时,我们面临的最大问题就是需要解析大量的网页元素,对于数据请求效率太低。特别是对于ajax请求的接口数据而言,本质上只需要拿到ajax返回的json数据直接解析json就可以,但是Selenium却要去大量解析网页内容,等待网页渲染,实在是一键很麻烦的事情。

### 3.查找解决办法

因此我决定上网查找资料,查找Selenium可以直接获取ajax请求结果的办法。经过搜索查找资料,网上的办法大多都是用代{过}{滤}理服务去截获请求,这个方法就需要写一个服务端的内容,无疑加大了工作量。我并不是很想用这种方法。

### 4.自己分析解决

苦苦思考下,突然想到了一个方法,调用Selenium在控制台执行js脚本的方法不知道行不行得通呢,于是我打算去试一下。

## 二、浏览器实操测试

那么Selenium执行Js脚本,肯定得编写出需要的脚本,这里我们不用编写的过于复杂,仅仅执行请求指定的ajax接口即可。

这里我随便找一个有ajax请求的网站。(示例网站:`https://book.qidian.com/info/1033601341/`)

1.打开网站,打开浏览器控制台,切换到网络选项卡,点击`Fetch/XHR`,指定捕获ajax请求,刷新网页,可以看到已经有了一些ajax请求被捕获到了。
!(https://s1.328888.xyz/2022/05/15/qKHx2.png)

2.我们随便选一个请求,鼠标右击,复制,复制为Fetch即可。
!(https://s1.328888.xyz/2022/05/15/qKdT7.png)
复制的结果如下:

```javascript
fetch("https://book.qidian.com/ajax/book/getFansHall?_csrfToken=YZNRZe0oR9VKe4QBccx6qIbURkXtcOCvg27LQNni&bookId=1033601341", {
"headers": {
    "accept": "application/json, text/javascript, */*; q=0.01",
    "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
    "sec-ch-ua": "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"101\", \"Microsoft Edge\";v=\"101\"",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": "\"Windows\"",
    "sec-fetch-dest": "empty",
    "sec-fetch-mode": "cors",
    "sec-fetch-site": "same-origin",
    "x-requested-with": "XMLHttpRequest"
},
"referrer": "https://book.qidian.com/info/1033601341/",
"referrerPolicy": "strict-origin-when-cross-origin",
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "include"
});
```

3.来到控制台,粘贴我们复制的Fetch,然后回车执行。
!(https://s1.328888.xyz/2022/05/15/qKDhM.png)

4.切换到网络选项卡,可以看到已经有一个新的请求被发起过了。
!(https://s1.328888.xyz/2022/05/15/qKlNX.png)

5.不难发现,控制台执行完Fetch后并没有得到我们想要的json数据,而是一个对象。这个时候就要重新改写一下第二步中复制的Fetch。让他的结果成为我们需要的Json数据,方便我们进行解析。

对复制的Fetch进行改写如下:

```javascript
fetch("https://book.qidian.com/ajax/book/getFansHall?_csrfToken=YZNRZe0oR9VKe4QBccx6qIbURkXtcOCvg27LQNni&bookId=1033601341", {
"headers": {
    "accept": "application/json, text/javascript, */*; q=0.01",
    "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
    "sec-ch-ua": "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"101\", \"Microsoft Edge\";v=\"101\"",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": "\"Windows\"",
    "sec-fetch-dest": "empty",
    "sec-fetch-mode": "cors",
    "sec-fetch-site": "same-origin",
    "x-requested-with": "XMLHttpRequest"
},
"referrer": "https://book.qidian.com/info/1033601341/",
"referrerPolicy": "strict-origin-when-cross-origin",
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "include"
}).then((res)=>{return res.json()}).then((res)=>{console.log(res)});
```

这里去除了最后的分号,加上了一句代码`.then((res)=>{return res.json()}).then((res)=>{console.log(res)});`

改过之后再去浏览器中测试一下,可以看的到已经能正常获取到json数据了。
!(https://s1.328888.xyz/2022/05/15/qK5fZ.png)

至此,在浏览器中的测试就完全通过了,下面我们要用代码进行实现上面的操作。

## 三、代码实现操作

1.使用Python+Selenium加载一个浏览器对象,打开指定的网页(这里依然使上面用测试时候的网址进行演示)。
2.对【二】中第5步中的代码再进行一次改写,目的是使代码可以通过Selenium中的`execute_script()`方法执行并且可以获取到返回的结果,改写如下:

```javascript
var resp = fetch("https://book.qidian.com/ajax/book/getFansHall?_csrfToken=YZNRZe0oR9VKe4QBccx6qIbURkXtcOCvg27LQNni&bookId=1033601341", {
"headers": {
    "accept": "application/json, text/javascript, */*; q=0.01",
    "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
    "sec-ch-ua": "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"101\", \"Microsoft Edge\";v=\"101\"",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": "\"Windows\"",
    "sec-fetch-dest": "empty",
    "sec-fetch-mode": "cors",
    "sec-fetch-site": "same-origin",
    "x-requested-with": "XMLHttpRequest"
},
"referrer": "https://book.qidian.com/info/1033601341/",
"referrerPolicy": "strict-origin-when-cross-origin",
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "include"
}).then((res)=>{return res.json()});
return resp
```

这里一共做了三处改动:

1.在`fetch`前加上了`var resp =`,将`fetch`返回的值保存为一个变量;

2.去掉了代码最后的`.then((res)=>{console.log(res)})`,原因是这一行是让结果在控制台中以json输出,其实获取json数据只需要`.then((res)=>{return res.json()})`这一行代码就足以了。

3.最后加了一行`return resp`,返回这个变量的值(这个值就是需要json数据)。

需要注意的是:这个网站的这个ajax请求后面会带上一个`csrfToken`,因此在Python中运行时要对其进行改变,不然请求肯定是得不到需要的正确数据的。

改写完之后就是把代码放在Pyhon中执行了,下面是一点示例代码(仅演示执行ajax,不说明获取token):

```python
    print('1.加载浏览器')
    browser = selenium(url='https://book.qidian.com/info/1033601341/',
                     driver_path='chromedriver.exe')
    print('2.执行JS代码')
    token = input('请输入token')
    fetch = r'''var resp = fetch("https://book.qidian.com/ajax/book/getFansHall?_csrfToken=''' + token + r'''&bookId=1033601341", {
"headers": {
    "accept": "application/json, text/javascript, */*; q=0.01",
    "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
    "sec-ch-ua": "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"101\", \"Microsoft Edge\";v=\"101\"",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": "\"Windows\"",
    "sec-fetch-dest": "empty",
    "sec-fetch-mode": "cors",
    "sec-fetch-site": "same-origin",
    "x-requested-with": "XMLHttpRequest"
},
"referrer": "https://book.qidian.com/info/1033601341/",
"referrerPolicy": "strict-origin-when-cross-origin",
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "include"
}).then((res)=>{return res.json()});
return resp'''
    resp = browser.execute_script(fetch)
    print(resp)
    input('回车退出')
    browser.quit()
```

值得注意的一点是:复制的fetch放在Python中有很多”\“,这些”\“不能被删除,要注意在python的字符串前加`r`,让字符串变为原生字符串。

下面是执行的结果展示:

1.Python执行结果:
!(https://s1.328888.xyz/2022/05/15/qZdc4.png)

2.浏览器控制台Network选项卡中的情况:
!(https://s1.328888.xyz/2022/05/15/qZlNB.png)

这里Python执行JavaScript代码在控制台是看不到记录的,直接从Network看即可。

## 四、声明与总结

1.首先,我在互联网找资料并没有找到此类方法,但是不排除有人已经用过,我只是想分享出来我自己思考的这个思路,如欲转载,请声明出处!

2.这个方法面对一些request请求不到的情况还是挺好用的,也不用考虑cookies的问题,且直接在浏览器中发起请求,单论ajax请求的效率还是与直接requests相差不多的。效率上也并没有特别低下,免去了直接解析网页的繁琐。

3.纯属个人见解,如有错误或者任何问题请指正!

话痨司机啊 发表于 2022-5-17 12:41

YuanFang0w0 发表于 2022-5-17 11:03
不是,我是因为他这个ajax用request请求直接就反爬了。不是参数拿不到。是所有参数齐全的情况下,我用req ...

你可以了解一下 ws技术

话痨司机啊 发表于 2022-5-17 09:41

本帖最后由 话痨司机啊 于 2022-5-17 09:56 编辑

问一下,写个JS ajax用execjs直接调用请求应该跟这个一样吧~,逆向JS直接把csftoken也搞下来,就可以本地运行了,不需要再Copy token去了吧~感觉你绕了个弯子,不知道我说的对不对,不过也是种办法,在扣不出来js的时候


LBF.define("qd/js/component/common.4ba51.js", function(require, exports, module) {
    var e = require("ui.Nodes.Node")
      , d = (require("qd/js/component/browserSupport.1ad6c.js"),
    require("qd/js/component/login.29081.js"),
    require("qidian.report"))
      , t = require("util.Cookie")
      , r = require("qd/js/component/third.a7feb.js");
    module.exports = e.inherit({
      el: "body",
      events: {},
      elements: {},
      render: function() {
            return this.setElement(this.el),
            this.init(),
            this
      },
      init: function() {
            var e = r.getThirdData()
            , t = e.ua
            , a = (e.isThird && (r.hideAdvertising(),
            r.isIframe() && r.hideLoginBox(),
            document.getElementById("reg-btn") && (document.getElementById("reg-btn").href = "//passport.qidian.com/reg.html?appid=10&areaid=" + e.areaid + "&target=iframe&ticket=1&auto=1&autotime=30&returnUrl=https%3A%2F%2Fwww.qidian.com"),
            document.getElementById("fixed-reg") && (document.getElementById("fixed-reg").href = "//passport.qidian.com/reg.html?appid=10&areaid=" + e.areaid + "&target=iframe&ticket=1&auto=1&autotime=30&returnUrl=https%3A%2F%2Fwww.qidian.com")),
            "https://pay.yuewen.com/pc/index?appId=10&areaId=" + e.areaid + "&&returnUrl=" + location.href)
            , e = ($("body").on("click", ".qqlivepay", function(e) {
                "QQLive" == t && (external && external.OpenQQLive && external.OpenQQLive(a),
                e.preventDefault())
            }),
            g_data.bookInfo && g_data.bookInfo.bookId || g_data.pageJson && g_data.pageJson.bookId)
            , n = g_data.chapter && g_data.chapter.id
            , i = g_data.chapter && g_data.chapter.vipStatus
            , o = g_data.readSetting && g_data.readSetting.rt;
            "undefined" == typeof g_data.isWebSiteType || 1 === g_data.isWebSiteType ? d.init({
                isQD: !0,
                cname: "QDpclog",
                bid: e,
                cid: n,
                ua: t,
                bookstatus: i,
                rt: o
            }) : 0 === g_data.isWebSiteType && d.init({
                isQD: !0,
                cname: "QDmmlog",
                bid: e,
                cid: n,
                ua: t,
                bookstatus: i,
                rt: o
            }),
            this.checkLang(),
            this.pageLoaded(),
            $.ajax({
                url: "/ajax/Help/getCode"
            }).done(function(e) {
                0 === e.code && $(".footer .advice").prop("href", "http://yw.95ib.com/online/?cid=0&uid=10&code=" + e.data)
            })
      },
      pageLoaded: function() {
            $("html").addClass("loaded")
      },
      createSender: function(e) {
            var t = new Image;
            t.onload = t.onerror = function() {
                t = null
            }
            ,
            t.src = e
      },
      checkLang: function() {
            "zht" == t.get("lang") && (require["async"]("qd/css/tradition_font.d9168.css"),
            require["async"]("qd/js/component/chinese.d5874.js", function(e) {
                $("#switchEl").html("简体版"),
                $(".lang").css("fontFamily", "T_FZZCYSK"),
                e.trans2Tradition("html")
            }))
      }
    })
});

狄人3 发表于 2022-5-15 22:59

Chromedriver嘛?这个其实还有一些更好的方法,但效率还是说不上很高吧

Soc 发表于 2022-5-15 23:18

学习一下

YuanFang0w0 发表于 2022-5-15 23:18

狄人3 发表于 2022-5-15 22:59
Chromedriver嘛?这个其实还有一些更好的方法,但效率还是说不上很高吧

我觉得只是ajax的话感觉跟requests差的不多

吾爱modi 发表于 2022-5-15 23:25

谢楼主分享,刚好用上。

Venda 发表于 2022-5-15 23:40

乍一看好像还行,仔细一看貌似没啥用

atmo 发表于 2022-5-15 23:48

不错不错,学习一下!

FreeOvO 发表于 2022-5-16 00:09

YuanFang0w0 发表于 2022-5-16 00:27

Venda 发表于 2022-5-15 23:40
乍一看好像还行,仔细一看貌似没啥用

至少对我来说我这个项目是用上了。

AdmiralHipper 发表于 2022-5-16 01:25

感谢,多了一种思路
页: [1] 2 3 4 5
查看完整版本: Python基于Selenium控制Chrome捕获Ajax请求(省去解析网页的繁琐)