快速上手: 三分钟搞定 Python XPath 语法

178 阅读4分钟

简介

XPath(XML Path Language)是一种用于在 XML 文档中查找信息的语言。它基于树状结构的 XML 文档,可以通过路径表达式来选取节点或节点集。也常常用来解析 HTML。

如果你是一个前端,对用路径获取元素可能陌生又熟悉。陌生是很多的路径,熟悉的路径上又有熟悉的属性和方法。下面我们就来探究一下 XPath 的魅力。

Python XPath 解析器

首先我们先了解一下工具,Python 是最简单的语言之一了,使用 Python 了解 XPath 尤其重要。

  • lxml
  • BeautifulSoup
  • scrapy
  • html5lib
  • python 标准库的:xml.etree.ElementTree

以 lxml 为例,首先安装 lxml 包:

pip install lxml

节点选择器

  • "/": 从根节点选取。
  • "//": 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。
  • ".": 选取当前节点。
  • "..": 选取当前节点的父节点。
  • "@": 选取属性。

以下是一个示例:

from lxml import html

# 创建一个 HTML 示例
html_string = '''
<html>
    <body>
        <div id="main">
            <p class="text">Hello, world!</p>
            <p class="text">Welcome to lxml.</p>
            <a href="http://example.com">Example</a>
        </div>
    </body>
</html>
'''

# 解析 HTML 字符串
tree = html.fromstring(html_string)

# 从根节点选取
root = tree.xpath('/html')[0]
print("Root node:", root.tag)  # 输出: Root node: html

# 从文档中的所有 p 节点中选择
paragraphs = tree.xpath('//p')
print("All paragraph texts:", [p.text for p in paragraphs])  # 输出: ['Hello, world!', 'Welcome to lxml.']

# 选取当前节点的父节点
p = tree.xpath('//p')[0]
parent = p.xpath('..')[0]
print("Parent of first paragraph:", parent.tag)  # 输出: div

# 选取当前节点
print("Current paragraph node:", p.tag)  # 输出: p

# 选取属性值
link = tree.xpath('//a')[0]
href = link.xpath('@href')[0]
print("Href attribute of the link:", href)  # 输出: http://example.com

通配符

  • "*": 匹配任何元素节点。
  • "@*": 匹配任何属性节点。

示例:

from lxml import html

# 创建一个 HTML 示例
html_string = '''
<html>
    <body>
        <div id="main">
            <p class="text">Hello, world!</p>
            <p class="text">Welcome to lxml.</p>
            <a href="http://example.com" title="Example">Example</a>
        </div>
    </body>
</html>
'''

# 解析 HTML 字符串
tree = html.fromstring(html_string)

# 匹配任何元素节点
elements = tree.xpath('//*')
print("All element tags:", [el.tag for el in elements])
# 输出: ['html', 'body', 'div', 'p', 'p', 'a']

# 匹配任何属性节点
attributes = tree.xpath('//@*')
print("All attribute values:", [attr for attr in attributes])
# 输出: ['main', 'text', 'text', 'http://example.com', 'Example']

谓词 (Predicates)

  • "[ ]" : 谓词用于查找特定的节点或包含特定值的节点。
  • "[position()]": 用于选取节点的位置。
  • "[@attribute='value']": 用于选取具有特定属性值的节点。
from lxml import html

# 创建一个 HTML 示例
html_string = '''
<html>
    <body>
        <div id="main">
            <p class="text">Hello, world!</p>
            <p class="text">Welcome to lxml.</p>
            <a href="http://example.com" title="Example">Example</a>
        </div>
    </body>
</html>
'''

# 解析 HTML 字符串
tree = html.fromstring(html_string)

# 使用谓词查找特定的节点
# 查找所有 p 元素中,文本包含 'Welcome' 的节点
specific_paragraphs = tree.xpath('//p[contains(text(), "Welcome")]')
print("Paragraphs containing 'Welcome':", [p.text for p in specific_paragraphs])
# 输出: ['Welcome to lxml.']

# 使用 position() 选择节点的位置
# 选择第二个 p 元素
second_paragraph = tree.xpath('//p[position()=2]')[0]
print("Second paragraph text:", second_paragraph.text)
# 输出: Welcome to lxml.

# 使用特定属性值选择节点
# 选择具有 title="Example" 的 a 元素
specific_link = tree.xpath('//a[@title="Example"]')[0]
print("Link with title 'Example':", specific_link.text)
# 输出: Example

函数

  • "text()": 选取文本内容。
  • "contains()": 检查某个节点的文本或属性值是否包含特定的子串。
  • "starts-with()": 检查某个节点的文本或属性值是否以特定的子串开头。
  • "count()": 计算选定的节点数。
from lxml import html

# 创建一个 HTML 示例
html_string = '''
<html>
    <body>
        <div id="main">
            <p class="text">Hello, world!</p>
            <p class="text">Welcome to lxml.</p>
            <a href="http://example.com" title="Example">Example</a>
            <a href="http://example.org" title="Example">Another Example</a>
        </div>
    </body>
</html>
'''

# 解析 HTML 字符串
tree = html.fromstring(html_string)

# 使用 text() 选取文本内容
paragraph_texts = tree.xpath('//p/text()')
print("Paragraph texts:", paragraph_texts)
# 输出: ['Hello, world!', 'Welcome to lxml.']

# 使用 contains() 检查文本是否包含特定子串
contains_text = tree.xpath('//p[contains(text(), "lxml")]')
print("Paragraphs containing 'lxml':", [p.text for p in contains_text])
# 输出: ['Welcome to lxml.']

# 使用 starts-with() 检查文本是否以特定子串开头
starts_with_text = tree.xpath('//p[starts-with(text(), "Hello")]')
print("Paragraphs starting with 'Hello':", [p.text for p in starts_with_text])
# 输出: ['Hello, world!']

# 使用 count() 计算选定的节点数
num_links = tree.xpath('count(//a)')
print("Number of links:", int(num_links))
# 输出: 2

运算符

  • "|": 选择若干路径。
  • "+、-、*、div":算术运算符。

多路径选择

from lxml import html

# 创建一个 HTML 示例
html_string = '''
<html>
    <body>
        <div class="content">
            <p>First paragraph.</p>
            <p>Second paragraph.</p>
            <span>Some span text.</span>
            <a href="http://example.com">Example link</a>
        </div>
    </body>
</html>
'''

# 解析 HTML 字符串
tree = html.fromstring(html_string)

# 使用 "|" 选择多个路径
elements = tree.xpath('//p | //span')
print("Selected elements:", [el.tag for el in elements])
# 输出: ['p', 'p', 'span']

# 获取这些元素的文本
texts = [el.text for el in elements]
print("Texts of selected elements:", texts)
# 输出: ['First paragraph.', 'Second paragraph.', 'Some span text.']

xpath 节点运算

from lxml import etree

# 创建一个 XML 示例
xml_string = '''
<root>
    <numbers>
        <num1>10</num1>
        <num2>20</num2>
        <num3>30</num3>
    </numbers>
</root>
'''

# 解析 XML 字符串
tree = etree.XML(xml_string)

# 选择数字节点并进行算术运算
sum_result = tree.xpath('(/root/numbers/num1 + /root/numbers/num2)[1]')
difference_result = tree.xpath('(/root/numbers/num2 - /root/numbers/num1)[1]')
product_result = tree.xpath('(/root/numbers/num1 * /root/numbers/num3)[1]')
division_result = tree.xpath('(/root/numbers/num3 div /root/numbers/num1)[1]')

print("Sum of num1 and num2:", sum_result)         # 输出: 30
print("Difference between num2 and num1:", difference_result)  # 输出: 10
print("Product of num1 and num3:", product_result)  # 输出: 300
print("Division of num3 by num1:", division_result)  # 输出: 3

Scrapy 中的 xpath

Scrapy 中的 xpath 在 Spider 类中的 parse 方法的 response 的中获取。下面是一些示例:

import scrapy

class ExampleSpider(scrapy.Spider):
    name = 'example'
    start_urls = ['http://example.com']

    def parse(self, response):
        # 提取标题
        title = response.xpath('//title/text()').get()
        print("Title:", title)

        # 提取所有段落文本
        paragraphs = response.xpath('//p/text()').getall()
        print("Paragraphs:", paragraphs)

        # 提取第二个段落
        second_paragraph = response.xpath('//p[2]/text()').get()
        print("Second paragraph:", second_paragraph)

        # 提取具有特定 href 的链接文本
        link_text = response.xpath('//a[@href="http://example.com"]/text()').get()
        print("Link text:", link_text)

        # 提取所有 href 属性
        links = response.xpath('//a/@href').getall()
        print("All href attributes:", links)

小结

本文主要介绍了 Python 中的 XPath 语法,以及 XPath 的解析库,这里着重介绍了 lxml 用法,同时也介绍了 scrapy 中 xpath 的各种用法。在日常编码中这些内容已经足够日常使用了。