XPath的使用


1.使用实例

通过Python的lxml库,利用XPath进行HTML的解析。

xpath对网页解析

代码:

from lxml import etree

text = '''

'''
html = etree.HTML(text)
#调用HTML类进行初始化,构造一个XPath解析对象

result = etree.tostring(html)
print(result.decode('utf-8'))
#调用tostring()方法即可输出修正后的HTML代码,但是结果是bytes类型
#用decode()方法将其转换为str类型

或者读取文本文件进行解析:

from lxml import etree

html = etree.parse('./test.html',etree.HTMLParser())
#读取文件解析,test.html与前面的HTML代码相同

result = etree.tostring(html)
print(result.decode('utf-8'))

运行结果:输出

结果补全了li节点标签,自动添加了body、html节点。

(直接读取文件输出多了一个DOCTYPE声明,对解析结果无影响)

2.所有节点

我们一般会用 // 开头的XPath规则来选取所有符合要求的结果。

使用 * 代表匹配所有节点。

以前面的test.html文件来演示。

代码:

from lxml import etree

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

运行结果:输出

[, , , , , , , , , , , , , ]

如果要选取所有的li节点,只需要使用//加上li节点名称即可。

代码:

from lxml import etree

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

运行结果:输出

[, , , , ]

提取出来的结果都是列表形式,每个元素都是Element对象。

3.子节点

通过 // 或 / 即可查找元素的子节点或子孙节点。

如果想选择li节点的所有直接a子节点。/a用于选择li节点的所有直接子节点a。

from lxml import etree

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

运行结果:输出

[, , , , ]

如果要获取所有的子孙节点,就可以使用 // 。

代码:

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)

运行结果:输出

[]

/ :用于获取直接子节点

// :用于获取子孙节点

4.父节点

如果知道了子节点,可以通过 .. 来查找父节点。

代码:

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)

运行结果同上。

5.属性匹配

可以使用@符号进行属性过滤。比如要选择class为item-0的li节点。

代码:

from lxml import etree

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

运行结果:输出

[, ]

6.文本获取

使用XPatn中的text()方法获取文本中的节点

尝试获取li节点中的文本。

代码:

from lxml import etree

html = etree.parse('./test.html',etree.HTMLParser())
result = html.xpath('//li[@class="item-0"]/text()')
#使用text()

print(result)

运行结果:输出

['\r\n     ']

并没有获取到任何文本,只获取到了换行符,这是为什么呢?因为 XPath 中 text 方法前面是 /,而此处 / 的含义是选取直接子节点,

很明显 li 的直接子节点都是 a 节点,文本都是在 a 节点内部的,所以这里匹配到的结果就是被修正的 li 节点内部的换行符,因为自动修正的 li 节点的尾标签换行了。

如果要获取到文本,有两种方式,一种是先选取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']

另一种是使用 //。

代码:

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内部的文本,即换行符。

7.属性获取

@可以获取节点属性。

如果我们想获取所有li节点下所有的a节点的href属性。

代码:

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']

此处和属性匹配的方法不同,属性匹配是中括号加属性名和值来限定某个属性,如 [@href="link1.html"],而此处的 @href 指的是获取节点的某个属性,二者需要做好区分。

8.属性多值匹配

某些节点的属性有多个值。

代码:

from lxml import etree

text = '''
  • first item
  • ''' 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 = '''
    
  • first item
  • ''' html = etree.HTML(text) result = html.xpath('//li[contains(@class,"li")]/a/text()') #contains()方法,第一个参数传入属性名称,第二个参数传入属性值 print(result)

    运行结果:输出

    ['first item']

    9.多属性匹配

    根据多个属性确定一个节点,就要同时匹配多个属性。

    代码:

    from lxml import etree
    
    text = '''
    
  • first item
  • ''' #这里li节点又增加了一个name属性,需要同时根据class和name来选择 html = etree.HTML(text) result = html.xpath('//li[contains(@class, "li") and @name="item"]/a/text()') #使用and操作符相连接 print(result)

    运行结果:输出

    ['first item']

    and是XPath中的运算符,此外还有很多。

    10.按序选择

    有时匹配了多个节点,却只想要其中几个节点。

    可以利用中括号传入索引的方法获取特定次序节点。

    代码:

    from lxml import etree
    
    text = '''
    
    '''
    html = etree.HTML(text)
    result = html.xpath('//li[1]/a/text()')
    #选取了第一个li节点,注意是以1开头的,而不是0
    
    print(result)
    result = html.xpath('//li[last()]/a/text()')
    #选取最后一个li节点,中括号中传入last()即可
    
    print(result)
    result = html.xpath('//li[position()<3]/a/text()')
    #选取了位置小于3的节点,也就是序号为1和2的节点
    
    print(result)
    result = html.xpath('//li[last()-2]/a/text()')
    #选取了倒数第三个li节点,传入last()-2即可,因为last()是最后一个,last()-2就是倒数第三个
    
    print(result)

    运行结果:输出

    ['first item']
    ['fifth item']
    ['first item', 'second item']
    ['third item']

    使用了last()、position()等函数,在XPath中还有许多其他种类的函数。

    11.节点轴选择

    XPath提供了很多节点轴选择方法。

    代码:

    from lxml import etree
    
    text = '''
    
    '''
    html = etree.HTML(text)
    result = html.xpath('//li[1]/ancestor::*')
    #调用ancestor轴,可以获取所有的先祖节点。
    #其后要跟两个冒号,其次是节点的选择器,使用 * 表示匹配所有节点。
    
    print(result)
    result = html.xpath('//li[1]/ancestor::div')
    #*改为div,只得到div这一个先祖节点。
    
    print(result)
    result = html.xpath('//li[1]/attribute::*')
    #调用attribute轴,可以获取所有属性值。
    #使用*表示获取节点的所有属性。
    
    print(result)
    result = html.xpath('//li[1]/child::a[@href="link1.html"]')
    #调用child轴,可以获取所有直接子节点。
    #其后加了限定条件,选择href属性为link1.html的a节点。
    
    print(result)
    result = html.xpath('//li[1]/descendant::span')
    #调用descendant轴,可以获取所有子孙节点。
    #其后加了限定条件,获取span节点,所以返回的结果只包含span节点而不包含a节点。
    
    print(result)
    result = html.xpath('//li[1]/following::*[2]')
    #调用following轴,可以获取当前节点之后的所有节点。
    #虽然使用的是 * 匹配,但又加了索引选择,所以只获取了第二个后续节点。
    
    print(result)
    result = html.xpath('//li[1]/following-sibling::*')
    #调用了following-sibling轴,可以获取当前节点之后的所有同级节点。
    #使用 * 匹配,所以获取了所有后续同级节点。
    
    print(result)

    运行结果:输出

    [, , , ]
    []
    ['item-0']
    []
    []
    []
    [, , , ]

    参考用书《python3网络爬虫开发实战》