使用XPath

XPath,全称 XML Path Language,即XML 路径语言,它是一门在 XML 文档中查找信息的语言。但是它同样适用于 HTML 文档的搜索。

所以在做爬虫的时候,我们完全可以使用 XPath 来做相应的信息提取。

XPath 概览

XPath 的选择功能十分强大,它提供了非常简洁明了的路径选择表达式。另外,它还提供了超过 100 个内建函数,用于字符串、数值、时间的匹配以及节点、序列的处理。几乎所有我们想要定位的节点,都可以用 XPath 来选择。

XPath 常用规则

下图列举了XPath的几个常用规则:

python爬虫xpath多个条件_HTML


这里列出了xpath 的常用匹配规则,示例如下:

//title[@lang='eng']

这就是一个xpath规则,它代表选择所有名称为title,同时属性lang的值为eng的节点。

后面会通过python 的lxml库,利用xpath进行html的解析。

实例引入

现在通过实例来感受一下使用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解析对象。这里需要注意的是,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节点。

另外,也可以直接读取文本文件进行解析:

html = etree.parse('./test.html',etree.HTMLParser())
result = etree.tostring(html)
print(result.decode('utf-8'))

其中test.html的内容就是上面例子中的html代码。这次的输出结果略有不同,多了一个DOCTYPE的声明,不过对解析无任何影响。

所有节点

我们一般会使用//开头的xpath规则来选取所有符合要求的节点。这里以前面的html文本为例,如果要选取所有的节点,可以这样实现:

from lxml import etree
html = etree.HTML(text)
result = html.xpath('//*')
print(result)

运行结果如下:

[<Element html at 0x2c1691e4808>, <Element body at 0x2c1691e4788>, <Element div at 0x2c1691e4748>, <Element ul 
at 0x2c1691e4848>, <Element li at 0x2c1691e4888>, <Element a at 0x2c1691e4908>, <Element li at 0x2c1691e4948>, 
<Element a at 0x2c1691e4988>, <Element li at 0x2c1691e49c8>, <Element a at 0x2c1691e48c8>, <Element li at 0x2c1691e4a08>, <Element a at 0x2c1691e4a48>, <Element li at 0x2c1691e4a88>, <Element a at 0x2c1691e4ac8>]

这里使用*代表匹配所有节点,也就是整个html文本中的所有节点都会被获取。返回的形式是一个列表,每一个元素都是Element对象,其后跟了节点的名称。

当然,此处匹配也可以指定节点名称。如果想获取所有的li节点,示例如下:

from lxml import etree
html = etree.HTML(text)
result = html.xpath('//li')
print(result)
子节点

我们通过/或//即可查找元素的子节点或子孙节点。假如现在想选择li节点的所有直接a子节点,可以这样实现:

from lxml import etree
html = etree.HTML(text)
result = html.xpath('//li/a')
print(result)

这里通过追加/a即选择了所有的li节点的直接a节点。因为//li用于选中所有的li节点,/a用于选中li节点的所有直接子节点a。

此处的/用于选择直接子节点,如果要获取所有子孙节点,就可以使用//。例如,要获取ul节点下的所有子孙a节点,可以这样实现:

from lxml import etree
html = etree.HTML(text)
result = html.xpath('//ul//a')
print(result)

运行结果是相同的,但是如果这里使用//ul/a,就无法获取任何结果了。因为/用于获取直接子节点,而在ul节点下没有直接的a子节点,只有li节点,所以无法获取任何匹配结果。
注意:/和//的区别,其中/用于获取直接子节点,//用于获取子孙节点。要根据情况自行判断。

父节点

我们知道通过连续的/或//可以查找子节点或子孙节点,那么假如我们知道了子节点,怎样来查找父节点呢?

比如,现在首先选中href属性为link4.html的a节点,然后再获取其父节点,然后获取其class属性,相关代码如下:

from lxml import etree
html = etree.HTML(text)
result = html.xpath('//a[@href="link4.html"]/../@class')
print(result)

>>> ['item-1']

同时我们也可以通过parent::来获取父节点,代码如下:

from lxml import etree
html = etree.HTML(text)
result = html.xpath('//a[@href="link4.html"]/parent::li/@class')
print(result)
属性匹配

在选取的时候,我们还可以通过@符号进行属性过滤。比如,这里要选择class为item-1的li节点,可以这样实现:

from lxml import etree
html = etree.HTML(text)
result = html.xpath('//li[@class="item-0"]')
print(result)

这里我们通过加入[@class=“item-0”],限制了节点的class属性为item-0,而html文本中符合条件的li节点有两个,所以结果应该是返回两个匹配到的元素。

文本获取

我们可以通过Element 对象的text()方法获取节点中的文本,接下来尝试获取前面li节点中的文本,相关代码如下:

from lxml import etree
html = etree.HTML(text)
result = html.xpath('//li[@class="item-0"]/text()')
print(result)

运行结果如下:

['\n']

奇怪的是,我们并没有获取到任何文本,只获取到了一个换行符,这是为什么呢?因为XPath中text()前面是/,所以这里匹配到的结果就是被修正的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节点内部的文本,就有两种方式,一种是先选取a节点再获取文本,另一种就是使用//。接下来,我们来看下二者的区别。

# 选取到a节点再获取文本
from lxml import etree
html = etree.HTML(text)
result = html.xpath('//li[@class="item-0"]/a/text()')
print(result)

>>> ['first item', 'fifth item']


# 使用//选取的结果
from lxml import etree
html = etree.HTML(text)
result = html.xpath('//li[@class="item-0"]//text()')
print(result)

>>> ['first item', '\n', 'fifth item', '\n']

可想而知,这里是选取所有子孙节点的文本,其中前两个就是li的子节点a节点的内部文本。另一个就是最后一个li节点内部的换行符。

所以说,如果想获取某些特定子孙节点下的所有文本,可以先选取到特定的子孙节点,然后再调用text()方法获取其内部文本。

属性获取

我们知道用text()可以获取对应节点内部文档,那么节点属性该怎样获取呢?我们可以使用@符号来获取,例如,我们要获取所有li节点下所有a节点的href属性,代码如下:

from lxml import etree
html = etree.HTML(text)
result = html.xpath('//li/a/@href')
print(result)

>>> ['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html']

这里我们通过@href即可获取节点的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()方法,第一个参数传入属性名称,第二个参数传入属性值,只要此属性包含所传入的属性值,就可以完成匹配。

此种方法在某个节点的某个属性有多个值的时候经常用到,如某个节点的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)

>>> ['first item']

这里的li节点又增加了一个属性name,要确定这个节点,需要同时根据class和name属性来选择。当两者需要同时满足时,需要使用and操作符相连,相连之后置于中括号内进行条件筛选。

这里的and其实是XPath中的运算符。另外,还有很多运算符,如 or、mod等,

python爬虫xpath多个条件_HTML_02

按序选择

有时候,我们在选择的时候某些属性可能同时匹配了多个节点,但是只想要其中的某个节点,如第二个节点或者最后一个节点,这时该怎么办呢?示例如下:

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" href='123'><a href="link5.html">fifth item</a>
</ul>
</div>
'''

html = etree.HTML(text)
result = html.xpath('//li[1]/a/@href')
print(result)
result = html.xpath('//li[last()]/a/@href')
print(result)
result = html.xpath('//li[position()<3]/a/@href')
print(result)
result = html.xpath('//li[last()-2]/a/@href')
print(result)

第一次选择时,我们选取了第一个li节点,中括号传入数字1即可。这里的序号是从1开头。

第二次选择时,我们选取了最后一个li节点,中括号中传入last()即可,返回的便是最后一个li节点。

第三次选择时,我们选取了位置小于3的li节点,也就是位置序号为1和2的节点,得到的结果就是前两个li节点。

第四次选择时,我们选取了倒数第三个li节点,在中括号中传入last()-2即可。因为last()是最后一个,所以last()-2就是倒数第三个。

运行结果如下:

['link1.html']
['link5.html']
['link1.html', 'link2.html']
['link3.html']

这里我们使用了last()\position()等函数。在XPath中,提供了100多个函数,具体可参考http://www.w3school.com.cn/xpath/xpath_functions.asp

节点轴选择(扩展)

XPath提供了很多节点轴选择方法,包括获取子元素、兄弟元素、父元素、祖先元素等,示例如下:

from lxml import etree

text = '''
<div>
<ul>
<li class="item-0"><a href="link1.html"><span>first item</span></a><b href="link1.html"><span>first item</span></b></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" href='123'><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::*')
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::*[1]')
print(result)

第一次选择时,我们调用了ancestor轴,可以获取所有祖先节点,其后需要跟两个冒号,然后是节点的选择器,这里我们直接使用*,表示匹配所有节点,也可以指定想要获取的标签名,例如第二次选择,则只获取了div这个祖先节点。

第三次选择,我们调用了attribute轴,可以获取所有属性值,其后跟的选择器还是*,代表获取节点的所有属性,也可以指定属性名。

第四次选择,我们调用了child轴,可以获取所有直接子节点。也可以加上限定条件。

第五次选择,我们调用了descendant轴,可以获取所有子孙节点。这里我们又加了限定条件span,只获取span节点。

第六次选择,我们调用了following轴,可以获取当前节点之后的所有节点。这里虽然我们使用的是*匹配,但又加了索引选择,所以只获取第二个后续节点。

第七次选择,我们调用了following-sibling轴,可以获取当前节点之后的所有同级节点。这里虽然我们使用的是*匹配,但又加了索引选择,所以只获取第一个后续同级节点。

结语

到现在为止,我们基本上把可能用到的XPath选择器介绍完了。XPath功能非常强大,内置函数非常多,熟练使用之后,可以大大提升对HTML文本信息的提取效率。