文章目录
- 使用XPath
- 1.XPath常用规则
- 2.所有节点
- 3.子节点
- 4.父节点
- 5.属性匹配
- 6.文本获取
- 7.属性获取
- 8.按序选择
使用XPath
XPath,全称XML Path Language,即XML路径语言,它是一门在XML文档中查找信息的语言。
1.XPath常用规则
这里列出了XPath的常用匹配规则,示例如下:
//title[@lang='eng']
这就是一个XPath规则,它代表选择所有名称为title,同时属性lang的值为eng的节点。
后面会通过Python的lxml库,利用XPath进行HTML的解析。
我们来看一个实例:
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'))
<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>
这里首先导入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'))
<!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>
这次的输出结果略有不同,多了一个DOCTYPE的声明,不过对解析无任何影响
2.所有节点
我们一般会用//开头的XPath规则来选取所有符合要求的节点。这里以前面的HTML文本为例,如果要选取所有节点,可以这样实现:
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//*')
print(result)
[<Element html at 0x1f6c0205648>, <Element body at 0x1f6c0019bc8>, <Element div at 0x1f6c0019c88>, <Element ul at 0x1f6c0205348>, <Element li at 0x1f6c0205688>, <Element a at 0x1f6c0205708>, <Element li at 0x1f6c0205748>, <Element a at 0x1f6c0205788>, <Element li at 0x1f6c02057c8>, <Element a at 0x1f6c02056c8>, <Element li at 0x1f6c0205808>, <Element a at 0x1f6c0205848>, <Element li at 0x1f6c0205888>, <Element a at 0x1f6c02058c8>]
这里使用*代表匹配所有节点,也就是整个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 0x1f6c01e6f88>, <Element li at 0x1f6c0205cc8>, <Element li at 0x1f6c0205d08>, <Element li at 0x1f6c0205d48>, <Element li at 0x1f6c0205d88>]
<Element li at 0x1f6c01e6f88>
这里可以看到提取结果是一个列表形式,其中每个元素都是一个 Element对象。如果要取出其中一个对象,可以直接用中括号加索引,如[0]。
3.子节点
我们通过/或//即可查找元素的子节点或子孙节点。假如现在想选择li节点的所有直接a子节点,可以这样实现:
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li/a')
print(result)
[<Element a at 0x1f6c01e6fc8>, <Element a at 0x1f6c0205588>, <Element a at 0x1f6c0205e08>, <Element a at 0x1f6c0205e48>, <Element a at 0x1f6c0205e88>]
/
用于选取直接子节点,如果要获取所有子孙节点,就可以使用//
。例如,要获取ul
节点下的所有子孙a
节点,可以这样实现:
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//ul//a')
print(result)
[<Element a at 0x1f6c02058c8>, <Element a at 0x1f6c0205908>, <Element a at 0x1f6c02070c8>, <Element a at 0x1f6c0207108>, <Element a at 0x1f6c0207148>]
但是如果这里用//ul/a
,就无法获取任何结果了。因为/
用于获取直接子节点,而在ul
节点下没有直接的a
子节点,只有li
节点,所以无法获取任何匹配结果,代码如下:
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//ul/a')
print(result)
[]
4.父节点
用..
来实现
from lxml import etree
html = etree.parse('test.html', etree.HTMLParser())
result = html.xpath('//a[@href = "link4.html"]/../@class')
print(result)
['item-1']
我们也可以通过parent::来获取父节点:
from lxml import etree
html = etree.parse('test.html', etree.HTMLParser())
result = html.xpath('//a[@href = "link4.html"]/parent::*/@class')
print(result)
['item-1']
5.属性匹配
@
符号用于进行属性过滤,如选取class为item-1的li节点:
from lxml import etree
html = etree.parse('test.html', etree.HTMLParser())
result = html.xpath('//li[@class = "item-0"]')
print(result)
[<Element li at 0x1f6c038cd08>, <Element li at 0x1f6c038c788>]
6.文本获取
我们用XPath中的text()方法获取li节点中的文本:
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']
我们再来看看使用//
选取的结果:
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', '\r\n ']
里是选取所有子孙节点的文本,其中前两个就是li的子节点a节点内部的文本,另外一个就是最后一个li节点内部的文本,即换行符。
7.属性获取
使用@
符号获取属性
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li/a/@href')
print(result)
['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html']
某些节点的某个属性可能有多个值,这里HTML文本中li节点的class属性有两个值li和li-first,此时如果还想用之前的属性匹配获取,就无法匹配了:
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)
[]
我们可以使用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)
['first item']
如果一个节点都多个属性,可以使用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']
另外,还有很多运算符,如or、mod等:
8.按序选择
我们选取了第一个li节点,中括号中传入数字1,选取最后一个li节点,中括号中传入last(),选取倒数第三个li节点,中括号中传入last() - 2
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)
['first item']
['fifth item']
['first item', 'second item']
['third item']