Python3网络爬虫宝典
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.4 爬虫工程师常用的库

通过图1-3我们了解到,爬虫程序的完整链条包括整理需求、分析目标、发出网络请求、文本解析、数据入库和数据出库。其中与代码紧密相关的有:发出网络请求、文本解析、数据入库和数据出库,接下来我们将学习不同阶段中爬虫工程师常用的库。

我们没有必要学习具备相同功能的各种各样的库,只需要选择其中使用者较多或比较称手的即可。例如,网页文本解析库有BeautifulSoup、Parsel和HTMLParser,但我们只需要学习Parsel就够了,这就像学习如何驾驶汽车时你不需要学习同类型车辆的驾驶方法一样。

1.4.1 网络请求库

网络请求是爬虫程序的开始,也是爬虫程序的重要组成部分之一。在代码片段1-1中,我们使用的是Python内置的urllib模块中request对象里的urlopen()方法。其实代码片段1-1中的代码已经非常简洁了,但持有“人生苦短”观念的Python工程师认为我们需要用更简单且编码速度更快的方法,所以他们创造了Requests库和Aiohttp库,知名的爬虫框架Scrapy也是这么诞生的。

1.4.1-1 网络请求库Requests

网络请求库Requests是Python系爬虫工程师用得最多的库,这个库以简单、易用和稳定而驰名。我们可以通过Python的包管理工具安装Requests库,对应命令如下:

img

安装完成后先来一次简单的HTTP GET请求体验。向电子工业出版社官网的出版社简介页面发出请求的代码如下:

img

网络请求发出以后,所有的响应信息都会赋值给response对象,例如响应状态码、响应正文、响应头等。当我们需要读取这些信息时,可以通过“.”符号访问对应的对象,例如:

img

代码运行结果如下:

img

由于响应正文内容过长,这里只打印响应状态码和响应头。运行结果中的200是本次请求的响应状态码,后面的信息是本次请求的响应头。

我们可以用Requests库改造代码片段1-1,将原来的urllib.request.urlopen()换成requests.get()。代码片段1-2为改动后的代码。

代码片段1-2

img

当我们需要发出HTTP POST请求时,只需要将requests.get()换成requests.post()即可,例如:

img

当我们希望伪造请求头欺骗服务端的校验措施时,可以通过自定义请求头的方式达到目的。例如,将请求头中的User-Agent伪造成与Chrome浏览器相同的标识,对应的代码如下:

img

有些请求会要求客户端携带参数,试想在登录场景中,发出HTTP POST请求时肯定会将用户名和密码一并发送给服务端。也就是说,我们需要构造请求正文并在发出网络请求时一并提交,对应的代码如下:

img

当网络状况不好的时候,我们常常会遇到网页无法加载的情况,在代码层面通常表现为请求发出后迟迟得不到响应。这时候我们想拥有一个超时功能,即超过N秒后放弃本次对服务端响应的等待,例如下面的代码:

img

请求发出后,0.8秒未收到响应信息则放弃等待,同时引发一个异常。

除了上面介绍的这些功能外,Requests库还有更细致和复杂的功能,例如Cookie的获取和设置、保持长链接、处理重定向、与HTTPS相关的证书事宜、身份认证和代理等。书里不会逐个介绍这些功能的应用场景和用法,当你在需要用到的时候去翻阅Requests库的文档并按照文档示例编写代码即可。

1.4.1-2 异步的网络请求库Aiohttp

自从协程这一概念被引入到Python,并且Python官方正式支持async、await关键字后,Python的协程就开始了蓬勃的发展。协程在I/O密集的应用中有着多线程和多进程难以望其项背的速度优势,这一特性也是它深受爬虫工程师喜爱的原因。

首先我们来了解什么是同步,什么是异步。从消息通信机制角度来看,同步指的是一次调用发生后,等待结果返回后才会进入下一次调用,如果不返回结果则一直处于等待状态,代码不会向下执行。代码片段1-3是我们平时编写的代码,第1行和第2行代码导入time模块和datetime模块下的datetime对象,第3行和第4行代码定义一个名为wait的方法,wait()方法中调用了time模块的sleep()方法占用5秒钟,第5~7行代码又做了哪些事呢?

(1)打印程序开始时间。

(2)调用wait()方法。

(3)打印程序结束时间。

代码片段1-3

img

代码运行结果如下:

img

图1-16描述了代码片段1-3运行时的顺序和时间关系。

img

图1-16 运行顺序和时间关系(1)

程序结束时间与程序开始时间相隔5秒钟,这正是调用wait()方法时被占用的5秒钟。

异步指的是一次调用发生后,不会等待结果返回就会立即进入下一次调用,无论结果什么时候返回,它都会立即向下执行。代码片段1-4是代码片段1-3的异步写法。

代码片段1-4

img
img

为了观察效果,在wait()方法中增加了打印“等我5秒钟”的代码。代码运行结果如下;

img

wait()方法被调用且执行了,但结束时间和开始时间之间并没有真的相差5秒钟,相隔的时间很短(毫秒)。就算在开始和结束之间调用多次wait()方法也一样,例如将main()中的wait()方法调用次数改为5次:

img

保存改动后运行代码,代码运行结果如下:

img
img

间隔时间依旧没有差异,但wait()方法真的执行了5次。异步代码运行时的顺序和时间关系如图1-17所示。

img

图1-17 运行顺序和时间关系(2)

由于不需要等待结果就会立即向下执行,所以print_time("开始")、5次wait()、print_time("结束")的调用时间间隔非常短(毫秒),不会出现图1-16中单次执行wait()耗时5秒的现象。

了解完同步和异步的差异后,我们来学习异步网络请求库的使用方法。爬虫工程师常接触到的、具备异步网络请求功能且支持async、await关键字的库分别是Aiohttp、Tornado和HTTPX。本节我们以Aiohttp为例,介绍异步网络请求库的基本功能和注意事项。

Aiohttp不仅具备完善的网络请求客户端功能,还支持网络服务端,也就是说我们可以用它打造一款Web应用。它既支持HTTP协议,又支持WebSocket协议,是爬虫工程师不可多得的趁手兵器。使用Python的包管理工具安装Aiohttp库的命令为:

img

Aiohttp官方文档给出的HTTP客户端示例代码如下:

img
img

这种写法与我们刚才编写的原生的Python异步代码语法接近,Aiohttp还利用异步上下文管理器实现了代码简化,真是非常用心了。这里用Aiohttp改造代码片段1-1,将原来的urllib.request.urlopen()换成async with aiohttp.ClientSession(),代码片段1-5为改动后的代码。

代码片段1-5

img
img

这样一来,程序的运行效率就会变得比之前高。图1-18描述了使用Requests库时的代码执行顺序。

img

图1-18 代码执行顺序(1)

在同步I/O的代码中,客户端的请求发起行为和服务端的返回响应行为交替进行。图1-19描述了使用Aiohttp库时的代码执行顺序。

img

图1-19 代码执行顺序(2)

客户端的请求发起行为和服务端的返回响应行为并非交替进行,因为异步请求不会等待服务端的响应,所以有可能出现客户端发出几次请求后服务端的响应还未返回的情况。

需要注意的是,代码片段1-5用到的文件读写语句with open是同步的I/O代码,异步的是网络I/O部分,而同步的是文件I/O部分。实际上这是很不好的,如果要使用异步来加快速度,那么就要避免在异步的流程中使用同步I/O代码。图1-20中从河西码头搬运箱子到河东码头的场景与异步I/O中掺杂同步I/O的情况相似。

img

图1-20 箱子搬运场景

假设河西码头有4名工人,每次可以搬运4个箱子,河东码头有10名工人,每次能接收10个箱子。但河西码头每次只运送1个箱子,等到河东码头确认收到箱子后才运送下一个箱子。这种情况下无论河西码头和河东码头有多少人手,码头的整体搬运能力都是1。解决这种问题的方法之一是不断地将河西码头的箱子运送到河东码头,无论河东码头是否收到箱子,都不用等待河东码头的确认消息。

Aiohttp库同样支持Cookie的获取和设置、处理重定向和代理等。本书不会逐个介绍这些功能的应用场景和用法,当你在需要用到的时候去翻阅Aiohttp库的文档并按照指引编写代码即可。

1.4.2 网页文本解析

监听到客户端发出的网络请求后,服务端会根据请求正文将资源返回给客户端,例如一份HTML文档或者JSON格式的数据。爬虫程序如何在响应正文中锁定关键内容并提取出来呢?对于较为规整的JSON数据,想要的内容可以通过类似get()这样的方法直接提取,但HTML文档就不同了。代码片段1-6是一份简单的HTML文档。

代码片段1-6

img
img

当我们用request.get()向指定的网址发出请求时,服务端很有可能将这样一个HTML文档响应给这次请求。假设我们需要从HTML文档中提取出文章标题、文章副标题、发布者、发布时间和文章正文,可选的解决方式有正则表达式和适用于HTML规则的文本解析库。

选用正则表达式的话,提取标题和副标题的写法比较简单:

img

代码运行结果为:

img

写法简单是因为标题和副标题的标签是唯一的,只需要将标签中的内容提取出来即可。但发布者、发布时间和文章正文包裹在div标签下的p标签和span标签中,这时候如果再用正则表达式去匹配就会变得很复杂,也极容易出错。面对这种情况,爬虫工程师们通常会选用专门的文本解析库,例如BeautifulSoup或Parsel。这里以Parsel库为例,演示如何使用专门的文本解析库提取HTML文档中的内容。

Parsel库支持CSS选择器语法,也支持XPATH路径查找语法,工程师可以根据自己的喜好选择语法。在开始之前,请用Python的包管理工具安装Parsel库,安装命令如下:

img

代码片段1-7是CSS选择器语法对应的提取代码。

代码片段1-7

img

代码片段1-7中的html代表的是代码片段1-6描述的HTML文本。代码运行结果为:

img

非常精准地锁定了内容所在的位置,并将内容提取出来。代码片段1-8是XPATH语法对应的提取代码。

代码片段1-8

img

代码片段1-8的运行结果与代码片段1-7的运行结果相同,说明都能够精准定位并提取内容。

Parsel库的效果这么好,它是如何根据我们编写的语法定位到指定的标签和对应的内容的呢?

实际上,Parsel库会将HTML文档中的标签转换为如图1-21所示的DOM节点,再根据CSS选择器语法或者XPATH语法定位指定的DOM节点,从而实现内容的提取。

img

图1-21 DOM节点(1)

通过CSS选择器语法定位发布者的代码为sel.css(".publisher::text"),程序会根据我们填写的.publisher和::text定位到内容为“今日新闻”的标签,图1-22描述了定位结果。

img

图1-22 DOM节点(2)

通过XPATH语法定位文章正文的代码为sel.xpath("//div[@class='content']/p/text()"),程序会根据我们填写的@class='content'、p和text定位到文章正文标签,图1-23描述了定位结果。

img

图1-23 DOM节点(3)

CSS选择器语法和XPATH路径查找语法对HTML文档DOM节点的定位十分精准,并且两种语法都非常简单易用,我们一起来学习吧!

1.4.2-1 CSS选择器语法

表1-1列出了CSS选择器语法及其示例描述。

表1-1 CSS选择器语法及其示例描述

img

续表

img

接下来我们通过两个例子加深对CSS选择器语法的理解。选择器语法:nth-child(n)可以用于定位文章正文中指定的段落。已知代码片段1-6的文章正文中有5个段落,那么定位到第一个段落的选择器写法为 sel.css(".content p:nth-child(1)")。也可以用:first-child定位第一个段落,写法为sel.css(".content p:first-child")。

我们可以借助浏览器开发者工具的Elements面板来确认我们的推测。唤起浏览器开发者工具后切换到Elements面板,并按快捷键Ctrl+F唤起搜索框,然后在搜索框中键入.content p:nth-child(1),此时Elements面板如图1-24所示。

img

图1-24 Elements面板(1)

在搜索框中键入.content p:first-child,此时Elements面板如图1-25所示。

img

图1-25 Elements面板(2)

再来看另外一个例子。文章发布者所在标签的class属性为publisher,文章发布时间所在标签的class属性为pubTime,选择器语法 [attribute^=value] 可以同时定位到这两个标签,对应写法为sel.css("span[class^="pub"]")。在搜索框中键入span[class^="pub"],此时Elements面板如图1-26所示。

img

图1-26 Elements面板(3)

动手试一试,是否感觉CSS选择器语法挺简单的呢?

1.4.2-2 XPATH路径查找语法

XPath是一门在XML文档中查找信息的语言,但也可以应用在HTML文档中。XPATH路径查找语法和CSS选择器语法的作用相同,但角色却完全不同。实际上,Parsel库会将我们编写的CSS选择器语句转换为XPATH路径查找语句,也就是说,真正发挥作用的是XPATH语句。

表1-2列出了XPATH路径查找语法表达式及其描述。

表1-2 XPATH路径查找语法表达式及其描述

img

前面我们已经体验过XPATH路径查找语法sel.xpath("//div[@class='content']/p/text()")带来的便利了,下面再逐步解析语法的作用。

首先是//div,这里的//的作用是从当前节点选取子孙节点,那么//div 代表从当前节点选取div子孙节点。由于当前节点为根节点,所以//div定位的是根节点下的所有div节点,包括层层嵌套的子孙节点,我们可以借助浏览器开发者工具的Elements面板来确认这个观点。唤起浏览器开发者工具后切换到Elements面板,并按快捷键Ctrl+F开启搜索栏,然后在搜索框中键入//div,此时Elements面板如图1-27所示。

img

图1-27 Elements面板(1)

面板底部的搜索框末尾的“1 of 3”中的1代表当前选中的节点序号,3代表符合搜索框规则的节点数。当前选中的节点序号是1,Elements面板中带有阴影的div标签就是当前选中的节点。我们可以通过点击搜索框末尾的上下箭头切换不同序号的节点,切换时Elements面板中的阴影就会出现在对应的节点上。

然后来看看第二段代码,也就是//div后面的 [@class='content']。这里的@符号代表选取属性,@class代表选择class属性,@class='content' 代表选取值为content的class属性。也就是说,//div[@class='content'] 定位的是class属性值为content的div节点,图1-28说明分析是对的。

img

图1-28 Elements面板(2)

有了前面的推导,后面跟着的 /p 就很容易理解了。/p 中的/指的是从当前节点选取直接子节点,/p 合起来代表选择当前节点的直接子节点 p。由于 class 属性值为content的div节点下有5个p节点,所以 //div[@class='content']/p的搜索结果总数肯定也是5。需要注意的是,text()并不是XPATH中的语法,而是Parsel库为了方便开发者提取节点内容支持的语法。

在语法方面,CSS选择器语法比XPATH路径查找语法简洁许多,但XPATH路径查找语法支持运算符。例如取余、与、或、加、减、乘、除、大于或等于、大于、等于、小于或等于、小于和不等于。代码表现为:

img

由于支持运算符,所以XPATH路径查找语法比CSS选择器语法更灵活。

1.4.2-3 借助开发者工具生成语法

对于层级较深或者没有属性的节点,写出定位语句的难度会比较大。这时候我们可以借助浏览器的开发者工具生成CSS选择器语法或XPATH路径查找语法。唤起浏览器开发者工具后切换到Elements面板,点击开发者工具左上角的第一个图标,然后将鼠标移动到想要定位的元素上,操作顺序如图1-29所示。

img

图1-29 操作顺序图示

找到想要定位的元素后用鼠标左键点击一下,此时浏览器会帮助我们在Elements面板中定位到对应的标签。在Elements面板定位的标签上右击,在弹出的快捷菜单栏中选择“Copy”→“Copy XPath”命令,即可将该标签对应的XPATH路径复制到剪贴板。操作过程中的Elements面板如图1-30所示。

img

图1-30 操作图示(1)

使用“粘贴”快捷键Ctrl+V便可将刚才复制的XPATH路径粘贴到编辑器中,路径为:

img

浏览器为我们生成的路径是完整路径,所以路径比较长。这个路径肯定能定位到指定的节点,但它并不一定是最优路径。这个span标签的class属性值为post-views,通过1.4.2-2节对XPATH路径查找语法的学习,我们很快便可写出最优的XPATH路径:

img

相比我们自己写的路径,浏览器生成的路径显得冗长,但这并不会影响定位准确性。

如果我们想让浏览器生成CSS选择器语法,那么按照图1-31的示意,在“Copy”菜单中选择“Copy selector”命令即可。

img

图1-31 操作图示(2)

得到的CSS选择器语句为:

img

这跟XPATH路径查找语法一样冗长,实际上我们只需要article:nth-child(6)>p.textmuted.views>span.post-views这段语句即可。如果想要更简短一些,我们可以把p、span和>符号去掉,最终语句为:

img