Ajax内容爬取
什么是 Ajax?
Ajax,全称为 Asynchronous JavaScript and XML,即异步的 JavaScript 和 XML。它不是一门编程语言,而是利用 JavaScript 在保证页面不被刷新、页面链接不改变的情况下与服务器交换数据并更新部分网页的技术。
对于传统的网页,如果想更新其内容,那么必须刷新整个页面,但有了 Ajax,便可以在页面不被全部刷新的情况下更新其内容。在这个过程中,页面实际上是在后台与服务器进行了数据交互,获取到数据之后,再利用 JavaScript 改变网页,这样网页内容就会更新了。
可以到 W3School 上体验几个示例感受一下:http://www.w3school.com.cn/ajax/ajax_xmlhttprequest_send.asp。
1. 实例引入
浏览网页的时候,我们会发现很多网页都有下滑查看更多的选项。比如,拿微博来说,以我的主页为例:https://m.weibo.cn/u/2830678474,切换到微博页面,一直下滑,可以发现下滑几个微博之后,再向下就没有了,转而会出现一个加载的动画,不一会儿下方就继续出现了新的微博内容,这个过程其实就是 Ajax 加载的过程,如图所示。
我们注意到页面其实并没有整个刷新,也就意味着页面的链接没有变化,但是网页中却多了新内容,也就是后面刷出来的新微博。这就是通过 Ajax 获取新数据并呈现的过程。
2. 基本原理
初步了解了 Ajax 之后,我们再来详细了解它的基本原理。发送 Ajax 请求到网页更新的这个过程可以简单分为以下 3 步:
- 发送请求
- 解析内容
- 渲染网页
下面我们分别来详细介绍一下这几个过程。
发送请求
我们知道 JavaScript 可以实现页面的各种交互功能,Ajax 也不例外,它也是由 JavaScript 实现的,
var xmlhttp; if (window.XMLHttpRequest) { //code for IE7+, Firefox, Chrome, Opera, Safari xmlhttp = new XMLHttpRequest(); } else { //code for IE6, IE5 xmlhttp = new ActiveXObject(“Microsoft.XMLHTTP”); } xmlhttp.onreadystatechange = function () { if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { document.getElementById(“myDiv”).innerHTML = xmlhttp.responseText; } }; xmlhttp.open(“POST”, “/ajax/“, true); xmlhttp.send(); |
这是 JavaScript 对 Ajax 最底层的实现,实际上就是新建了 XMLHttpRequest
对象,然后调用 onreadystatechange
属性设置了监听,然后调用 open
和 send
方法向某个链接(也就是服务器)发送了请求。前面用 Python 实现请求发送之后,可以得到响应结果,但这里请求的发送变成 JavaScript 来完成。由于设置了监听,所以当服务器返回响应时,onreadystatechange
对应的方法便会被触发,然后在这个方法里面解析响应内容即可。
解析内容
得到响应之后,onreadystatechange
属性对应的方法便会被触发,此时利用 xmlhttp
的 responseText
属性便可取到响应内容。这类似于 Python 中利用 requests 向服务器发起请求,然后得到响应的过程。那么返回内容可能是 HTML,可能是 JSON,接下来只需要在方法中用 JavaScript 进一步处理即可。比如,如果是 JSON 的话,可以进行解析和转化。
渲染网页
JavaScript 有改变网页内容的能力,解析完响应内容之后,就可以调用 JavaScript 来针对解析完的内容对网页进行下一步处理了。比如,通过 document.getElementById().innerHTML
这样的操作,便可以对某个元素内的源代码进行更改,这样网页显示的内容就改变了,这样的操作也被称作 DOM 操作,即对网页文档进行操作,如更改、删除等。
上例中,document.getElementById("myDiv").innerHTML=xmlhttp.responseText
便将 ID 为 myDiv
的节点内部的 HTML 代码更改为服务器返回的内容,这样 myDiv
元素内部便会呈现出服务器返回的新数据,网页的部分内容看上去就更新了。
我们观察到,这 3 个步骤其实都是由 JavaScript 完成的,它完成了整个请求、解析和渲染的过程。
再回想微博的下拉刷新,这其实就是 JavaScript 向服务器发送了一个 Ajax 请求,然后获取新的微博数据,将其解析,并将其渲染在网页中。
因此,我们知道,真实的数据其实都是一次次 Ajax 请求得到的,如果想要抓取这些数据,需要知道这些请求到底是怎么发送的,发往哪里,发了哪些参数。如果我们知道了这些,不就可以用 Python 模拟这个发送操作,获取到其中的结果了吗?
Ajax分析方法
这里还以前面的微博为例,我们知道拖动刷新的内容由 Ajax 加载,而且页面的 URL 没有变化,那么应该到哪里去查看这些 Ajax 请求呢?
1. 分析案例
这里还需要借助浏览器的开发者工具,下面以 Chrome 浏览器为例来介绍。
首先,用 Chrome 浏览器打开微博的链接 https://m.weibo.cn/u/2830678474,随后在页面中点击鼠标右键,从弹出的快捷菜单中选择,随后在页面中点击鼠标右键,从弹出的快捷菜单中选择) “检查” 选项,此时便会弹出开发者工具,如图所示:
前面也提到过,这里其实就是在页面加载过程中浏览器与服务器之间发送请求和接收响应的所有记录。
Ajax 其实有其特殊的请求类型,它叫作 xhr。在图中我们可以发现一个名称以 getIndex 开头的请求,其 Type 为 xhr,这就是一个 Ajax 请求。用鼠标点击这个请求,可以查看这个请求的详细信息。
在右侧可以观察到其 Request Headers、URL 和 Response Headers 等信息。其中 Request Headers 中有一个信息为 X-Requested-With:XMLHttpRequest
,这就标记了此请求是 Ajax 请求,如图所示:
随后点击一下 Preview,即可看到响应的内容,它是 JSON 格式的。这里 Chrome 为我们自动做了解析,点击箭头即可展开和收起相应内容。
观察可以发现,这里的返回结果是我的个人信息,如昵称、简介、头像等,这也是用来渲染个人主页所使用的数据。JavaScript 接收到这些数据之后,再执行相应的渲染方法,整个页面就渲染出来了。
另外,也可以切换到 Response 选项卡,从中观察到真实的返回数据,如图所示:
接下来,切回到第一个请求,观察一下它的 Response 是什么,如图所示:
这是最原始的链接 https://m.weibo.cn/u/2830678474 返回的结果,其代码只有不到 50 行,结构也非常简单,只是执行了一些 JavaScript。
所以说,我们看到的微博页面的真实数据并不是最原始的页面返回的,而是后来执行 JavaScript 后再次向后台发送了 Ajax 请求,浏览器拿到数据后再进一步渲染出来的。
2. 过滤请求
接下来,再利用 Chrome 开发者工具的筛选功能筛选出所有的 Ajax 请求。在请求的上方有一层筛选栏,直接点击 XHR,此时在下方显示的所有请求便都是 Ajax 请求了,如图所示:
接下来,不断滑动页面,可以看到页面底部有一条条新的微博被刷出,而开发者工具下方也一个个地出现 Ajax 请求,这样我们就可以捕获到所有的 Ajax 请求了。
随意点开一个条目,都可以清楚地看到其 Request URL、Request Headers、Response Headers、Response Body 等内容,此时想要模拟请求和提取就非常简单了。
下图所示的内容便是我的某一页微博的列表信息:
到现在为止,我们已经可以分析出 Ajax 请求的一些详细信息了,接下来只需要用程序模拟这些 Ajax 请求,就可以轻松提取我们所需要的信息了。
Ajax实战
在上一节中我们已经学习了 Ajax 的基本原理和分析方法,这一节我们来结合一个实际的案例来看一下 Ajax 分析和爬取页面的具体实现。
1. 准备工作
在本节开始之前,我们需要做好如下准备工作:
- 安装好 Python 3(最低为 3.6 版本),并成功运行 Python 3 程序。
- 了解 Python HTTP 请求库 requests 的基本用法。
- 了解 Ajax 基础知识和分析 Ajax 的基本方法。
以上内容在前面的章节中均有讲解,如尚未准备好,建议先熟悉一下这些内容。
2. 爬取目标
本节我们以一个示例网站来试验一下 Ajax 的爬取,其链接为:https://spa1.scrape.center/,该示例网站的数据请求是通过 Ajax 完成的,页面的内容是通过 JavaScript 渲染出来的,页面如图所示:
可能大家看着这个页面似曾相识,心想这不就是上一个案例的网站吗?但其实不是。这个网站的后台实现逻辑和数据加载方式完全不同。只不过最后呈现的样式是一样的。
这个网站同样支持翻页,可以点击最下方的页码来切换到下一页,如图所示:
点击每一个电影的链接进入详情页,页面结构也是完全一样的,如图所示:
我们需要爬取的数据也是和原来相同的,包括电影的名称、封面、类别、上映日期、评分、剧情简介等信息。
本节中我们需要完成的目标如下。
- 分析页面数据的加载逻辑。
- 用 requests 实现 Ajax 数据的爬取。
- 将每部电影的数据保存成一个 JSON 数据文件。
由于本节主要讲解 Ajax,所以对于数据存储和加速部分就不再展开详细实现,主要是讲解 Ajax 的分析和爬取实现。
好,我们现在就开始吧。
3. 初步探索
首先,我们先尝试用之前的 requests 来直接提取页面,看看会得到怎样的结果。用最简单的代码实现一下 requests 获取首页源码的过程,代码如下:
import requests url = ‘https://spa1.scrape.center/‘ html = requests.get(url).text print(html) |
运行结果如下:
可以看到,爬取结果就只有这么一点 HTML 内容,而我们在浏览器中打开这个页面,却能看到如图所示的结果:
在 HTML 中,我们只能看到在源码中引用了一些 JavaScript 和 CSS 文件,并没有观察到有任何电影数据信息。
如果遇到这样的情况,这说明我们现在看到的整个页面便是 JavaScript 渲染得到的,浏览器执行了 HTML 中所引用的 JavaScript 文件,JavaScript 通过调用一些数据加载和页面渲染方法,才最终呈现了图中所示的结果。
在一般情况下,这些数据都是通过 Ajax 来加载的, JavaScript 在后台调用这些 Ajax 数据接口,得到数据之后,再把数据进行解析并渲染呈现出来,得到最终的页面。所以说,要想爬取这个页面,我们可以直接爬取 Ajax 接口获取数据就好了。
在上一节中,我们已经了解了 Ajax 分析的基本方法,下面我们就来分析一下 Ajax 接口的逻辑并实现数据爬取吧。
4. 爬取列表页
首先我们来分析一下列表页的 Ajax 接口逻辑,打开浏览器开发者工具,切换到 Network 面板,勾选上 Preserve Log 并切换到 XHR 选项卡,如图所示:
接着重新刷新页面,再点击第二页、第三页、第四页的按钮,这时候可以观察到页面上的数据发生了变化,同时开发者工具下方就监听到了几个 Ajax 请求,如图所示:
由于我们切换了 4 页,每次翻页也出现了对应的 Ajax 请求,我们可以点击查看其请求详情。观察其请求的 URL 和参数以及响应内容是怎样的,如图所示。
这里我们点开了最后个结果,观察到其 Ajax 接口请求的 URL 地址为:https://spa1.scrape.center/api/movie/?limit=10&offset=40,这里有两个参数,一个是 limit
,这里是 10;一个是 offset
,这里也是 40。
通过多个 Ajax 接口的参数,我们可以观察到这么一个规律:limit
一直为 10,这就正好对应着每页 10 条数据;offset
在依次变大,页面每加 1 页,offset
就加 10,这就代表着页面的数据偏移量,比如第二页的 offset
为 10 则代表着跳过 10 条数据,返回从 11 条数据开始的结果,再加上 limit
的限制,那就是第 11 条至第 20 条数据的结果。
接着我们再观察一下响应的数据,切换到 Preview 选项卡,结果如图所示:
可以看到,结果就是一些 JSON 数据,它有一个 results
字段,是一个列表,列表中每一个元素都是一个字典。观察一下字典的内容,这里我们正好可以看到有对应的电影数据的字段了,如 name
、alias
、cover
、categories
,对比下浏览器中的真实数据,各个内容完全一致,而且这个数据已经非常结构化了,完全就是我们想要爬取的数据,真的是得来全不费工夫。
这样的话,我们只需要把所有页面的 Ajax 接口构造出来,所有列表页的数据我们都可以轻松获取到了。
我们先定义一些准备工作,导入一些所需的库并定义一些配置,代码如下:
import requests import logging logging.basicConfig(level=logging.INFO, format=’%(asctime)s - %(levelname)s: %(message)s’) INDEX_URL = ‘https://spa1.scrape.center/api/movie/?limit={limit}&offset={offset}‘ |
这里我们引入了 requests 和 logging 库,并定义了 logging 的基本配置,接着我们定义了 INDEX_URL
,这里把 limit
和 offset
预留出来了变成了占位符,可以动态传入参数构造一个完整的列表页 URL。
下面我们来实现一下详情页的爬取。还是和原来一样,我们先定义一个通用的爬取方法,其代码如下:
def scrape_api(url): logging.info(‘scraping %s…’, url) try: response = requests.get(url) if response.status_code == 200: return response.json() logging.error(‘get invalid status code %s while scraping %s’, response.status_code, url) except requests.RequestException: logging.error(‘error occurred while scraping %s’, url, exc_info=True) |
这里我们定义了一个 scrape_api
方法,和之前不同的是,这个方法专门用来处理 JSON 接口,最后的 response
调用的是 json
方法,它可以解析响应的内容并将其转化成 JSON 字符串。
接着在这个基础之上,我们定义一个爬取列表页的方法,其代码如下:
LIMIT = 10 def scrape_index(page): url = INDEX_URL.format(limit=LIMIT, offset=LIMIT * (page - 1)) return scrape_api(url) |
这里我们定义了一个 scrape_index
方法,它接收一个参数 page
,该参数代表列表页的页码。
这里我们先构造了一个 url
,通过字符串的 format
方法,传入 limit
和 offset
的值。这里 limit
就直接使用了全局变量 LIMIT
的值;offset
则是动态计算的,就是页码数减一再乘以 limit
,比如第一页 offset
就是 0,第二页 offset
就是 10,以此类推。构造好了 url
之后,直接调用 scrape_api
方法并返回结果即可。
这样我们就完成了列表页的爬取,每次请求都会得到一页 10 部的电影数据。
由于这时爬取到的数据已经是 JSON 类型了,所以我们不用像之前那样去解析 HTML 代码来提取数据了,爬到的数据就是我们想要的结构化数据,因此解析这一步就可以直接省略啦。
到此为止,我们能成功爬取列表页并提取出电影列表信息了。
5. 爬取详情页
这时候我们已经可以拿到每一页的电影数据了,但是看看这些数据实际上还缺少了一些我们想要的信息,如剧情简介等信息,所以需要进一步进入到详情页来获取这些内容。
这时候点击任意一部电影,如《教父》,进入其详情页,这时可以发现页面的 URL 已经变成了 https://spa1.scrape.center/detail/40,页面也成功展示了详情页的信息,如图所示:
另外,我们也可以观察到在开发者工具中又出现了一个 Ajax 请求,其 URL 为 https://spa1.scrape.center/api/movie/40/,通过 Preview 选项卡也能看到 Ajax 请求对应响应的信息,如图 所示。
稍加观察就可以发现,Ajax 请求的 URL 后面有一个参数是可变的,这个参数就是电影的 id
,这里是 40,对应《教父》这部电影。
如果我们想要获取 id
为 50 的电影,只需要把 URL 最后的参数改成 50 即可,即 https://spa1.scrape.center/api/movie/50/,请求这个新的 URL 我们就能获取 id
为 50 的电影所对应的数据了。
同样,响应结果也是结构化的 JSON 数据,字段也非常规整,我们直接爬取即可。
现在分析好了详情页的数据提取逻辑,那么怎么和列表页关联起来呢?这个 id
哪里来呢?我们回过头来再看看列表页的接口返回数据,如图所示。
可以看到,列表页原本的返回数据就带了 id
这个字段,所以我们只需要拿列表页结果中的 id
来构造详情页的 Ajax 请求的 URL 就好了。
接着,我们就先定义一个详情页的爬取逻辑,代码如下:
DETAIL_URL = ‘https://spa1.scrape.center/api/movie/{id}‘ def scrape_detail(id): url = DETAIL_URL.format(id=id) return scrape_api(url) |
这里我们定义了一个 scrape_detail
方法,它接收一个参数 id
。这里的实现也非常简单,先根据定义好的 DETAIL_URL
加 id
构造一个真实的详情页 Ajax 请求的 URL,然后直接调用 scrape_api
方法传入这个 url
即可。
接着,我们定义一个总的调用方法,将以上方法串联调用起来,代码如下:
TOTAL_PAGE = 10 def main(): for page in range(1, TOTAL_PAGE + 1): index_data = scrape_index(page) for item in index_data.get(‘results’): id = item.get(‘id’) detail_data = scrape_detail(id) logging.info(‘detail data %s’, detail_data) if name == ‘main‘: main() |
这里我们定义了一个 main
方法,首先遍历获取了页码 page
,然后把 page
当参数传递给了 scrape_index
方法,得到列表页的数据。接着我们遍历每个列表页的每个结果,获取到每部电影的 id
,然后把 id
当作参数传递给 scrape_detail
方法来爬取每部电影的详情数据,并将其赋值为 detail_data
,输出即可。
运行结果如下:
2020-03-19 02:51:55,981 - INFO: scraping https://spa1.scrape.center/api/movie/?limit=10&offset=0… 2020-03-19 02:51:56,446 - INFO: scraping https://spa1.scrape.center/api/movie/1… 2020-03-19 02:51:56,638 - INFO: detail data {‘id’: 1, ‘name’: ‘霸王别姬’, ‘alias’: ‘Farewell My Concubine’, ‘cover’: ‘https://p0.meituan.net/movie/ce4da3e03e655b5b88ed31b5cd7896cf62472.jpg@464w_644h_1e_1c‘, ‘categories’: [‘剧情’, ‘爱情’], ‘regions’: [‘中国大陆’, ‘中国香港’], ‘actors’: [{‘name’: ‘张国荣’, ‘role’: ‘程蝶衣’, …}, …], ‘directors’: [{‘name’: ‘陈凯歌’, ‘image’: ‘https://p0.meituan.net/movie/8f9372252050095067e0e8d58ef3d939156407.jpg@128w_170h_1e_1c'}], ‘score’: 9.5, ‘rank’: 1, ‘minute’: 171, ‘drama’: ‘影片借一出《霸王别姬》的京戏,牵扯出三个人之间一段随时代风云变幻的爱恨情仇。段小楼(张丰毅 饰)与程蝶衣(张国荣 饰)是一对打小一起长大的师兄弟,…’, ‘photos’: […], ‘published_at’: ‘1993-07-26’, ‘updated_at’: ‘2020-03-07T16:31:36.967843Z’} 2020-03-19 02:51:56,640 - INFO: scraping https://spa1.scrape.center/api/movie/2… 2020-03-19 02:51:56,813 - INFO: detail data {‘id’: 2, ‘name’: ‘这个杀手不太冷’, ‘alias’: ‘Léon’, ‘cover’: ‘https://p1.meituan.net/movie/6bea9af4524dfbd0b668eaa7e187c3df767253.jpg@464w_644h_1e_1c‘, ‘categories’: [‘剧情’, ‘动作’, ‘犯罪’], ‘regions’: [‘法国’], ‘actors’: [{‘name’: ‘让·雷诺’, ‘role’: ‘莱昂 Leon’, …}, …], ‘directors’: [{‘name’: ‘吕克·贝松’, ‘image’: ‘https://p0.meituan.net/movie/0e7d67e343bd3372a714093e8340028d40496.jpg@128w_170h_1e_1c'}], ‘score’: 9.5, ‘rank’: 3, ‘minute’: 110, ‘drama’: ‘里昂(让·雷诺 饰)是名孤独的职业杀手,受人雇佣。一天,邻居家小姑娘马蒂尔德(纳塔丽·波特曼 饰)敲开他的房门,要求在他那里暂避杀身之祸。…’, ‘photos’: […], ‘published_at’: ‘1994-09-14’, ‘updated_at’: ‘2020-03-07T16:31:43.826235Z’} … |
由于内容较多,这里省略了部分内容。
可以看到,其实整个爬取工作就已经完成了,这里会顺次爬取每一页列表页 Ajax 接口,然后去顺次爬取每部电影的详情页 Ajax 接口,打印出每部电影的 Ajax 接口响应数据,而且都是 JSON 格式。这样,所有电影的详情数据都会被我们爬取到啦。
6. 保存数据
好,成功提取到详情页信息之后,我们下一步就要把数据保存起来了。在前面我们学习了 MongoDB 的相关操作,接下来我们就把数据保存到 MongoDB 吧。
在这之前,请确保现在有一个可以正常连接和使用的 MongoDB 数据库,这里我就以本地 localhost 的 M 哦能够 DB 数据库为例来进行操作,其运行在 27017 端口上,无用户名和密码。
将数据导入 MongoDB 需要用到 PyMongo 这个库。接下来我们把它们引入一下,然后同时定义一下 MongoDB 的连接配置,实现如下:
MONGO_CONNECTION_STRING = ‘mongodb://localhost:27017’ MONGO_DB_NAME = ‘movies’ MONGO_COLLECTION_NAME = ‘movies’ import pymongo client = pymongo.MongoClient(MONGO_CONNECTION_STRING) db = client[‘movies’] collection = db[‘movies’] |
在这里我们声明了几个变量,介绍如下:
- MONGO_CONNECTION_STRING:MongoDB 的连接字符串,里面定义了 MongoDB 的基本连接信息,如 host、port,还可以定义用户名密码等内容。
- MONGO_DB_NAME:MongoDB 数据库的名称。
- MONGO_COLLECTION_NAME:MongoDB 的集合名称。
这里我们用 MongoClient 声明了一个连接对象,然后依次声明了存储的数据库和集合。
接下来,我们再实现一个将数据保存到 MongoDB 的方法,实现如下:
def save_data(data): collection.update_one({ ‘name’: data.get(‘name’) }, { ‘$set’: data }, upsert=True) |
在这里我们声明了一个 save_data 方法,它接收一个 data 参数,也就是我们刚才提取的电影详情信息。在方法里面,我们调用了 update_one 方法,第一个参数是查询条件,即根据 name 进行查询;第二个参数就是 data 对象本身,就是所有的数据,这里我们用 $set
操作符表示更新操作;第三个参数很关键,这里实际上是 upsert 参数,如果把这个设置为 True,则可以做到存在即更新,不存在即插入的功能,更新会根据第一个参数设置的 name 字段,所以这样可以防止数据库中出现同名的电影数据。
注:实际上电影可能有同名,但该场景下的爬取数据没有同名情况,当然这里更重要的是实现 MongoDB 的去重操作。
好的,那么接下来 main 方法稍微改写一下就好了,改写如下:
def main(): for page in range(1, TOTAL_PAGE + 1): index_data = scrape_index(page) for item in index_data.get(‘results’): id = item.get(‘id’) detail_data = scrape_detail(id) logging.info(‘detail data %s’, detail_data) save_data(detail_data) logging.info(‘data saved successfully’) |
这里就是加了 save_data 方法的调用,并加了一些日志信息。
重新运行,我们看下输出结果:
2020-03-19 02:51:06,323 - INFO: scraping https://spa1.scrape.center/api/movie/?limit=10&offset=0… 2020-03-19 02:51:06,440 - INFO: scraping https://spa1.scrape.center/api/movie/1… 2020-03-19 02:51:06,551 - INFO: detail data {‘id’: 1, ‘name’: ‘霸王别姬’, ‘alias’: ‘Farewell My Concubine’, ‘cover’: ‘https://p0.meituan.net/movie/ce4da3e03e655b5b88ed31b5cd7896cf62472.jpg@464w_644h_1e_1c‘, ‘categories’: [‘剧情’, ‘爱情’], ‘regions’: [‘中国大陆’, ‘中国香港’], ‘actors’: [{‘name’: ‘张国荣’, ‘role’: ‘程蝶衣’, ‘image’: ‘https://p0.meituan.net/movie/5de69a492dcbd3f4b014503d4e95d46c28837.jpg@128w_170h_1e_1c'}, …, {‘name’: ‘方征’, ‘role’: ‘嫖客’, ‘image’: ‘https://p1.meituan.net/movie/39687137b23bc9727b47fd24bdcc579b97618.jpg@128w_170h_1e_1c'}], ‘directors’: [{‘name’: ‘陈凯歌’, ‘image’: ‘https://p0.meituan.net/movie/8f9372252050095067e0e8d58ef3d939156407.jpg@128w_170h_1e_1c'}], ‘score’: 9.5, ‘rank’: 1, ‘minute’: 171, ‘drama’: ‘影片借一出《霸王别姬》的京戏,牵扯出三个人之间一段随时代风云变幻的爱恨情仇。段小楼(张丰毅 饰)与程蝶衣(张国荣 饰)是一对打小一起长大的师兄弟,两人一个演生,一个饰旦,一向配合天衣无缝,尤其一出《霸王别姬》,更是誉满京城,为此,两人约定合演一辈子《霸王别姬》。但两人对戏剧与人生关系的理解有本质不同,段小楼深知戏非人生,程蝶衣则是人戏不分。段小楼在认为该成家立业之时迎娶了名妓菊仙(巩俐 饰),致使程蝶衣认定菊仙是可耻的第三者,使段小楼做了叛徒,自此,三人围绕一出《霸王别姬》生出的爱恨情仇战开始随着时代风云的变迁不断升级,终酿成悲剧。’, ‘photos’: [‘https://p0.meituan.net/movie/45be438368bb291e501dc523092f0ac8193424.jpg@106w_106h_1e_1c‘, …, ‘https://p0.meituan.net/movie/0d952107429db3029b64bf4f25bd762661696.jpg@106w_106h_1e_1c'], ‘published_at’: ‘1993-07-26’, ‘updated_at’: ‘2020-03-07T16:31:36.967843Z’} 2020-03-19 02:51:06,583 - INFO: data saved successfully 2020-03-19 02:51:06,583 - INFO: scraping https://spa1.scrape.center/api/movie/2… |
由于输出内容较多,这里省略了部分内容。
我们可以看到这里我们成功爬取到了数据,并且提示了数据存储成功的信息,没有任何报错信息。
接下来我们使用 Robo 3T 连接 MongoDB 数据库看下爬取的结果,由于我使用的是本地的 MongoDB,所以在 Robo 3T 里面我直接输入 localhost 的连接信息即可,这里请替换成自己的 MongoDB 连接信息,如图所示:
连接之后我们便可以在 movies 这个数据库,movies 这个集合下看到我们刚才爬取的数据了,如图所示:
可以看到数据就是以 JSON 格式存储的,一条数据就对应一部电影的信息,各种嵌套信息也一目了然,同时第三列还有数据类型标识。
这样就证明我们的数据就成功存储到 MongoDB 里了。
7. 总结
本节中我们通过一个案例来体会了 Ajax 分析和爬取的基本流程,希望大家通过本节能够更加熟悉 Ajax 的分析和爬取实现。
另外,我们也观察到,由于 Ajax 接口大部分返回的是 JSON 数据,所以在一定程度上可以避免一些数据提取的工作,这也在一定程度上减轻了工作量。
本节代码:https://github.com/Python3WebSpider/ScrapeSpa1。
# 经典动态渲染工具 Selenium 的使用
前面我们讲解了 Ajax 的分析方法,利用 Ajax 接口我们可以非常方便地完成数据爬取。只要我们能找到 Ajax 接口的规律,就可以通过某些参数构造出对应的请求,数据自然就能轻松爬取到。
但是在很多情况下,一些 Ajax 请求的接口通常会包含加密参数,如 token
、sign
等,如:https://spa2.scrape.center/,它的 Ajax 接口是包含一个 token
参数的,如图所示。
由于请求接口时必须加上 token
参数,所以我们如果不深入分析找到 token
的构造逻辑,是难以直接模拟这些 Ajax 请求的。
此时解决方法通常有两种:一种就是深挖其中的逻辑,把其中 token
的构造逻辑完全找出来,再用 Python 复现,构造 Ajax 请求;另外一种方法就是直接通过模拟浏览器的方式来绕过这个过程,因为在浏览器里我们可以看到这个数据,如果能把看到的数据直接爬取下来,当然也就能获取对应的信息了。
由于第一种方法难度较高,这里我们就先介绍第二种方法:模拟浏览器爬取。
这里使用的工具为 Selenium,这里就来先了解一下 Selenium 的基本使用方法。
Selenium 是一个自动化测试工具,利用它可以驱动浏览器执行特定的动作,如点击、下拉等操作,同时还可以获取浏览器当前呈现的页面的源代码,做到可见即可爬。对于一些 JavaScript 动态渲染的页面来说,此种抓取方式非常有效。本节中,就让我们来感受一下它的强大之处吧。
1. 准备工作
本节以 Chrome 为例来讲解 Selenium 的用法。在开始之前,请确保已经正确安装好了 Chrome 浏览器并配置好了 ChromeDriver。另外,还需要正确安装好 Python 的 Selenium 库。
安装方法可以参考:https://setup.scrape.center/selenium,全部配置完成之后,我们便可以开始本节的学习了。
2. 基本用法
准备工作做好之后,首先来大体看一下 Selenium 的功能。示例如下:
from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.wait import WebDriverWait browser = webdriver.Chrome() try: browser.get(‘https://www.baidu.com‘) input = browser.find_element_by_id(‘kw’) input.send_keys(‘Python’) input.send_keys(Keys.ENTER) wait = WebDriverWait(browser, 10) wait.until(EC.presence_of_element_located((By.ID, ‘content_left’))) print(browser.current_url) print(browser.get_cookies()) print(browser.page_source) finally: browser.close() |
运行代码后发现,会自动弹出一个 Chrome 浏览器。浏览器首先会跳转到百度,然后在搜索框中输入 Python,接着跳转到搜索结果页,如图所示。
此时在控制台的输出结果如下:
https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=0&rsv_idx=1&tn=baidu&wd=Python&rsv_pq=c94d0df9000a72d0&rsv_t=07099xvun1ZmC0bf6eQvygJ43IUTTUOl5FCJVPgwG2YREs70GplJjH2F%2BCQ&rqlang=cn&rsv_enter=1&rsv_sug3=6&rsv_sug2=0&inputT=87&rsv_sug4=87 [{‘secure’: False, ‘value’: ‘B490B5EBF6F3CD402E515D22BCDA1598’, ‘domain’: ‘.baidu.com’, ‘path’: ‘/‘, ‘httpOnly’: False, ‘name’: ‘BDORZ’, ‘expiry’: 1491688071.707553}, {‘secure’: False, ‘value’: ‘22473_1441_21084_17001’, ‘domain’: ‘.baidu.com’, ‘path’: ‘/‘, ‘httpOnly’: False, ‘name’: ‘H_PS_PSSID’}, {‘secure’: False, ‘value’: ‘12883875381399993259_00_0_I_R_2_0303_C02F_N_I_I_0’, ‘domain’: ‘.www.baidu.com‘, ‘path’: ‘/‘, ‘httpOnly’: False, ‘name’: ‘__bsi’, ‘expiry’: 1491601676.69722}] … |