异步加载的基本逻辑与浏览器抓包一般流程

本篇内容不涉及任何R语言或者Python代码实现,仅从异步加载的逻辑实现过程以及浏览器抓包分析的角度来给大家分享一下个人近期学习爬虫的一些心得。

涉及到的工具有Chrome浏览器(开发者工具)、postman(一款非常优秀的Chrome网络请求构造工具,你可以在Chrome浏览器在线商店里搜到,也可以下载桌面版)。

1、异步加载概念及实现过程 2、浏览器抓包分析一般流程

异步加载的英文简称是ajax,即“Asynchronous Javascript And XML”(异步JavaScript和XML)是指一种创建交互式网页应用的网页开发技术。它可以在无需重新加载整个网页的情况下,通过在后台与服务器进行局部数据交换,使得网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的特定部分进行更新。

这是百度百科对于异步加载的一般定义,在传统web开发中,使用同步加载模式,更新网页时,所有内容必须重载,导致多请求进程阻塞,网页迟迟无法加载,给web端体验造成很大的伤害。但是异步加载则解决了这个问题,通过异步加载,不仅提高了web端浏览体验,而且减缓了服务器端压力。

但异步加载却给网络数据抓取造成了很大的困难。困难在于,异步加载把所有网络资源分成了两大部分,一部分是静态的html文档(DOM文档),另一部分是嵌入在HTML文档内的js动态脚本。(这里暂时忽略css重叠样式表,它与任务目标几乎没什么联系)。这些js脚本通过<script>元素标签进行引用,是预定义好的js事件函数,我们所说的异步加载便是通过这些js脚本内的事件函数驱动的。

(浏览器在接受静态文档的同时,可以执行js脚本,与服务器交换数据并更新html内的数据块,但是R或者Python这种请求发送终端是无法做到这一点儿的)

这些事件函数内部,从新构建了一系列网络请求,这些网络请求可能是GET类型,也有可能是POST类型,这些请求便是异步加载的核心实现方式——XMLHttpRequest。XHR是由js脚本构建的,而js脚本是由其嵌入html的位置(<script>元素的所处位置)的html动作控制的。这些动作可能是鼠标点击事件、鼠标悬浮事件、下拉菜单、输入框输入查询关键词之后的回车等。

打开浏览器,并通过网址链接到主网页之后,浏览器会自动加载HTML文档,而同时内嵌的js脚本也会通过异步加载方式初始化一部分数据,这些js脚本加载过程与浏览器渲染html的过程并不相互影响。当用户在浏览器界面的特定位置点击或者实施某些html动作时,这些动作会驱动对应位置的js脚本执行其预定义的事件函数,构建XHR请求,这些XHR请求与服务器进行部分数据交互,返回数据之后,再通过回调函数操作对应位置html元素,将数据插入对应位置,至此数据交换的流程结束。

而对于我们这些爬虫学习者而言,其实想要找的就是这些js脚本构建的异步加载请求对象,通过截获这些请求,伪装成浏览器的身份,进而替代浏览器完成数据请求,并获取返回数据。这些异步请求在Chrome的开发者工具中往往都能截获到。

那么在浏览器得开发者工具中,以上所述得各部分又是如何是怎么对应的呢?

打开网易云课堂得主页,按F12进入开发者工具工作台。

Elements模块是浏览器加载后后的带有数据得完整HTML文档。

如何你是使用请求网页的方式来提取数据,那么通常你需要关注得便是这个模块。但是今天我们的主角是异步加载,所以定位到第二个模块——Network,该模块涉及到所有的浏览器与web服务器之间的交互请求记录。在Network模块的all子模块中,是所有请求列表,它包含了请求的所有dom文件、js脚本、css重叠样式表、img文件(图形对象)、Media文件(流媒体文件)、字体文件等。

而在XHR子菜单中,你可以看到这些加载文件中,以异步加载方式进行的文件对象。(xhr就是XMLHttpRequest的缩写),这个栏目将是我们爬虫抓包的主战场,所以要熟练这个界面的所有信息。

在XHR模块的Name列表中,有很多异步加载请求,你需要迅速过滤出我们想要的异步加载请求对象。

这里有一个秘诀!

这些请求对象一般包含两类,一类是.js文件,这些文件是javascript脚本文件,它们是事件驱动函数,是动作中介,尽管所有的异步加载请求都是由它们发起,返回数据也是由它们负责接收并且插入html文档的,但是它们本身并不包含数据,仅仅是一组脚本函数而已。所以在xhr中所有带有js结尾的文件都可以略过。(因为仅就抓包而言,你无须弄清楚这些请求实现的底层过程)。第二类是剩余的那些带有参数的链接、或者是以.json结尾文件。这些对象便是以上所说的js脚本构建的异步加载请求的目标,也是我们想要截获的请求。

针对本例而言,因为之前爬过网易云课堂,所以我心里知道想要的请求对象是studycourse.json,即便不知道,过滤掉js脚本之后,剩余链接中带有参数特征的,或者以json结尾的对象通常就是我们想要找的对象。

怎么验证呢,打开studycourse.json对象(鼠标点击),此时,右侧会出现五个子菜单。一般默认定位在Headers,这里的信息不足以让我们判断,该请求是否是想要的请求,此时你可以定位到第二项——Preview项目中。

当你定位到Preview项目,看到里面的json格式的数据包,打开后里面的内容与我们首页看到的刘凯老师的课程信息一致时,这时候就没错了,十拿九稳了。我们已经截获了想要的请求。

此时再次回到第一个菜单Headers中,Headers中一共四个模块,分别对应我们抓包分析中构造浏览器请求的四大部分。

General模块告诉我们的有用信息主要有两条:

该请求的地址是http://study.163.com/p/search/studycourse.json,这个地址也是我们伪造请求的唯一地址,请求类型是一个POST请求。

Response Headers

该模块是请求的响应报头,也即当请求构造成功之后,反回的数据有关内容。

它告诉我们的最为重要的信息(影响我们爬虫构建过程的)是返回的数据格式(Content-Type:application/json;charset=UTF-8),json返回值决定着我们需要对返回数据使用json的反序列化。(在R中可以使用jsonlite中的fromJSON,在Python中使用json包中的loads.json())。

Requests Headers

该模块是构造请求的请求报头,主要告知我们请求的一些具体信息,期待获取的数据,发送请求的终端类型,以及登录信息,参照页地址等。

重点关注其中的Cookies参数、Content-Type参数、Referer参数、User-Agent参数、edu-script-token参数。

User-Agent是标识请求发送的设备类型(常用于规避服务端反爬,可以伪造合法的终端类型)。 Content-Type是请求参数提交的类型,这里是application/json,就是json对象(在R里可以通过jsonlite包的toJSON()函数构造,在Python里使用json.dumps()函数序列化参数)。

Referer是参照页地址,也就是我们在浏览器看到的想要抓取的内容主页。(注意区别与上述所说的抓包需要的请求地址)

edu-script-token是一个重要的随机参数(意味着每打开 一次浏览器都会变动),它应该是当前进程的一个标识(个人理解)。

Cookies是登录状态,用于表明用户登录身份认证。 (requests参数虽然有常用的预定义参数,但是不同网站还有会有些独特的参数类型,实际抓包过程需要不断尝试)

Reqests Payload

最后是本次抓包分析的重头戏,查询条件部分。因为数据很多(通常情况下),不可能一次返回,所以我们需要构建一个查询表单,该表单是POST特有的(GET方法的查询参数包含在url中)。这些查询字符串规定了了返回数据中的活动课程 id,课程排序方式,课程作者,每次返回课程数目,页面课程最大数据,每次返回数据时课程偏移量等信息。

activityId:0
keyword:"刘凯"
orderType:5
pageIndex:1
pageSize:20
priceType:-1
searchTimeType:-1

这些表单需要构成一个json序列之后才能上传,R语言中稍微有些曲折,RCurl包中需要借助jsonlite包中的toJSON()函数进行参数序列化,httr包则含有可选的参数编码类型,直接指定即可。Python中的urllib、requests库,则直接通过json包的json.dumps()函数进行json序列化即可。

从新梳理一下:

General模块确定请求URL、请求方式:POST

Requests模块确定Cookies、Content-Type(请求参数提交格式)、Referer(请求定位的参照页)、User-Agent(设备类型)、edu-script-token(当前进程信息)

Resposes模块确定请求返回数据的格式:Content-Type,决定着我们使用什么函数处理返回值。

Request Payload模块决定提交参数信息(提交方式由Requests模块的Content-Type参数决定)。

至此异步加载的流程分析阶段完毕。

下面分享如何使用postman这款请求构造工具进行请求模拟,测试请求参数以及报头信息是否合法,是否可以 正常返回数据。

postman是一款很好用的网络请求模拟构造软件,打开之后,第一部分选择请求类型,第二部分输入请求URL,第三部分输入请求headers,第四部分输入请求的 查询表单体。

在输入body时,记得选择raw单选按钮,并且格式选择JSON(application/json),这是该请求指定的参数提交方式。

以上流程完成之后,可以点击send。

正常的话,在该界面底部就会返回json数据块儿,这些数据块会被自动按照其原格式解析和格式化,json返回值格式化之后如下所示:

在数据块中随便选中一个序列字段,使用Ctrl+F查找功能,查看一下一共有几个,与浏览器中课程数目是否一致。

结果一致,浏览器中的对应页面刚好也是9门课,本次请求构造成功,测试结束,需要构造的请求格式如下:

请求类型:POST 请求资源地址:http://study.163.com/p/search/studycourse.json 请求报头参数: Content-Type:application/json edu-script-token:40a297a878e54bdb9440a31345ad5f63 User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36 请求查询表单参数:(发送前需要序列化为json对象)

{
    "pageSize":20,
    "pageIndex":1,
    "searchTimeType":-1,
    "orderType":5,
    "priceType":-1,
    "activityId":0,
    "keyword":"刘凯"
}

将以上信息,使用R语言中的RCurl中的postForm函数、httr包中的POST函数,或者Python中的urllib包、requests包均可以模拟构造该请求,详细请求构造过程,不再重复,感兴趣可以参考这几篇文章。

网易云课堂Excel课程爬虫思路 左手用R右手Pyhon系列——趣直播课程抓取实战 Python数据抓取与可视化实战——网易云课堂人工智能与大数据板块课程实战 R语言网络数据抓取的又一个难题,终于攻破了! R语言爬虫实战——网易云课堂数据分析课程板块数据爬取 R语言爬虫实战——知乎live课程数据爬取实战

往期案例数据请移步本人GitHub: https://github.com/ljtyduyu/DataWarehouse/tree/master/File

原文发布于微信公众号 - 数据小魔方(datamofang)

原文发表时间:2017-11-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏web

vue.js环境搭建

1823
来自专栏程序员宝库

从0开始发布一个无依赖、高质量的键盘npm包

没有发布过npm包的同学,可能会对NPM对开发有一种蜜汁敬畏,觉得这是一个很高大上的东西。甚至有次面试,面试官问我有没有发过npm包,当时只用过还没写过,我想应...

1261
来自专栏沈唁志

使用WeeChat进行Internet中继聊天

WeeChat是一个用C语言编写的基于终端的多平台Internet中继聊天(IRC)客户端.Weechat旨在灵活和可扩展,因此具有用不同语言编写的各种插件,包...

1K3
来自专栏web开发

初识NodeJS

1.JavaScript 模块化规范 浏览器环境 AMD Asynchronous Module Definition RequireJS CMD Com...

48210
来自专栏不想当开发的产品不是好测试

centos下安装python3

前言 本文操作基本参考下面的文章,因此直说几个关键点,大家阅读这篇文章的话,跳转到原作者那里去吧 原作者: ehlxr 原文链接: https://ehlxr....

44110
来自专栏JMCui

Linux 常用性能工具简介.

一、wget 文件下载 使用wget下载单个文件:wget URL 下载并以不同的文件名保存:wget -O wordpress.zip URL wget限速下...

6025
来自专栏CodeSheep的技术分享

为Hexo博客添加LiveRe评论系统

1823
来自专栏架构师之路

网页端收消息,究竟是推还是拉?

抛开这些技术细节不谈,暂且认为服务端对每一个用户都有一个“待收消息”的队列,里面存放了需要给这个用户的一切消息。

1142
来自专栏前端技术总结

基于iframe的跨域与更新父窗体地址栏的解决方案

管理平台前端页面需要在当前前端框架结构基础上,在顶级导航中增加两个模块:首页、运维管理模块,以此接入运维平台提供的页面。在访问到内部某个页面后,希望父窗体的地...

2.1K135
来自专栏C/C++基础

error: '[class name]' does not name a type

从命令中可以看出,我是对源文件tc_mysql.cpp进行编译。但是却始终报如下错误:

711

扫码关注云+社区