爬虫基础(6)网页解析之XPath库

一. XPath库简介

XPath 全称 XML Path Language,即 XML 路径语言,它是一门在 XML 文档中查找信息的语言。它最初是用来搜寻 XML 文档的,但是它同样适用于 HTML 文档的搜索。所以在做爬虫时,我们完全可以使用 XPath 来做相应的信息抽取。

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

XPath 于1999年11月16日成为 W3C 标准,它被设计为供 XSLT、XPointer 以及其他 XML 解析软件使用,更多的文档可以访问其官方网站:http s://www.w3.org/TR/xpath/

二. 安装lxml库

在 Ubuntu 虚拟机中,lxml 能通过 pip 来安装:

$ python -m pip install lxml
或者
$pip install lxml

其他平台下的安装可参考其他相应的安装方式,此处不做概述。

三. XPath库详析

1. XPath常用的规则

表达式 描述
nodename 选取此节点的所有子节点
/ 从当前节点选取直接子节点
// 从当前节点选取子孙节点
. 选取当前节点
.. 选取当前节点的父节点
@ 选取属性
* 选取所有元素节点与元素名
@* 选取所有属性
[@attrib] 选取具有给定属性的所有元素
[@attrib='value'] 选取给定属性具有给定值得所有元素
[tag] 选取所有具有指定元素的直接子节点
[tag='text'] 选取所有具有指定元素并且文本内容是text的节点

2. 选取所有节点

一般我们会用// 开头的XPath规则来选择所有符合要求的节点。我们看看下面的示例:

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//*')
print(result)

# 运行结果:
[<Element html at 0x7f8a1a02a748>, <Element body at 0x7f8a1a02a848>, <Element div at 0x7f8a1a02a888>, <Element div at 0x7f8a1a02a8c8>, <Element div at 0x7f8a1a02a908>, <Element ul at 0x7f8a1a02a988>, <Element li at 0x7f8a1a02a9c8>, <Element a at 0x7f8a1a02aa08>, <Element li at 0x7f8a1a02aa48>, <Element a at 0x7f8a1a02a948>, <Element li at 0x7f8a1a02aa88>, <Element a at 0x7f8a1a02aac8>, <Element li at 0x7f8a1a02ab08>, <Element a at 0x7f8a1a02ab48>, <Element div at 0x7f8a1a02ab88>, <Element span at 0x7f8a1a02abc8>, <Element a at 0x7f8a1a02ac08>, <Element strong at 0x7f8a1a02ac48>, <Element span at 0x7f8a1a02ac88>, <Element a at 0x7f8a1a02acc8>, <Element span at 0x7f8a1a02ad08>, <Element a at 0x7f8a1a02ad48>, <Element span at 0x7f8a1a02ad88>]

上述代码中的test.html 文件将用于下面所有的示例中,其内容如下:

<div class="header">
    <div class="header_contain">
        <div class="logo"></div>
        <ul class="menu">
            <li><a href="../news/index.html">Home Page</a></li>
            <li><a href="../course/course.html">Online Class</a></li>
            <li><a href="../doc/docDownload.html">Download Document</a></li>
            <li><a href="../news/search.html">Search</a></li>
        </ul>
        <div class="login">
            <span class="span Admin"><a href="../user/admin.html"><strong>USER ADMIN:</strong></a></span>
            <span class="item"><a href="../user/login.html">SIGN IN | </a></span>
            <span class="item"><a href="../user/register.html">SIGN UP | </a></span>
            <span class="item" id="logout" >SIGN OUT</span>
        </div>
    </div>
</div>

上述例子获取的是整个HTML文本中所有的节点。运行结果返回的是一个列表,每一个元素都是Element类型,气候跟了节点名称,例如:html、body、div、ul、li、a等。

当然,选取节点时也可以匹配指定的节点名称,如果想获取所有span 节点,示例如下:

扫描二维码关注公众号,回复: 12856110 查看本文章
from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//span')
print(result)
print(result4[2])

# 运行结果:
[<Element span at 0x7f491e251808>, <Element span at 0x7f491e251848>, <Element span at 0x7f491e251888>, <Element span at 0x7f491e2518c8>]
<Element span at 0x7f491e251888>

如果要取出其中一个对象,可以直接用中括号加索引来获取。

3. 选取子节点

我们通过 /// 即可查找元素的子节点或子孙节点。例如下面示例选取 li 节点的所有直接 a 子节点:

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li/a')
print(result)

# 运行结果:
[<Element a at 0x7f79ecb96848>, <Element a at 0x7f79ecb96888>, <Element a at 0x7f79ecb968c8>, <Element a at 0x7f79ecb96908>]

此处的 / 用于选取直接子节点,如果要选取所有的子孙节点,就可以使用 // ,例如:选取 class = "header_contain" 的 div 节点下的所有 a 节点:

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//div[@class="header_contain"]//a')
print(result)

# 运行结果:
[<Element a at 0x7f79ecb96748>, <Element a at 0x7f79ecb96a08>, <Element a at 0x7f79ecb96a48>, <Element a at 0x7f79ecb96a88>, <Element a at 0x7f79ecb96ac8>, <Element a at 0x7f79ecb96b48>, <Element a at 0x7f79ecb96b88>]

这里总共选取出七个 a 标签节点,其中前四个 a 节点是 li 节点下的子节点,而后三个是 span 节点下的子节点。代码中 [] 里的内容在XPath中被称为“谓语”,它被用来查找某个特定的节点或者包含某个指定的值的节点。谓语一般被嵌在方括号中。在 XPath 中,谓语是一个被经常用到的概念,下面的诸多示例中都有应用。

**注意:**如果通过 / 来获取某一节点下并不存在的直接子节点,则返回的列表为空。

4. 选取父节点

我们知道通过连续的 /// 可以查找子节点或子孙节点,那么假如我们知道了子节点,怎样来查找父节点呢?我们可以用 .. 来实现。例如,获取 class="menu" 的 ul 节点的父节点所拥有的 class 属性:

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//ul[@class="menu"]/../@class')
print(result)

# 运行结果:
['header_contain']

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

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//ul[@class="menu"]/parent::*/@class')
print(result)

运行结果同上述示例。

5. 以属性匹配

在选取的时候,我们还可以用 @ 进行属性过滤。例如:下面示例中选取 class=“login” 的 div 节点:

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//div[@class="login"]')
print(result)

# 运行结果:
[<Element div at 0x7fbf6c6ef808>]

6. 获取文本

我们用 XPath 中的 text() 方法获取节点中的文本。下面我们尝试获取前面 test.html 文件中的 span 节点里的文本:

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//span[@class="item"]/text()')
print(result)

# 运行结果:
['SIGN OUT']

可以发现,运行结果获取了第三个 class="item" 的 span 节点下的文本内容,而前两个 span 节点下的文本内容并未获取到,这是为什么呢?因为 XPath 中 text() 前面是 / ,即选取直接子节点,很明显前两个 span 节点的直接子节点都是 a 节点,文本都是在 a 节点内部,所以这里匹配到的结果只有第三个 span 节点内部的文本内容。

那么,如果想要获取 class="item" 的 span 节点内部的文本,有两种方法:一是先选取 a 节点再获取其文本;二是使用 // ,下面我们就上面的例子进行修改并运行:

# 方法1:
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//span[@class="item"]/a/text()')
print(result)

# 运行结果:
['SIGN IN | ', 'SIGN UP | ']

# 方法2:
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//span[@class="item"]//text()')
print(result)

# 运行结果:
['SIGN IN | ', 'SIGN UP | ', 'SIGN OUT']

上述两种方法的运行结果是不同的。我们不难发现其原因:方法1获取的是 span 节点下的 a 节点的文本内容,而方法2获取的是 span 节点下的所有节点的文本内容,包括 a 节点的文本内容。

通过上述示例我们可以发现两种方法之间的区别:如果想要获取子孙节点内部的所有文本,可以直接用 // 加 text() 的方式,这样可以保证获取到最全面的文本信息,但是可能会夹杂一些换行符(当某个节点未完全闭合时)等特殊字符;如果想要获取某些特定子孙节点下的所有文本,可以先选取到特定的子孙节点,然后再调用 text() 方法获取其内部文本,这样可以保证获取的结果是整洁的。

7. 获取属性

节点属性可以用@符号获取。例如:我们可以获取所有 li 节点下的所有 a 节点的 href 属性:

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li/a/@href')
print(result)

# 运行结果:
['../news/index.html', '../course/course.html', '../doc/docDownload.html', '../news/search.html']

这里我们通过 @href 即可获取节点的 href 属性。**注意:**此处和属性匹配的方法不同,属性匹配是中括号加属性名和值来限定某个属性,如 [@class="item"],而此处的 @href 指的是获取节点的某个属性,二者需要做好区分。

8. 属性多值匹配

有时候某些节点的某个属性可能有多个值,例如 test.html 文件中的 span 节点:

<span class="span Admin">USER ADMIN:</span>

这个 span 节点的 class 属性有两个值:span 和 Admin。此时如果依旧用之前的属性匹配获取节点或节点内容,那么返回的列表为空。这时需要用 contains() 函数解决这个问题:

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//div[@class="login"]/span[contains(@class,"span")]/text()')
print(result)

# 运行结果:
['USER ADMIN:']

contains() 方法需要传入两个参数:第一个参数传入属性名称,第二个参数传入属性值,只要此属性包含所传入的属性值就可以完成匹配。

9. 多属性匹配

另外,我们可能还遇到一种情况,那就是根据多个属性确定一个节点,这时就需要同时匹配多个属性。此时可以使用运算符 and 来连接。例如下面示例:

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//span[@class="item" and @id="logout"]/text()')
print(result)

# 运行结果:
['SIGN OUT']

这里的 span 节点有两个属性:id 和 name。要确定这个节点,需要同时根据 id 和 name 属性来选择。用 and 操作符连接两个条件,相连之后置于中括号内进行条件筛选。

XPath 中除了 and 操作符之外,还有很多运算符,常用的一些操作符如下表总结:

运算符 描述
or
and
mod 计算除法的余数
| 计算两个节点集
+ 加法
- 减法
* 乘法
div 除法
= 等于
!= 不等于
< 小于
<= 小于等于
> 大于
>= 大于等于

10. 按序选择

有时候,我们在选择的时候某些属性可能同时匹配了多个节点,但是我们只想要其中的某个节点,如第二个节点或者最后一个节点,这该怎么办呢?此时可以利用中括号传入索引的方法获取特定次序的节点,如下示例:

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
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()-1]/a/text()')
print(result)

# 运行结果:
['Home Page']
['Search']
['Home Page', 'Online Class']
['Download Document']

**注意:**此处的索引并不是从0开始,而是从1开始,这与python中的列表索引有所不同。

11. 节点轴选择

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

from lxml import etree

html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//span[1]/ancestor::*')
print(result)
result = html.xpath('//span[1]/ancestor::div')
print(result)
result = html.xpath('//span[1]/attribute::*')
print(result)
result = html.xpath('//span[1]/child::a[@href="../user/admin.html"]')
print(result)
result = html.xpath('//span[1]/descendant::strong')
print(result)
result = html.xpath('//span[1]/following::*[2]')
print(result)
result = html.xpath('//span[1]/following-sibling::*')
print(result)

# 运行结果:
[<Element html at 0x7f108143a708>, <Element body at 0x7f108143a808>, <Element div at 0x7f108143a848>, <Element div at 0x7f108143a888>, <Element div at 0x7f108143a8c8>]
[<Element div at 0x7f108143a848>, <Element div at 0x7f108143a888>, <Element div at 0x7f108143a8c8>]
['span Admin']
[<Element a at 0x7f108143a848>]
[<Element strong at 0x7f108143a888>]
[<Element a at 0x7f108143a808>]
[<Element span at 0x7f108143a848>, <Element span at 0x7f108143a8c8>, <Element span at 0x7f108143a908>]
  • 第一次选择返回结果是第一个 li 节点的所有祖先节点,包括 html、body、div和ul;
  • 第二次选择返回结果是祖先节点中的 div 节点;
  • 第三次选择返回结果是第一个 span 节点的所有属性值;
  • 第四次选择返回结果是 href 属性值为 “…/user/admin.html” 的 a 节点;
  • 第五次选择返回结果是 span 节点下的子节点 strong,而不是子节点 a;
  • 第六次选择返回结果是当前节点之后的所有节点中的第二个节点,即该 span 节点之后的第一个 span 节点下的 a 节点;如果不加索引,则返回结果是该 span 节点之后的所有 span 节点及其下面的 a 节点;
  • 第七次选择返回结果是当前的 span 节点之后的所有同级节点,即该 span 节点之后所有的 span 节点。

除了上述的几个节点轴之外还有其他一些节点轴,整理如下表所示:

轴名称 结果
ancestor 选取当前节点的所有先辈(父、祖父等)
ancestor-or-self 选取当前节点的所有先辈(父、祖父等)以及当前节点本身
attribute 选取当前节点的所有属性
child 选取当前节点的所有子元素
descendant 选取当前节点的所有后代元素(子、孙等)
descendant-or-self 选取当前节点的所有后代元素(子、孙等)以及当前节点本身
following 选取文档中当前节点的结束标签之后的所有节点
following-sibling 选取文档中当前节点的结束标签之后的所有同级节点
preceding 选取文档中当前节点的开始标签之前的所有节点
preceding-sibling 选取当前节点之前的所有同级节点
namespace 选取当前节点的所有命名空间节点
parent 选取当前节点的父节点
self 选取当前节点

如果想查询更多 XPath 的用法,可以查看: http://www.w3school.com.cn/xpath/index.asp。

猜你喜欢

转载自blog.csdn.net/qq_45617055/article/details/115057240