在解析网络抓取的 HTML 内容时,有多种技术可以选择我们想要的数据。 对于简单的文本解析,可以使用正则表达式,但HTML被设计成机器可读的文本结构。我们可以利用这一事实并使用特殊路径语言(如 CSS 选择器和 XPath)以更高效和可靠的方式提取数据! 您可能熟悉样式表文档 ( .css
) 中的 CSS 选择器,但是,XPath 不止于此,它以其自己独特的语法实现了完整的文档导航。
使用 CSS 选择器解析 HTML
在本文中,我们将深入研究这种独特的路径语言,以及如何使用它从现代复杂的 HTML 文档中提取所需的详细信息。我们将从快速介绍和表达式备忘单开始,并使用交互式 XPath 测试器探索概念。 最后,我们将介绍各种编程语言中的 XPath 实现,以及涉及 Web 抓取中的 XPath 的一些常见习语和技巧。让我们开始吧!
什么是 Xpath?
XPath 代表“XML 路径语言”,本质上意味着它是一种查询语言,用于描述 XML/HTML 类型文档从 A 点到 B 点的路径。 您可能知道的其他路径语言是 CSS 选择器,它通常描述应用样式的路径,或者特定于工具的语言,如jq,它描述 JSON 类型文档的路径。 对于 HTML 解析,Xpath 比 CSS 选择器有一些优势:
- Xpath 可以在各个方向遍历 HTML 树并且是位置感知的。
- Xpath 可以在返回结果之前对其进行转换。
- Xpath 很容易扩展附加功能。
在我们深入研究 Xpath 之前,让我们快速概述一下 HTML 本身,以及它如何使 xpath 语言能够使用正确的指令找到任何东西。
HTML 概述
HTML(超文本标记语言)旨在易于机器阅读和解析。换句话说,HTML 遵循节点及其属性的树状结构,我们可以轻松地以编程方式对其进行导航。 让我们从一个小示例页面开始并说明其结构:
<head> <title> </title> </head> <body> <h1>Introduction</h1> <div> <p>some description text: /p> <a class="link" href="https://example.com">example link</a> </div> </body>
在这个简单网页的基本示例中,我们可以看到文档已经类似于数据树。让我们更进一步说明这一点: 在这里,我们可以更轻松地理解它:它是一棵节点树,每个节点还可以附加属性,如关键字属性(如和)和class
自然href
属性(如文本)。 现在我们已经熟悉了 HTML,让我们来熟悉一下 Xpath 本身吧!
Xpath 语法概述
本文中我们仅以我们网站jingzhengli.com为例,实际运用中你可替换为你需要的任意网站
Xpath 选择器通常称为“xpath”,单个 xpath 表示从根到所需端点的目的地。这是一种相当独特的路径语言,所以让我们从快速浏览基本语法开始。 网络抓取中的平均 xpath 选择器通常看起来像这样:
在此示例中,XPath 将选择具有类“按钮”的节点href
的属性,该类也在节点的正下方:<a>
<div>
<div> <a class="button" href="https://www.jingzhengli.com/">jingzhengli</a> </div>
//div/a[contains(@class, "button")]/@href
https://www.jingzhengli.com/CSS
ERROR: Syntax error, unrecognized expression: //div/a[contains(@class, "button")]/@href
Xpath 选择器由多个表达式组成,这些表达式连接在一起成为一个字符串。让我们看看这个 XPath 备忘单中最常用的表达式:
表达 | 描述 |
---|---|
/node |
选择与节点名称匹配的直接子节点。 |
//node |
选择任何后代——孩子、孙子、曾孙等。 |
* |
可以使用通配符代替节点名称 |
@ |
选择节点的属性,例如a/@href 将选择节点href 的属性值a |
text() |
选择节点的文本值 |
[] |
选择器约束 – 可用于过滤掉不符合某些条件的节点 |
parent or .. |
选择当前节点的父节点,例如/div/a/.. 将选择div 元素 |
self or . |
选择当前节点(这在 xpath 函数中作为参数很有用,我们稍后会介绍更多) |
following-sibling::node |
选择所有以下类型的兄弟姐妹,例如following-sibling::div 将选择div 当前节点下的所有兄弟姐妹 |
preceding-sibling::node |
选择类型的所有前面的兄弟姐妹,例如preceding-sibling::div 将选择div 当前节点下面的所有兄弟姐妹 |
function() |
调用已注册的 xpath 函数,例如/a/text() 将返回a 节点的文本值 |
这份 XPath 备忘单可能需要大量的知识——所以,让我们通过一些现实生活中的例子来巩固这些知识。
基本导航
在编写 xpaths 时,我们应该注意的第一件事是所有 xpaths 都必须有一个根(又名点 A)和最终目标(又名点 B)。知道这个和 xpath 轴语法,我们可以开始描述我们的选择器路径:
<div> <p class="socials"> Follow us on <a href="https://twitter.com/@jingzhengli_dev">Twitter!</a> </p> </div>
div/p/a
xpath:
<a href="https://twitter.com/@jingzhengli_dev">Twitter!</a>
CSS: div/p/a
ERROR: Syntax error, unrecognized expression: div/p/a
在这里,我们的简单 xpath 简单地描述了从根到节点的路径a
。 我们只使用/
直接子语法,但是对于大文档,直接 xpath 通常不可靠,因为对树结构或顺序的任何更改都会破坏我们的路径。 最好围绕一些智能上下文设计我们的 xpath。例如,在这里我们可以看到这个<a>
节点在<p class="socials">
node 下——我们可以强烈推断这两个节点很可能会放在一起:
<div> <p class="socials"> Follow us on <a href="https://twitter.com/@jingzhengli_dev">Twitter!</a> </p> </div>
//p[@class='socials']/a
xpath
<a href="https://twitter.com/@jingzhengli_dev">Twitter!</a>
CSS
ERROR: Syntax error, unrecognized expression: //p[@class='socials']/a
有了这个 xpath,我们摆脱了很多结构依赖,有利于上下文。通常,现代网站的上下文比结构稳定得多,在上下文和结构之间找到正确的平衡是创建可靠 xpath 的关键! 此外,我们可以将约束选项 ( []
) 与值测试函数结合起来,例如contains()
使我们的 xpath 更加可靠:
<div> <p class="socials"> Follow us on <a href="https://twitter.com/@jingzhengli_dev">Twitter!</a> </p> </div>
//p[@class='socials']/a[contains(@href, 'twitter.com')]
<a href="https://twitter.com/@jingzhengli_dev">Twitter!</a>CSS
ERROR: Syntax error, unrecognized expression: //p[@class='socials']/a[contains(@href, 'twitter.com')]使用 XPath
contains()
文本函数,我们可以过滤掉任何不包含文本的结果。 Xpath 函数非常强大,不仅可以检查真实性,还可以在运行时修改内容:
<div> <p class="socials"> Follow us on <a href="https://twitter.com/@jingzhengli_dev">Twitter!</a> or connect with us on <a href="https://www.linkedin.com/company/jingzhengli/">Linkedin</a> </p> </div>
//p[@class="socials"]/a/concat(text(), ": ", @href)
Twitter!: https://twitter.com/@jingzhengli_dev Linkedin: https://www.linkedin.com/company/jingzhengli/CSS
ERROR: Syntax error, unrecognized expression: //p[@class="socials"]/a/concat(text(), ": ", @href)在这里,我们添加了
concat()
函数,它将所有提供的参数连接成一个值,然后才执行我们的匹配检查。
导航复杂结构
有时树的复杂性超过了基于上下文的选择器,我们必须实施一些复杂的结构检查。为此,xpath 具有强大的树导航功能,允许选择任何级别的祖先和兄弟姐妹:<div> <span>For price contact </span> <a>Sales department </a> <div> <span>total: </span> </div> <span>166.00$</span> <span>*taxes apply</span> </div>
//span[contains(text(), 'items:')]/following-sibling::span[position()>1 and position()<last()]/text() | //span[contains(text(), 'addons')]/following-sibling::span/text()xpath
1. 166.00$ 2. 25.00$ 3. 0.5$CSS
ERROR: Syntax error, unrecognized expression: //span[contains(text(), 'items:')]/following-sibling::span[position()>1 and position()<last()]/text() | //span[contains(text(), 'addons')]/following-sibling::span/text()在此示例中,我们使用
position()
函数仅选择特定范围内的兄弟姐妹。|
我们还使用运算符(or
可以使用 for 或运算符)结合到 xpaths以完全检索所有定价信息。 如您所见,如果我们对路径逻辑有创意,xpaths 可以非常强大并解析几乎任何 html 结构!
扩展功能
大多数客户端中的 Xpath 可以扩展额外的功能,有些客户端甚至带有预注册的非标准功能。 例如,在 Python 的lxml(以及它基于parsel 的包)中,我们可以像这样轻松地注册新函数:from lxml import etree def myfunc(context, *args): return True xpath_namespace = etree.FunctionNamespace(None) xpath_namespace['myfunc'] = myfunc其他语言客户端遵循类似的过程。
Xpath 客户端
几乎每种编程语言都包含某种用于 XML 文件解析的 xpath 客户端。由于 HTML 只是 XML 的一个子集,我们几乎可以在所有现代语言中安全地使用 xpath!Python 中的 Xpath
在 Python 中,有多个包实现了 xpath 功能,但是它们中的大多数都基于lxml包,它是libxml2和libxslt C 语言库的 pythonic 绑定。这意味着 Python 中的 Xpath 选择器速度非常快,因为它在后台使用了强大的 C 组件。 虽然lxml是一个很好的包装器,但它缺乏网络抓取中使用的许多现代 API 可用性功能。为此,基于 lxml 的包parsel (由scrapy使用)和pyquery提供了更丰富的功能集。 使用parsel的示例用法:from parsel import Selector html = """ <html> <head> <title>Page Title</title> </head> <body> <h1 class="title">Page header by <a href="#">company</a></h1> <p>This is the first paragraph</p> <p>This is the second paragraph</p> </body> </html> """ sel = Selector(html) sel.xpath("//p").getall() # [ # "<p>This is the first paragraph</p>", # "<p>This is the second paragraph</p>", # ]其他工具推荐:
- cssselect – 将 css 选择器转换为 xpath 选择器
- parsel-cli – 用于测试 css/xpath 选择器的实时 REPL
PHP 中的 Xpath
在 PHP 中,最流行的 xpath 处理器是 Symphony 的DomCrawler:use Symfony\Component\DomCrawler\Crawler; $html = <<<'HTML' <html> <head> <title>Page Title</title> </head> <body> <h1 class="title">Page header by <a href="#">company</a></h1> <p>This is the first paragraph</p> <p>This is the second paragraph</p> </body> </html> HTML; $crawler = new Crawler($html); $crawler->filterXPath('//p');其他工具推荐:
- css-selector – 将 css 选择器转换为 xpath 选择器
Javascript 中的 Xpath
Javascript 原生支持 xpath,您可以在MDN 的 Introduction to Using Xpath in Javascript中阅读更多相关信息 其他工具推荐:其他语言的 Xpath
大多数其他语言都有某种 XPath 客户端,因为 XML 解析是一项重要的数据交换功能。意思是,我们可以用我们选择的语言解析网络抓取的内容! 例如,C# 本身也支持 XPath,您可以在官方文档中阅读更多相关信息,Objecive C 和其他低级语言也是如此。 虽然某些语言可能没有第一方 XPath 客户端,但很可能有一个社区包。例如,Go 语言在 xml、html 甚至 json 中都有 xpath 的社区包。浏览器自动化中的 Xpath
浏览器自动化工具支持 XPath,无需任何额外的 Javascript 包。 要在Selenium中访问 XPath,我们可以使用by.XPath
选择器。例如,对于 Python 中的 Selenium:
from selenium import webdriver from selenium.webdriver.common.by import By driver = webdriver.Chrome() driver.get("https://httpbin.org/html") element = driver.find_element(By.XPATH, '//p')要在Playwright中访问 XPath,我们可以使用定位器功能,该功能将 CSS 选择器或 XPath 作为参数。例如在 Playwright 和 Python 中:
from playwright.sync_api import sync_playwright with sync_playwright() as pw: browser = pw.chromium.launch(headless=False) context = browser.new_context(viewport={"width": 1920, "height": 1080}) page = context.new_page() page.goto("http://httpbin.org/html") paragraphs = page.locator("//p") # this can also take CSS selectors要在Puppeteer中访问 XPath ,我们可以使用
$
和$$
方法。例如在 Javascript 中的 Puppeteer 中:
const puppeteer = require('puppeteer') const browser = await puppeteer.launch(); let page = await browser.newPage(); await page.goto('http://httpbin.org/html'); await page.$("//p");
常问问题
为了结束这个介绍,让我们看一下有关使用 XPath 选择器进行 HTML 解析的一些常见问题:XPATH 比 CSS 选择器快吗?
许多 CSS 选择器库将 CSS 选择器转换为 XPATH,因为它更快、更强大。话虽如此,这取决于每个单独的库和选择器本身的复杂性。一些使用广泛选择路径的 XPATH 选择器//
在计算上可能非常昂贵。
我的 xpath 选择了比它应该多的数据
XPATH 广泛的选择器路径//
是全局的而不是相对的。要使它们相对,我们必须添加相对性标记.
->.//
如何通过多个名称匹配节点?
要通过多个名称匹配节点,我们可以使用通配符选择器和名称检查条件://*[contains("p h1 head", name())]
– 将选择 h1、p 和头节点。
如何选择两个节点之间的选择元素?
如果我们知道两个节点,如文本标题,我们可以巧妙地使用符号来选择文本preceding-sibling
: //h2[@id="faq"]//following-sibling::p[(preceding-sibling::h2[1])[@id="faq"]]
– 这个 xpath 选择 h2 标签下的所有段落节点faq
,而不是其他h2
节点下的元素。
<div> <span>items: </span> <span>(taxes not included)</span> <span>166.00$</span> <span>25.00$</span> <span>*taxes apply</span> <div> <span>addons:</span> <span>0.5$</span> </div> </div>
//span[contains(text(), 'items:')]/following-sibling::span[position()>1 and position()<last()]/text() | //span[contains(text(), 'addons')]/following-sibling::span/text()xpath
1. 166.00$ 2. 25.00$ 3. 0.5$CSS
ERROR: Syntax error, unrecognized expression: //span[contains(text(), 'items:')]/following-sibling::span[position()>1 and position()<last()]/text() | //span[contains(text(), 'addons')]/following-sibling::span/text()