Ajax hook

AJAX_HOOK

1.Ajax-hook 原理解析

github 链接: https://github.com/wendux/Ajax-hook 中文介绍:http://www.jianshu.com/p/9b634f1c9615

Ajax Hook 顾名思义就是 Hook Ajax 请求了,Ajax 最重要的两个部分?当然就是 Request、Response 了,有了 Hook,我们就能在发起 Request 前和得到 Response 后对二者进行处理了。

(1)整体思路-代理模式

Ajax-hook实现的整体思路是实现一个XMLHttpRequest的代理对象,然后覆盖全局的XMLHttpRequest,这样一但上层调用 new XMLHttpRequest这样的代码时,其实创建的是Ajax-hook的代理对象实例。具体原理图如下:

img

ajax-hook原理图

上图中青色部分为Ajax-hook实现的代理XMLHttpRequest,内部会调用真正的XMLHttpRequest。我们看一下hookAjax的部分源码:

ob.hookAjax = function (funs) {
  //保存真正的XMLHttpRequest对象
  window._ahrealxhr = window._ahrealxhr || XMLHttpRequest
  //1.覆盖全局XMLHttpRequest,代理对象
  XMLHttpRequest = function () {
    //创建真正的XMLHttpRequest实例
    this.xhr = new window._ahrealxhr;
    for (var attr in this.xhr) {
      var type = "";
      try {
        type = typeof this.xhr[attr]
      } catch (e) {}
      if (type === "function") {
        //2.代理方法
        this[attr] = hookfun(attr);
      } else {
        //3.代理属性
        Object.defineProperty(this, attr, {
          get: getFactory(attr),
          set: setFactory(attr)
        })
      }
    }
  }
  ......

Ajax-hook 一开始先保存了真正的XMLHttpRequest对象到一个全局对象,然后在注释1处,Ajax-hook覆盖了全局的XMLHttpRequest对象,这就是代理对象的具体实现。在代理对象内部,首先创建真正的XMLHttpRequest实例,记为xhr,然后遍历xhr所有属性和方法,在2处hookfun为xhr的每一个方法生成一个代理方法,在3处,通过defineProperty为每一个属性生成一个代理属性。下面我们重点看一看代理方法和代理属性的实现。

(2)代理方法

代理方法通过hookfun函数生成,我们看看hookfun的具体实现:

function hookfun(fun) {
 return function () {
    var args = [].slice.call(arguments)
    //1.如果fun拦截函数存在,则先调用拦截函数
    if (funs[fun] && funs[fun].call(this, args, this.xhr)) {
      return;
    }
   //2.调用真正的xhr方法
   this.xhr[fun].apply(this.xhr, args);
 }
}

为了叙述清晰,我们假设fun为 send函数,其中funs为用户提供的拦截函数对象。代码很简单,首先会根据用户提供的funs判断用户是否要拦截send, 如果提供了send的拦截方法,记为send_hook, 则上层调用代理对象send方法时,则会先调用send_hook,同时将调用参数和当前的xhr对象传递给send_hook,如果send_hook返回了true, 则调用终止,直接返回,相当于调用被终止了,如果没有返回或返回的是false,则会走到注释2处,此处调用了xhr的send方法,至此ajax send被调用成功。 所以,我们在send_hook中可以拿到调用的参数并修改,因为参数是以数组形式传递,改变会被记录,当然,我们也可以返回true直接终止调用。

(3)代理属性

属性如onload、onreadystatechange等,上层在调用ajax时通常要设置这些回调以处理请求到的数据,Ajax-hook也能够实现在请求返回时先拿到数据第一个进行处理,然后将处理过的数据传递给用户提供的回调。要实现这个功能,直接的思路就是用户设置回调时将用户提供的回调保存起来,然后设置成代理回调,当数据返回时,代理回调会被调用,然后在代理回调中首先将返回的数据提供给拦截函数处理,然后再将处理后的数据传递给用户真正的回调。那么问题来了,如何捕获用户设置回调的动作?一段典型的用户调用代码如下:

var xh=new XMLHttpRequest;
xh.open("https://xxx")
xh.onload=function(data){ //1
  //处理请求到的数据
}

也就是说上面代码1处的赋值时机代理对象怎么捕获?如果在赋值的时候有机会执行代码就好了。我们回过头来看看上面原理图,有没有注意到proxy props后面的小括号里的 es5,答案就在这里! es5中对于属性引入了setter、getter,详细内容请参考:
Javascript getter: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get
Javascript setter: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set

Ajax-hook通过getFactory和setFactory生成setter、getter方法。我们来看看它们的实现:

function getFactory(attr) {
    return function () {
        return this[attr + "_"] || this.xhr[attr]
    }
}

function setFactory(attr) {
    return function (f) {
        var xhr = this.xhr;
        var that = this;
        //区分是否回调属性
        if (attr.indexOf("on") != 0) {
            this[attr + "_"] = f;
            return;
        }
        if (funs[attr]) {
            xhr[attr] = function () {
                funs[attr](that) || f.apply(xhr, arguments);
            }
        } else {
            xhr[attr] = f;
        }
    }
}
(4)属性修改

如果需要对返回的数据进行加工处理,比如返回的数据是json字符串,如果你想将它转化为对象再传递给上层,你可能会在onload回调中这么写:

xhr.responseText = JSON.parse(xhr.responseText)

但是,这里有坑,因为xhr的responseText属性并不是writeable的(详情请移步 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty ),这也就意味着你无法直接更改xhr.responseText的值,而Ajax-hook也代理了这些原始属性,我们可以通过代理xhr对象来赋值:

xhr.getProxy().responseText = JSON.parse(xhr.responseText)

关于,代理xhr对象原生xhr对象的区别请参考Ajax-hook github文档

python实现AJAx_HOOK

案例介绍

下面我们就拿一个我自己的案例来讲吧,链接为:https://dynamic2.scrape.center/,界面如下:

null

这个网站是一个电影数据网站,其数据都是通过 Ajax 加载的,但是这些 Ajax 请求都带着加密参数 token,如图所示:

null

其实这个参数你要解的话倒不是很难,不过也得费点时间。

然后再看下 Ajax 的返回结果,如图所示:

null

很纯很清晰!所以我们如果能够在得到 Ajax Response 的时候就把这些数据直接拿到,那就美滋滋了。

怎么办?自然是用刚才所说的 Ajax-hook 了。

所以,我们这里就用上这个 Ajax-hook 来对这些数据进行实时处理吧。

实战操作

首先,第一步那我们得能用上 Ajax-hook,怎么用呢?那肯定得需要引入一下这个 Ajax-hook 库,浏览器里的这个页面又怎么引入呢?

答案有很多,比如复写 JavaScript、Tampermonkey、Selenium 等等。

这里我们就用最简单的方法,Selenium 自动执行一下 Ajax-hook 的源代码就好了。

那这时候我们就需要找到 Ajax-hook 的源码了,去 GitHub 一找就有了,链接为:https://raw.githubusercontent.com/wendux/Ajax-hook/master/dist/ajaxhook.min.js,如图所示:

null

看,代码量真不多吧。

我们把这个代码复制,粘贴到 https://dynamic2.scrape.center/ 这个网站的控制台里。

这时候我们会得到一个 ah 对象,代表 Ajax-hook,我们就能用它里面的 proxy 方法了。

怎么用呢?就直接实现 onResponse 方法,打印 Response 的结果就好了,实现如下:

ah.proxy({  //请求成功后进入  onResponse: (response, handler) => {    if (response.config.url.startsWith('/api/movie')) {      console.log(response.response)      handler.next(response)    }  }})

把这段代码也放在控制台运行下,这时候我们就实现了 Ajax Response 的 Hook 了,只要有 Ajax 请求,Response 的结果就会被输出出来。

这时候如果我们点击翻页,触发一个新的 Ajax 请求,就可以看到控制台输出了 Response 的结果,如图所示:

null

嗯,这下我们就能获取到 Ajax 的数据了。

数据转发

那现在数据在浏览器里面啊,我们怎么存下来呢?

存还不简单,最简单的,把这个数据转发给自己的一个接口保存下来就好了。

那我们就用 Flask 简单弄一个接口吧,记得解除跨域限制,实现如下:

import json
from flask import Flask, request, jsonify
from flask_cors import CORS

app = Flask(__name__)
CORS(app)


@app.route('/receiver/movie', methods=['POST'])
def receive():
    content = json.loads(request.data)
    print(content)
    # to something
    return jsonify({'status': True})


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=80, debug=True)

这里我就简单写了个示例,写了一个能接收 POST 请求的 API,地址为 /receiver/movie,然后把 POST 的数据打印出来再返回一个响应。

当然这里你可以做很多操作了,比如把数据切割,存储到数据库等都是可以的。

好的,那现在服务器有了,我们就在 Ajax-hook 这边把数据发过来吧。

这里我们借助于 axios 这个库,其库地址为 https://unpkg.com/axios@0.19.2/dist/axios.min.js,也是放在浏览器执行就能用。

引入 axios 之后,我们把之前的 proxy 方法修改为如下内容:

ah.proxy({
  //请求成功后进入
  onResponse: (response, handler) => {
    if (response.config.url.startsWith('/api/movie')) {
      axios.post('http://localhost/receiver/movie', {
        url: window.location.href,
        data: response.response
      })
      console.log(response.response)
      handler.next(response)
    }
  }
})

其实这里就是调用了 axios 的 post 方法,然后把当前 url 和 Response 的数据发给了 Server。

到现在为止,每次 Ajax 请求的 Response 结果都会被发给这个 Flask Server,Flask Server 对其进行存储和处理就好了。

自动化

OK,那现在我们已经可以实现 Ajax 拦截和数据转发了,最后一步自然就是把爬取自动化了。

自动化就分为三部分:

•打开网站。•注入 Ajax-hook、axios、proxy 的代码。•自动点击下一页翻页。

最关键的就是第二步了,我们把刚才 Ajax-hook、axios、proxy 的代码都放在一个 hook.js 文件里面,用 Selenium 的 execute_script 来执行就好了。

其他的几步很简单,最后实现如下:

from selenium import webdriver
import time

browser = webdriver.Chrome()
browser.get('https://dynamic2.scrape.center/')
browser.execute_script(open('hook.js').read())
time.sleep(2)

for index in range(10):
    print('current page', index)
    btn_next = browser.find_element_by_css_selector('.btn-next')
    btn_next.click()
    time.sleep(2)

最后,运行一下。

可以发现浏览器先打开了页面,然后模拟点击了下一页,再回过头来观察下 Flask Server 这边,可以看到 Ajax 的数据就接收到了,如图所示:

null

OK,到此为止。

总结

至此,我们就完成了:

•Ajax Response Hook•数据转发与接收•浏览器自动化

以后我们再遇到类似的情形,也可以用同样的思路来处理了。

本节代码:https://github.com/Python3WebSpider/AjaxHookSpider。

本文转载地址为:https://mp.weixin.qq.com/s/Nw3ZoBHa4f4Ew8XEfuyY1g 崔庆才
https://www.jianshu.com/p/7337ac624b8e lazydu

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值