文主要围绕以xpath和lxml库进行展开:
一、xpath 概念、xpath节点、xpath语法、xpath轴、xpath运算符
二、lxml的安装、lxml的使用、lxml案例
一、xpath
1.xpath概念
XPath 是一门在 XML 文档中查找信息的语言。XPath 使用路径表达式在 XML 文档中进行导航 。XPath 包含一个标准函数库 。XPath 是 XSLT 中的主要元素 。XPath 是一个 W3C 标准 。
2.xpath节点
xpath有七种类型的节点:元素、属性、文本、命名空间、处理指令、注释以及文档(根)节点。
节点关系:父、子、兄弟、先辈、后辈。
3.xpath语法
xpath语法在W3c网站上有详细的介绍,这里截取部分知识,供大家学习。
XPath 使用路径表达式在 XML 文档中选取节点。节点是通过沿着路径或者 step 来选取的。下面列出了最有用的路径表达式:
表达式 | 描述 |
nodename | 选取此节点的所有子节点。 |
/ | 从根节点选取。 |
// | 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。 |
. | 选取当前节点。 |
.. | 选取当前节点的父节点。 |
@ | 选取属性。 |
在下面的表格中,我们已列出了一些路径表达式以及表达式的结果:
路径表达式 | 结果 |
bookstore | 选取 bookstore 元素的所有子节点。 |
/bookstore | 选取根元素 bookstore。注释:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径! |
bookstore/book | 选取属于 bookstore 的子元素的所有 book 元素。 |
//book | 选取所有 book 子元素,而不管它们在文档中的位置。 |
bookstore//book | 选择属于 bookstore 元素的后代的所有 book 元素,而不管它们位于 bookstore 之下的什么位置。 |
//@lang | 选取名为 lang 的所有属性。 |
谓语用来查找某个特定的节点或者包含某个指定的值的节点。
谓语被嵌在方括号中。
在下面的表格中,我们列出了带有谓语的一些路径表达式,以及表达式的结果:
路径表达式 | 结果 |
/bookstore/book[1] | 选取属于 bookstore 子元素的第一个 book 元素。 |
/bookstore/book[last()] | 选取属于 bookstore 子元素的最后一个 book 元素。 |
/bookstore/book[last()-1] | 选取属于 bookstore 子元素的倒数第二个 book 元素。 |
/bookstore/book[position()<3] | 选取最前面的两个属于 bookstore 元素的子元素的 book 元素。 |
//title[@lang] | 选取所有拥有名为 lang 的属性的 title 元素。 |
//title[@lang='eng'] | 选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性。 |
/bookstore/book[price>35.00] | 选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。 |
/bookstore/book[price>35.00]/title | 选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。 |
XPath 通配符可用来选取未知的 XML 元素。
通配符 | 描述 |
* | 匹配任何元素节点。 |
@* | 匹配任何属性节点。 |
node() | 匹配任何类型的节点。 |
在下面的表格中,我们列出了一些路径表达式,以及这些表达式的结果:
路径表达式 | 结果 |
/bookstore/* | 选取 bookstore 元素的所有子元素。 |
//* | 选取文档中的所有元素。 |
//title[@*] | 选取所有带有属性的 title 元素。 |
通过在路径表达式中使用"|"运算符,您可以选取若干个路径。
在下面的表格中,我们列出了一些路径表达式,以及这些表达式的结果:
路径表达式 | 结果 |
//book/title | //book/price | 选取 book 元素的所有 title 和 price 元素。 |
//title | //price | 选取文档中的所有 title 和 price 元素。 |
/bookstore/book/title | //price | 选取属于 bookstore 元素的 book 元素的所有 title 元素,以及文档中所有的 price 元素。 |
4.xpath 轴
轴可定义相对于当前节点的节点集。
轴名称 | 结果 |
ancestor | 选取当前节点的所有先辈(父、祖父等)。 |
ancestor-or-self | 选取当前节点的所有先辈(父、祖父等)以及当前节点本身。 |
attribute | 选取当前节点的所有属性。 |
child | 选取当前节点的所有子元素。 |
descendant | 选取当前节点的所有后代元素(子、孙等)。 |
descendant-or-self | 选取当前节点的所有后代元素(子、孙等)以及当前节点本身。 |
following | 选取文档中当前节点的结束标签之后的所有节点。 |
namespace | 选取当前节点的所有命名空间节点。 |
parent | 选取当前节点的父节点。 |
preceding | 选取文档中当前节点的开始标签之前的所有节点。 |
preceding-sibling | 选取当前节点之前的所有同级节点。 |
self | 选取当前节点。 |
5.xpath运算符
下面列出了可用在 XPath 表达式中的运算符:
运算符 | 描述 | 实例 | 返回值 |
| | 计算两个节点集 | //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。 |
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 |
好了,xpath的内容就这么多了。接下来我们要介绍一个神器lxml,他的速度很快,曾经一直是我使用beautifulsoup时最钟爱的解析器,没有之一,因为他的速度的确比其他的html.parser 和html5lib快了许多。
二、lxml
1.lxml安装
lxml 是一个xpath格式解析模块,安装很方便,直接pip install lxml 或者easy_install lxml即可。
2.lxml 使用
lxml提供了两种解析网页的方式,一种是你解析自己写的离线网页时,另一种 则是解析线上网页。
导入包:
from lxml import etree
1.解析离线网页:
html=etree.parse('xx.html',etree.HTMLParser())
aa=html.xpath('//*[@id="s_xmancard_news"]/div/div[2]/div/div[1]/h2/a[1]/@href')
print(aa)
2.解析在线网页:
from lxml import etree
import requests
rep=requests.get('https://www.baidu.com')
html=etree.HTML(rep.text)
aa=html.xpath('//*[@id="s_xmancard_news"]/div/div[2]/div/div[1]/h2/a[1]/@href')
print(aa)
那么我们怎么获取这些标签和标签对应的属性值了,很简单,首先获取标签只需你这样做:
然后我们可以,比方说,你要获取a标签内的文本和它的属性href所对应的值,有两种方法,
1.表达式内获取
aa=html.xpath('//*[@id="s_xmancard_news"]/div/div[2]/div/div[1]/h2/a[1]/text()')
ab=html.xpath('//*[@id="s_xmancard_news"]/div/div[2]/div/div[1]/h2/a[1]/@href')
2.表达式外获取
aa=html.xpath('//*[@id="s_xmancard_news"]/div/div[2]/div/div[1]/h2/a[1]')
aa.text
aa.attrib.get('href')
这样就完成了获取,怎么样,是不是很简单了,哈哈哈。
下面再来lxml的解析规则:
表达式 | 描述 |
nodename | 选取此节点的所有子节点 |
/ | 从当前节点选取直接子节点 |
// | 从当前节点选取子孙节点 |
. | 选取当前节点 |
.. | 选取当前节点的父节点 |
@ | 选取属性 |
html=lxml.etree.HTML(text)
#使用text构造一个XPath解析对象,etree模块可以自动修正HTML文本
html=lxml.etree.parse('./ex.html',etree.HTMLParser())
#直接读取文本进行解析
from lxml import etree
result=html.xpath('//*')
#选取所有节点
result=html.xpath('//li')
#获取所有li节点
result=html.xpath('//li/a')
#获取所有li节点的直接a子节点
result=html.xpath('//li//a')
#获取所有li节点的所有a子孙节点
result=html.xpath('//a[@href="link.html"]/../@class')
#获取所有href属性为link.html的a节点的父节点的class属性
result=html.xpath('//li[@class="ni"]')
#获取所有class属性为ni的li节点
result=html.xpath('//li/text()')
#获取所有li节点的文本
result=html.xpath('//li/a/@href')
#获取所有li节点的a节点的href属性
result=html.xpath('//li[contains(@class,"li")]/a/text())
#当li的class属性有多个值时,需用contains函数完成匹配
result=html.xpath('//li[contains(@class,"li") and @name="item"]/a/text()')
#多属性匹配
result=html.xpath('//li[1]/a/text()')
result=html.xpath('//li[last()]/a/text()')
result=html.xpath('//li[position()<3]/a/text()')
result=html.xpath('//li[last()-2]/a/text()')
#按序选择,中括号内为XPath提供的函数
result=html.xpath('//li[1]/ancestor::*')
#获取祖先节点
result=html.xpath('//li[1]/ancestor::div')
result=html.xpath('//li[1]/attribute::*')
#获取属性值
result=html.xpath('//li[1]/child::a[@href="link1.html"]')
#获取直接子节点
result=html.xpath('//li[1]/descendant::span')
#获取所有子孙节点
result=html.xpath('//li[1]/following::*[2]')
#获取当前节点之后的所有节点的第二个
result=html.xpath('//li[1]/following-sibling::*')
#获取后续所有同级节点
3.lxml案例
为了偷懒,小编决定还是采用urllib那篇文章的代码,哈哈哈,机智如我。
好了,今天就讲这么多,大家感兴趣的话可以多多关注哦,精彩不停息!!!!
本文参考文献:
https://www.w3school.com.cn/
想学习更多前端、Python爬虫、大数据等计算机知识,请前往:http://pdcfighting.com/
XPath 全称为 Xml Path Language,即 Xml 路径语言,是一种在 Xml 文档中查找信息的语言。它提供了非常简洁的路径选择表达式,几乎所有的节点定位都可以用它来选择。
XPath 可以用于 Xml 和 Html,在爬虫中经常使用 XPath 获取 Html 文档内容。
lxml 是 Python 语言用 Xpath 解析 XML、Html文档功能最丰富的、最容易的功能模块。
在 XPath 中有七种节点分别是元素、属性、文本、文档、命名空间、处理指令、注释,前3种节点为常用节点
请看下面的 Html 例子,(注:这个例子全文都需要使用)
<!DOCTYPE html>
<html>
<body>
<div>
<!-- 这里是注释 -->
<h4>手机品牌商<span style="margin-left:10px">4</span></h4>
<ul>
<li>小米</li>
<li>华为</li>
<li class='blank'> OPPO </li>
<li>苹果</li>
</ul>
</div>
<div>
<h4>电脑品牌商<span style="margin-left:10px">3</span></h4>
<ul class="ul" style="color:red">
<li>戴尔</li>
<li>机械革命</li>
<li>ThinkPad</li>
</ul>
</div>
</body>
</html>
在上面的例子中
<html> 为文档节点
<li>小米</li> 为元素节点
class='blank' 为属性节点
<!-- 这里是注释 --> 为注释节点
在 XPath中有多中节点关系分别是父节点、子节点、同胞节点、先辈节点、后代节点
在上面的例子中
表达式描述nodeName选择nodeName节点的所有子节点/从根节点开始//从匹配的节点开始选择节点.选择当前节点..选择当前节点的父节点@选择元素*匹配任意元素节点@*匹配任意属性节点
用上面的 Html 文档举个例子
路径表达式描述body选取 body 的所有子节点/html选取 html 节点//div选取所有 div 节点//div/./h4div 节点下的 h4 节点../div选取当前节点的父节点下的所有 div 节点//@class所有带有 class 元素的节点//*选择所有节点//@*选择所有属性节点
表达式描述position()返回节点的 index 位置last()返回节点的个数contains(string1,string2)string1 是否包含 string2text()返回文本节点comment()返回注释节点normalize-space(string)去除首位空格,中间多个空格用一个空格代替substring(string,start,len)返回从 start 位置开始的指定长度的子字符串,第一个字符下标为1substring-before(string1,string2)返回string1中位于第一个string2之前的部分substring-after(string1,string2)返回string1中位于第一个string2之后的部分
同样用上面的Html文档举个例子
路径表达式描述//div[position()>1]选择第二个 div 节点//div[last()]选择最后一个 div 节点contains(//h4[2],’手机’)第二个 h4 标签是否包含手机字符串//li/text()li 节点中的文本内容//div/comment()div 节点下的 html 注释normalize-space(//li[@class=’blank’])li 节点下 class属性为 blank 的文本去掉空格substring(//h4[1],1,2)第一个 h4 节点的前2个字substring-before(//h4[1],’品牌商’)第一个 h4 节点的品牌商字符串之前的字符串substring-after(//h4[1],’品牌商’)第一个 h4 节点的品牌商字符串之后的字符串
XPath 中的谓语就是删选表达式,相当于 SQL 中的 Where 条件,谓语被嵌在 [ ] 中
路径表达式描述//div[1]选择第一个 div 节点//div[2]/ul/li[last()]选择第二个 div 节点下的最后一个 li 节点//div[2]/ul/li[position()>3]选择第二个 div 节点下的前两个 li 节点//ul[@class]选择所有带 class 属性的 ul 节点//ul[@class=’computer’]选择 class 属性为 computer 的 ul 节点//h4[span=4]选择 h4 节点下 span 值等于4的节点
以上内容介绍了 XPath 的基本语法,下面将介绍 XPath 如何在 Python 中使用。
sudo pip3 install lxml==4.4.1
lxml.etree 一个强大的 Xml 处理模块,etree 中的 ElementTree 类是一个主要的类,用于对XPath的解析、增加、删除和修改节点。
from lxml import etree
etree.parse() 函数可以解析一个网页文件还可以解析字符串, 在网页中下载的数据一般都是字符串形式的,使用 parse(StringIO(str)) 将整个页面内容解析加载构建一个 ElementTree 对象,ElementTree 可以使用 XPath 语法精准找到需要的数据。
1.加载页面到内存
from lxml import etree
from io import StringIO
test_html='''
<html>
<body>
<div>
<!-- 这里是注释 -->
<h4>手机品牌商<span style="margin-left:10px">4</span></h4>
<ul>
<li>小米</li>
<li>华为</li>
<li class='blank'> OPPO </li>
<li>苹果</li>
</ul>
</div>
<div>
<h4>电脑品牌商<span style="margin-left:10px">3</span></h4>
<ul class="ul" style="color:red">
<li>戴尔</li>
<li>机械革命</li>
<li>ThinkPad</li>
</ul>
</div>
</body>
</html>'''
html=etree.parse(StringIO(test_html))
print(html)
结果:
<lxml.etree._ElementTree object at 0x10bd6b948>
2.获取所有 li 标签数据
li_list=html.xpath('//li')
print("类型:")
print(type(li_list))
print("值:")
print(li_list)
print("个数:")
print(len(li_list))
for l in li_list:
print("li文本为:" + l.text)
结果:
类型:
<class 'list'>
值:
[<Element li at 0x10543c9c8>, <Element li at 0x10543ca08>, <Element li at 0x10543ca48>, <Element li at 0x10543ca88>, <Element li at 0x10543cac8>, <Element li at 0x10543cb48>, <Element li at 0x10543cb88>]
个数:
7
li文本为:小米
li文本为:华为
li文本为: OPPO
li文本为:苹果
li文本为:戴尔
li文本为:机械革命
li文本为:ThinkPad
3.获取带 class=’blank’ 属性数据
blank_li_list=html.xpath('//li[@class="blank"]')
print("类型:")
print(type(blank_li_list))
print("值:")
print(blank_li_list)
print("个数:")
print(len(blank_li_list))
for l in blank_li_list:
print("li文本为:" + l.text)
结果:
类型:
<class 'list'>
值:
[<Element li at 0x105253a48>]
个数:
1
li文本为: OPPO
4.属性操作
ul=html.xpath('//ul')[1]
#遍历属性
for name, value in ul.attrib.items():
print('{0}="{1}"'.format(name, value))
#添加新的属性
ul.set("new_attr", "true")
# 获取单个属性
new_attr=ul.get('new_attr')
print(new_attr)
结果:
class="ul"
style="color:red"
true
5.获取最后一个div标签数据
last_div=html.xpath('//div[last()]')
print("TAG:")
print(last_div.tag)
print("值:")
print(last_div.text)
结果
div
值:
6.添加子节点
child=etree.Element("child")
child.text="这里是新的子元素"
last_div.append(child)
# 在最后一个 div 标签查找新的子元素
clild_text=last_div.find("child").text
print(clild_text)
7.删除子元素
# 查找并设置第一个查询到的元素
first_ul=html.find("//ul")
ul_li=first_ul.xpath("li")
for li in ul_li:
# 删除元素
first_ul.remove(li)
ul_li=first_ul.xpath("li")
if len(ul_li)==0:
print("元素被删除了")
8.遍历元素后代
body=html.find("body")
for sub in body.iter():
print(sub.tag)
print(sub.text)
结果
body
div
<cyfunction Comment at 0x10c374b10>
这里是注释
h4
手机品牌商
span
4
ul
...
庆才,Python技术控,爬虫博文访问量已过百万。喜欢钻研,热爱生活,乐于分享。
个人博客:静觅 | http://cuiqingcai.com/
XPath,全称 XML Path Language,即 XML 路径语言,它是一门在XML文档中查找信息的语言。XPath 最初设计是用来搜寻XML文档的,但是它同样适用于 HTML 文档的搜索。
所以在做爬虫时,我们完全可以使用 XPath 来做相应的信息抽取,本节我们来介绍一下 XPath 的基本用法。
XPath 的选择功能十分强大,它提供了非常简洁明了的路径选择表达式,另外它还提供了超过 100 个内建函数用于字符串、数值、时间的匹配以及节点、序列的处理等等,几乎所有我们想要定位的节点都可以用XPath来选择。
XPath 于 1999 年 11 月 16 日 成为 W3C 标准,它被设计为供 XSLT、XPointer 以及其他 XML 解析软件使用,更多的文档可以访问其官方网站:https://www.w3.org/TR/xpath/。
我们现用表格列举一下几个常用规则:
表达式描述
nodename选取此节点的所有子节点
/从当前节点选取直接子节点
//从当前节点选取子孙节点
.选取当前节点
..选取当前节点的父节点
@选取属性
在这里列出了XPath的常用匹配规则,例如 / 代表选取直接子节点,// 代表选择所有子孙节点,. 代表选取当前节点,.. 代表选取当前节点的父节点,@ 则是加了属性的限定,选取匹配属性的特定节点。
例如:
//title[@lang=’eng’]
这就是一个 XPath 规则,它就代表选择所有名称为 title,同时属性 lang 的值为 eng 的节点。
在后文我们会介绍 XPath 的详细用法,通过 Python 的 LXML 库利用 XPath 进行 HTML 的解析。
在使用之前我们首先要确保安装好了 LXML 库,如没有安装可以参考第一章的安装过程。
我们现用一个实例来感受一下使用 XPath 来对网页进行解析的过程,代码如下:
from lxml import etree
text='''
<div>
<ul>
<li><a href="https://ask.hellobi.com/link1.html">first item</a></li>
<li><a href="https://ask.hellobi.com/link2.html">second item</a></li>
<li><a href="https://ask.hellobi.com/link3.html">third item</a></li>
<li><a href="https://ask.hellobi.com/link4.html">fourth item</a></li>
<li><a href="https://ask.hellobi.com/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])
运行结果:
[<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="https://ask.hellobi.com/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="https://ask.hellobi.com/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="https://ask.hellobi.com/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="https://ask.hellobi.com/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="https://ask.hellobi.com/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="https://ask.hellobi.com/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 21
\计算两个节点集//book \//cd返回所有拥有 book 和 cd 元素的节点集
+加法6 + 410
-减法6 - 42
*乘法6 * 424
div除法8 div 42
=等于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/xpath/xpath_operators.asp。
有时候我们在选择的时候可能某些属性同时匹配了多个节点,但是我们只想要其中的某个节点,如第二个节点,或者最后一个节点,这时该怎么办呢?
这时可以利用中括号传入索引的方法获取特定次序的节点,示例如下:
from lxml import etree
text='''
<div>
<ul>
<li><a href="https://ask.hellobi.com/link1.html">first item</a></li>
<li><a href="https://ask.hellobi.com/link2.html">second item</a></li>
<li><a href="https://ask.hellobi.com/link3.html">third item</a></li>
<li><a href="https://ask.hellobi.com/link4.html">fourth item</a></li>
<li><a href="https://ask.hellobi.com/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/xpath/xpath_functions.asp。
XPath 提供了很多节点轴选择方法,英文叫做 XPath Axes,包括获取子元素、兄弟元素、父元素、祖先元素等等,在一定情况下使用它可以方便地完成节点的选择,我们用一个实例来感受一下:
from lxml import etree
text='''
<div>
<ul>
<li><a href="https://ask.hellobi.com/link1.html"><span>first item</span></a></li>
<li><a href="https://ask.hellobi.com/link2.html">second item</a></li>
<li><a href="https://ask.hellobi.com/link3.html">third item</a></li>
<li><a href="https://ask.hellobi.com/link4.html">fourth item</a></li>
<li><a href="https://ask.hellobi.com/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="https://ask.hellobi.com/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/xpath/xpath_axes.asp。
End.
运行人员:中国统计网小编(微信号:itongjilove)
微博ID:中国统计网
中国统计网,是国内最早的大数据学习网站,公众号:中国统计网
http://www.itongji.cn
*请认真填写需求信息,我们会在24小时内与您取得联系。