上一篇文章: Python3网络爬虫实战---2七、Requests与正则表达式抓取猫眼电影排行
下一篇文章:
上一节咱们实现了一个最基本的爬虫,但提取页面信息时咱们使用的是正则表达式,用过以后咱们会发现构造一个正则表达式仍是比较的繁琐的,并且万一有一点地方写错了就可能会致使匹配失败,因此使用正则来提取页面信息多多少少仍是有些不方便的。html
对于网页的节点来讲,它能够定义 id、class 或其余的属性,并且节点之间还具备层次关系,在网页中能够经过 XPath 或 CSS 选择器来定位一个或多个节点。那么在页面解析时,咱们利用 XPath 或 CSS 选择器来提取到某个节点,而后再调用相应的方法去获取它的正文内容或者属性不就能够提取咱们想要的任意信息了吗?node
在 Python 中,咱们怎样来实现这个操做呢?不用担忧,这种解析库已经很是多了,其中比较强大的库有 LXML、BeautifulSoup、PyQuery 等等,本章咱们就来介绍一下这三个解析库的使用,有了它们,咱们不用再为正则发愁,并且解析效率也会大大提升,实为爬虫必备利器。正则表达式
XPath,全称 XML Path Language,即 XML 路径语言,它是一门在XML文档中查找信息的语言。XPath 最初设计是用来搜寻XML文档的,可是它一样适用于 HTML 文档的搜索。segmentfault
因此在作爬虫时,咱们彻底可使用 XPath 来作相应的信息抽取,本节咱们来介绍一下 XPath 的基本用法。网络
XPath 的选择功能十分强大,它提供了很是简洁明了的路径选择表达式,另外它还提供了超过 100 个内建函数用于字符串、数值、时间的匹配以及节点、序列的处理等等,几乎全部咱们想要定位的节点均可以用XPath来选择。函数
XPath 于 1999 年 11 月 16 日 成为 W3C 标准,它被设计为供 XSLT、XPointer 以及其余 XML 解析软件使用,更多的文档能够访问其官方网站:https://www.w3.org/TR/xpath/。网站
咱们现用表格列举一下几个经常使用规则:spa
表达式 | 描述 |
---|---|
nodename | 选取此节点的全部子节点 |
/ | 从当前节点选取直接子节点 |
// | 从当前节点选取子孙节点 |
. | 选取当前节点 |
.. | 选取当前节点的父节点 |
@ | 选取属性 |
在这里列出了XPath的经常使用匹配规则,例如 / 表明选取直接子节点,// 表明选择全部子孙节点,. 表明选取当前节点,.. 表明选取当前节点的父节点,@ 则是加了属性的限定,选取匹配属性的特定节点。设计
例如:code
//title[@lang=’eng’]
这就是一个 XPath 规则,它就表明选择全部名称为 title,同时属性 lang 的值为 eng 的节点。
在后文咱们会介绍 XPath 的详细用法,经过 Python 的 LXML 库利用 XPath 进行 HTML 的解析。
在使用以前咱们首先要确保安装好了 LXML 库,如没有安装能够参考第一章的安装过程。
咱们现用一个实例来感觉一下使用 XPath 来对网页进行解析的过程,代码以下:
from lxml import etree text = ''' <div> <ul> <li class="item-0"><a href="link1.html">first item</a></li> <li class="item-1"><a href="link2.html">second item</a></li> <li class="item-inactive"><a href="link3.html">third item</a></li> <li class="item-1"><a href="link4.html">fourth item</a></li> <li class="item-0"><a href="link5.html">fifth item</a> </ul> </div> ''' html = etree.HTML(text) result = etree.tostring(html) print(result.decode('utf-8'))
在这里咱们首先导入了 LXML 库的 etree 模块,而后声明了一段 HTML 文本,调用 HTML 类进行初始化,这样咱们就成功构造了一个 XPath 解析对象,在这里注意到 HTML 文本中的最后一个 li 节点是没有闭合的,可是 etree 模块能够对 HTML 文本进行自动修正。
在这里咱们调用 tostring() 方法便可输出修正后的 HTML 代码,可是结果是 bytes 类型,在这里咱们利用 decode() 方法转成 str 类型,结果以下:
<html><body><div> <ul> <li class="item-0"><a href="link1.html">first item</a></li> <li class="item-1"><a href="link2.html">second item</a></li> <li class="item-inactive"><a href="link3.html">third item</a></li> <li class="item-1"><a href="link4.html">fourth item</a></li> <li class="item-0"><a href="link5.html">fifth item</a> </li></ul> </div> </body></html>
咱们能够看到通过处理以后 li 节点标签被补全,而且还自动添加了 body、html 节点。
另外咱们也能够直接读取文本文件进行解析,示例以下:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = etree.tostring(html) print(result.decode('utf-8'))
其中 test.html 的内容就是上面例子中的 HTML 代码,内容以下:
<div> <ul> <li class="item-0"><a href="link1.html">first item</a></li> <li class="item-1"><a href="link2.html">second item</a></li> <li class="item-inactive"><a href="link3.html">third item</a></li> <li class="item-1"><a href="link4.html">fourth item</a></li> <li class="item-0"><a href="link5.html">fifth item</a> </ul> </div>
此次的输出结果略有不一样,多了一个 DOCTYPE 的声明,不过对解析无任何影响,结果以下:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"> <html><body><div> <ul> <li class="item-0"><a href="link1.html">first item</a></li> <li class="item-1"><a href="link2.html">second item</a></li> <li class="item-inactive"><a href="link3.html">third item</a></li> <li class="item-1"><a href="link4.html">fourth item</a></li> <li class="item-0"><a href="link5.html">fifth item</a> </li></ul> </div></body></html>
咱们通常会用 // 开头的 XPath 规则来选取全部符合要求的节点,以上文的 HTML 文本为例,若是咱们要选取全部节点,能够这样实现:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//*') print(result)
运行结果:
[<Element html at 0x10510d9c8>, <Element body at 0x10510da08>, <Element div at 0x10510da48>, <Element ul at 0x10510da88>, <Element li at 0x10510dac8>, <Element a at 0x10510db48>, <Element li at 0x10510db88>, <Element a at 0x10510dbc8>, <Element li at 0x10510dc08>, <Element a at 0x10510db08>, <Element li at 0x10510dc48>, <Element a at 0x10510dc88>, <Element li at 0x10510dcc8>, <Element a at 0x10510dd08>]
咱们在这里使用 * 表明匹配全部节点,也就是整个 HTML 文本中的全部节点都会被获取,能够看到返回形式是一个列表,每一个元素是 Element 类型,其后跟了节点的名称,如 html、body、div、ul、li、a 等等,全部的节点都包含在列表中了。
固然此处匹配也能够指定节点名称,若是咱们想获取全部 li 节点,示例以下:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//li') print(result) print(result[0])
在这里咱们要选取全部 li 节点可使用 //,而后直接加上节点的名称便可,调用时直接调用 xpath() 方法便可提取。
运行结果:
[<Element li at 0x105849208>, <Element li at 0x105849248>, <Element li at 0x105849288>, <Element li at 0x1058492c8>, <Element li at 0x105849308>] <Element li at 0x105849208>
在这里咱们能够看到提取结果是一个列表形式,其每个元素都是一个 Element 对象,若是要取出其中一个对象能够直接用中括号加索引便可取出,如 [0]。
咱们经过 / 或 // 便可查找元素的子节点或子孙节点,加入咱们如今想选择 li 节点全部直接 a 子节点,能够这样来实现:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//li/a') print(result)
在这里咱们经过追加一个 /a 即选择了全部 li 节点的全部直接 a 子节点,由于 //li 是选中全部li节点, /a 是选中li节点的全部直接子节点 a,两者组合在一块儿即获取了全部li节点的全部直接 a 子节点。
运行结果:
[<Element a at 0x106ee8688>, <Element a at 0x106ee86c8>, <Element a at 0x106ee8708>, <Element a at 0x106ee8748>, <Element a at 0x106ee8788>]
可是此处的 / 是选取直接子节点,若是咱们要获取全部子孙节点就该使用 // 了,例如咱们要获取 ul 节点下的全部子孙 a 节点,能够这样来实现:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//ul//a') print(result)
运行结果是相同的。
可是这里若是咱们用 //ul/a 就没法获取任何结果了,由于 / 是获取直接子节点,而在 ul 节点下没有直接的 a 子节点,只有 li 节点,因此没法获取任何匹配结果,代码以下:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//ul/a') print(result)
运行结果:
[]
所以在这里咱们要注意 / 和 // 的区别,/ 是获取直接子节点,// 是获取子孙节点。
咱们知道经过连续的 / 或 // 能够查找子节点或子孙节点,那假如咱们知道了子节点怎样来查找父节点呢?在这里咱们能够用 .. 来获取父节点。
好比咱们如今首先选中 href 是 link4.html 的 a 节点,而后再获取其父节点,而后再获取其 class 属性,代码以下:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//a[@href="link4.html"]/../@class') print(result)
运行结果:
['item-1']
检查一下结果,正是咱们获取的目标 li 节点的 class,获取父节点成功。
同时咱们也能够经过 parent:: 来获取父节点,代码以下:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//a[@href="link4.html"]/parent::*/@class') print(result)
在选取的时候咱们还能够用 @ 符号进行属性过滤,好比在这里若是咱们要选取 class 为 item-1 的 li 节点,能够这样实现:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//li[@class="item-0"]') print(result)
在这里咱们经过加入 [@class="item-0"] 就限制了节点的 class 属性为 item-0,而 HTML 文本中符合条件的 li 节点有两个,因此返回结果应该返回两个匹配到的元素,结果以下:
[<Element li at 0x10a399288>, <Element li at 0x10a3992c8>]
可见匹配结果结果正是两个,至因而不是那正确的两个,咱们在后面验证一下。
咱们用 XPath 中的 text() 方法能够获取节点中的文本,咱们接下来尝试获取一下上文 li 节点中的文本,代码以下:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//li[@class="item-0"]/text()') print(result)
运行结果以下:
['\n ']
很奇怪的是咱们并无获取到任何文本,而是只获取到了一个换行符,这是为何呢?由于 XPath 中 text() 前面是 /,而此 / 的含义是选取直接子节点,而此处很明显 li 的直接子节点都是 a 节点,文本都是在 a 节点内部的,因此这里匹配到的结果就是被修正的 li 节点内部的换行符,由于自动修正的li节点的尾标签换行了。
即选中的是这两个节点:
<li class="item-0"><a href="link1.html">first item</a></li> <li class="item-0"><a href="link5.html">fifth item</a> </li>
其中一个节点由于自动修正,li 节点的尾标签添加的时候换行了,因此提取文本获得的惟一结果就是 li 节点的尾标签和 a 节点的尾标签之间的换行符。
所以,若是咱们想获取 li 节点内部的文本就有两种方式,一种是选取到 a 节点再获取文本,另外一种就是使用 //,咱们来看下两者的区别是什么。
首先咱们选取到 a 节点再获取文本,代码以下:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//li[@class="item-0"]/a/text()') print(result)
运行结果:
['first item', 'fifth item']
能够看到这里返回值是两个,内容都是属性为 item-0 的 li 节点的文本,这也印证了咱们上文中属性匹配的结果是正确的。
在这里咱们是逐层选取的,先选取了 li 节点,又利用 / 选取了其直接子节点 a,而后再选取其文本,获得的结果刚好是符合咱们预期的两个结果。
咱们再来看下用另外一种方式 // 选取的结果,代码以下:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//li[@class="item-0"]//text()') print(result)
运行结果:
['first item', 'fifth item', '\n ']
不出所料,这里返回结果是三个,可想而知这里是选取全部子孙节点的文本,其中前两个就是 li 的子节点 a 节点内部的文本,另一个就是最后一个 li 节点内部的文本,即换行符。
因此说,若是咱们要想获取子孙节点内部的全部文本,能够直接用 // 加 text() 的方式获取,这样能够保证获取到最全面的文本信息,可是可能会夹杂一些换行符等特殊字符。若是咱们想获取某些特定子孙节点下的全部文本,能够先选取到特定的子孙节点,而后再调用 text() 方法获取其内部文本,这样能够保证获取的结果是整洁的。
咱们知道了用 text() 能够获取节点内部文本,那么节点属性该怎样获取呢?其实仍是用 @ 符号就能够,例如咱们想获取全部 li 节点下全部 a 节点的 href 属性,代码以下:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//li/a/@href') print(result)
在这里咱们经过 @href 便可获取节点的 href 属性,注意此处和属性匹配的方法不一样,属性匹配是中括号加属性名和值来限定某个属性,如 [@href="link1.html"],而此处的 @href 指的是获取节点的某个属性,两者须要作好区分。
运行结果:
['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html']
能够看到咱们成功获取了全部 li 节点下的 a 节点的 href 属性,以列表形式返回。
有时候某些节点的某个属性可能有多个值,例以下面例子:
from lxml import etree text = ''' <li class="li li-first"><a href="link.html">first item</a></li> ''' html = etree.HTML(text) result = html.xpath('//li[@class="li"]/a/text()') print(result)
在这里 HTML 文本中的 li 节点的 class 属性有两个值 li 和 li-first,可是此时若是咱们还想用以前的属性匹配获取就没法匹配了,代码运行结果:
[]
这时若是属性有多个值就须要用 contains() 函数了,代码能够改写以下:
from lxml import etree text = ''' <li class="li li-first"><a href="link.html">first item</a></li> ''' html = etree.HTML(text) result = html.xpath('//li[contains(@class, "li")]/a/text()') print(result)
这样咱们经过 contains() 方法,第一个参数传入属性名称,第二个参数传入属性值,这样只要此属性包含所传入的属性值就能够完成匹配了。
运行结果:
['first item']
此种选择方式在某个节点的某个属性有多个值的时候常常会用到,如某个节点的 class 属性一般有多个。
另外咱们可能还遇到一种状况,咱们可能须要根据多个属性才能肯定一个节点,这是就须要同时匹配多个属性才能够,那么这里可使用运算符 and 来链接,示例以下:
from lxml import etree text = ''' <li class="li li-first" name="item"><a href="link.html">first item</a></li> ''' html = etree.HTML(text) result = html.xpath('//li[contains(@class, "li") and @name="item"]/a/text()') print(result)
在这里 HTML 文本的 li 节点又增长了一个属性 name,这时候咱们须要同时根据 class 和 name 属性来选择,就能够 and 运算符链接两个条件,两个条件都被中括号包围,运行结果以下:
['first item']
这里的 and 实际上是 XPath 中的运算符,另外还有不少运算符,如 or、mod 等等,在此总结以下:
运算符 | 描述 | 实例 | 返回值 |
---|---|---|---|
or | 或 | price=9.80 or price=9.70 | 若是 price 是 9.80,则返回 true。若是 price 是 9.50,则返回 false。 |
and | 与 | price>9.00 and price<9.90 | 若是 price 是 9.80,则返回 true。若是 price 是 8.50,则返回 false。 |
mod | 计算除法的余数 | 5 mod 2 | 1 |
\ | 计算两个节点集 | //book //cd | 返回全部拥有 book 和 cd 元素的节点集 |
+ | 加法 | 6 + 4 | 10 |
- | 减法 | 6 - 4 | 2 |
* | 乘法 | 6 * 4 | 24 |
div | 除法 | 8 div 4 | 2 |
= | 等于 | price=9.80 | 若是 price 是 9.80,则返回 true。若是 price 是 9.90,则返回 false。 |
!= | 不等于 | price!=9.80 | 若是 price 是 9.90,则返回 true。若是 price 是 9.80,则返回 false。 |
< | 小于 | price<9.80 | 若是 price 是 9.00,则返回 true。若是 price 是 9.90,则返回 false。 |
<= | 小于或等于 | price<=9.80 | 若是 price 是 9.00,则返回 true。若是 price 是 9.90,则返回 false。 |
> | 大于 | price>9.80 | 若是 price 是 9.90,则返回 true。若是 price 是 9.80,则返回 false。 |
>= | 大于或等于 | price>=9.80 | 若是 price 是 9.90,则返回 true。若是 price 是 9.70,则返回 false。 |
此表参考来源:http://www.w3school.com.cn/xp...。
有时候咱们在选择的时候可能某些属性同时匹配了多个节点,可是咱们只想要其中的某个节点,如第二个节点,或者最后一个节点,这时该怎么办呢?
这时能够利用中括号传入索引的方法获取特定次序的节点,示例以下:
from lxml import etree text = ''' <div> <ul> <li class="item-0"><a href="link1.html">first item</a></li> <li class="item-1"><a href="link2.html">second item</a></li> <li class="item-inactive"><a href="link3.html">third item</a></li> <li class="item-1"><a href="link4.html">fourth item</a></li> <li class="item-0"><a href="link5.html">fifth item</a> </ul> </div> ''' html = etree.HTML(text) result = html.xpath('//li[1]/a/text()') print(result) result = html.xpath('//li[last()]/a/text()') print(result) result = html.xpath('//li[position()<3]/a/text()') print(result) result = html.xpath('//li[last()-2]/a/text()') print(result)
第一次选择咱们选取了第一个 li 节点,中括号中传入数字1便可,注意这里和代码中不一样,序号是以 1 开头的,不是 0 开头的。
第二次选择咱们选取了最后一个 li 节点,中括号中传入 last() 便可,返回的即是最后一个 li 节点。
第三次选择咱们选取了位置小于 3 的 li 节点,也就是位置序号为 1 和 2 的节点,获得的结果就是前 2 个 li 节点。
第四次选择咱们选取了倒数第三个 li 节点,中括号中传入 last()-2便可,由于 last() 是最后一个,因此 last()-2 就是倒数第三个。
运行结果以下:
['first item'] ['fifth item'] ['first item', 'second item'] ['third item']
在这里咱们使用了 last()、position() 等函数,XPath 中提供了 100 多个函数,包括存取、数值、字符串、逻辑、节点、序列等处理功能,具体全部的函数做用能够参考:http://www.w3school.com.cn/xp...。
XPath 提供了不少节点轴选择方法,英文叫作 XPath Axes,包括获取子元素、兄弟元素、父元素、祖先元素等等,在必定状况下使用它能够方便地完成节点的选择,咱们用一个实例来感觉一下:
from lxml import etree text = ''' <div> <ul> <li class="item-0"><a href="link1.html"><span>first item</span></a></li> <li class="item-1"><a href="link2.html">second item</a></li> <li class="item-inactive"><a href="link3.html">third item</a></li> <li class="item-1"><a href="link4.html">fourth item</a></li> <li class="item-0"><a href="link5.html">fifth item</a> </ul> </div> ''' html = etree.HTML(text) result = html.xpath('//li[1]/ancestor::*') print(result) result = html.xpath('//li[1]/ancestor::div') print(result) result = html.xpath('//li[1]/attribute::*') print(result) result = html.xpath('//li[1]/child::a[@href="link1.html"]') print(result) result = html.xpath('//li[1]/descendant::span') print(result) result = html.xpath('//li[1]/following::*[2]') print(result) result = html.xpath('//li[1]/following-sibling::*') print(result)
运行结果:
[<Element html at 0x107941808>, <Element body at 0x1079418c8>, <Element div at 0x107941908>, <Element ul at 0x107941948>] [<Element div at 0x107941908>] ['item-0'] [<Element a at 0x1079418c8>] [<Element span at 0x107941948>] [<Element a at 0x1079418c8>] [<Element li at 0x107941948>, <Element li at 0x107941988>, <Element li at 0x1079419c8>, <Element li at 0x107941a08>]
第一次选择咱们调用了 ancestor 轴,能够获取全部祖先节点,其后须要跟两个冒号,而后是节点的选择器,这里咱们直接使用了 *,表示匹配全部节点,所以返回结果是第一个 li 节点的全部祖先节点,包括 html,body,div,ul。
第二次选择咱们又加了限定条件,此次在冒号后面加了 div,这样获得的结果就只有 div 这个祖先节点了。
第三次选择咱们调用了 attribute 轴,能够获取全部属性值,其后跟的选择器仍是 *,这表明获取节点的全部属性,返回值就是 li 节点的全部属性值。
第四次选择咱们调用了 child 轴,能够获取全部直接子节点,在这里咱们又加了限定条件选取 href 属性为 link1.html 的 a 节点。
第五次选择咱们调用了 descendant 轴,能够获取全部子孙节点,这里咱们又加了限定条件获取 span 节点,因此返回的就是只包含 span 节点而没有 a 节点。
第六次选择咱们调用了 following 轴,能够获取当前节点以后的全部节点,这里咱们虽然使用的是 * 匹配,但又加了索引选择,因此只获取了第二个后续节点。
第七次选择咱们调用了 following-sibling 轴,能够获取当前节点以后的全部同级节点,这里咱们使用的是 * 匹配,因此获取了全部后续同级节点。
以上是XPath轴的简单用法,更多的轴的使用能够参考:http://www.w3school.com.cn/xp...。
到如今为止咱们基本上把可能用到的 XPath 选择器介绍完了, XPath 功能很是强大,内置函数很是多,熟练使用以后能够大大提高 HTML 信息的提取效率。
如想查询更多 XPath 的用法能够查看:http://www.w3school.com.cn/xp...。
如想查询更多 Python LXML 库的用法能够查看:http://lxml.de/。