使用 PHP 8.4 新 DOM Selector 解析 HTML
上个月发布的 PHP 8.4 为 HTML 解析、DOM 遍历和操作带来了三大改进:
- 新的 HTML5 解析器可准确处理现代网络内容
- 强大的 CSS 选择器支持元素检索
- 更符合 DOM 规范的新 DOM 类
对于从事web搜刮、内容提取或 HTML 转换的开发人员来说,这些功能和性能都有了显著提升。
在 PHP 8.4 发布的报道中,这些功能并没有得到应有的关注。而且 PHP 网站上的文档仍然很少。最近,我开始更新 Mozilla 的 Readability 的 PHP 移植,以使用这些新功能,因此想与大家分享更多信息。
技术基础
这些改进的核心是 Lexbor,它是由 Alexander Borisov 创建的基于 C 语言的 HTML 解析器。它提供快速、符合标准的 HTML 解析和 CSS 选择器支持。现在,PHP 8.4 的官方 DOM 扩展中包含了它,默认情况下已启用,无需额外配置。

新的 DOM 类更贴近 DOM 规范。如果你熟悉 JavaScript 中的 DOM 遍历和操作,你会发现许多熟悉的方法和属性现在都可以在 PHP 中使用,包括 querySelector 和 querySelectorAll。
旧办法: 使用 libxml 进行解析
PHP 以前依赖 libxml 来解析 XML 和 HTML。遗憾的是,libxml 难以处理现代 HTML,许多页面被解析器弄得一团糟。让我们看一个简单的例子来说明这个问题。
这是一个有效的 HTML5 文档,包含两个段落和一个脚本标记:
<!DOCTYPE html>
<title>Valid HTML5 Document</title>
<p>Paragraph 1</p>
<script>console.log("</html>Console log text");</script>
<p>Paragraph 2</p>
当尝试解析该文档并计算其段落数时,PHP 发现了三个元素,而不是两个:
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->loadHtml($html);
$paragraphs = $dom->getElementsByTagName('p');
echo "{$paragraphs->length} paragraphs found.";
// Output: 3 paragraphs found.
为什么找到的是三段而不是两段?脚本元素中出现的 </html> 会干扰 libxml 解析器。libxml 没有将其视为脚本中的文本,而是将其解释为 HTML 结尾标记。当我们将生成的 DOM 序列化为 HTML 时,我们可以看到文档是如何被篡改的:
<html>
<body>
<p>Paragraph 1</p>
<script>console.log("</script>
</body>
</html>
<html>
<p>Console log text");</p>
<p>Paragraph 2</p>
</html>
为了克服这些限制,许多开发人员转而使用其他解析器。HTML5-PHP 很受欢迎,但它是用 PHP 而不是 C 编写的,因此速度明显比 libxml 慢。此外,目前还不清楚它在跟上 HTML 生活标准方面付出了多少努力(详情见下文)。
新方法: 使用 Lexbor
进行解析 PHP 8.4 通过新的 HTML5 解析器解决了这些解析难题。让我们用新的解析器来解析相同的 HTML:
$newDom = Dom\HTMLDocument::createFromString($html);
$paragraphs = $newDom->getElementsByTagName('p');
echo "{$paragraphs->length} paragraphs found.";
// Output: 2 paragraphs found.
现在,解析器可以正确识别两个段落。您可以在这里尝试运行新旧解析器。
根据负责这些新添加功能的 Niels Dossche 的说法,新解析器的性能与 libxml 解析器不相上下,甚至还要更快一些。
Lexbor 与 HTML5-PHP
对于当前的 HTML5-PHP 用户来说,改用新的 DOM API 和解析器具有一些优势。
性能
用 C 语言编写的 Lexbor 性能应该比 HTML5-PHP 好得多。在我的测试中,处理包含博客文章和新闻文章的 HTML 页面时,Lexbor 的速度要快 3.6 倍。尼尔斯认为,在处理较大的 HTML 文档时,速度优势会更加明显。
符合标准
HTML 规范是一个不断发展的活标准,解析器在执行当前标准时可能会有所不同。
HTML5-PHP 于 2013 年启动,其 README 仍然引用 2012 年版本的 W3C HTML5 标准。Lexbor 于 2018 年启动,基于较新的 WHATWG 标准,而 WHATWG 现在是 HTML 标准的唯一发布者。因此,Lexbor 可能比 HTML5-PHP 更接近当前标准。
另外值得注意的是,HTML5-PHP 目前依赖于 PHP 的旧 DOM 类,而这些类并不支持本文其余部分介绍的 PHP 新 DOM API 的改进功能。
使用新的 DOM 类
为了实现向后兼容性,PHP 8.4 在现有 DOM 类的基础上引入了新的 DOM 类。这意味着您可以根据需要继续使用 DOMDocument,甚至可以在同一代码库中同时使用新旧两个类。
以下是开始使用的方法:
$dom = Dom\HTMLDocument::createFromString($html);
在 DOM 命名空间下,新类遵循更简单的命名约定:
作为 DOM 属性的顶级 HTML 元素
现在,您可以通过这些方便的 Dom\Document 属性访问 HTML 文档的主要部分:
- head (只读)
“作为 html 元素子元素的第一个头部元素。这些元素必须位于 HTML 名称空间中。如果没有匹配的元素,则评估值为空。” - body
“是 body 标记或 frameset 标记的 html 元素的第一个子元素。这些元素必须在 HTML 名称空间中。如果没有匹配的元素,则其值为空。” - title
“由 HTML 的 title 元素或 SVG 的 SVG title 元素设置的文档标题。如果没有标题,则返回空字符串”。
示例
$dom = Dom\HTMLDocument::createFromString('<p>My document</p>');
echo $dom->saveHtml($dom->body);
// Output: <body><p>My document</p></body>
$dom->title = 'My title';
echo $dom->saveHtml($dom->head);
// Output: <head><title>My title</title></head>
使用 innerHTML
PHP 8.4 还引入了 innerHTML 属性,它为使用元素内容提供了更简便的方法。无需直接操作 DOM 节点,而是使用 HTML 字符串:
$dom = Dom\HTMLDocument::createFromString('<body><h1>Test</h1></body>');
echo $dom->body->innerHTML;
// Output: <h1>Test</h1>
$dom->body->innerHTML = '<p>Something new</p>';
echo $dom->saveHtml();
// Output: <html><head></head><body><p>Something new</p></body></html>
请注意,目前还不支持 outerHTML。
现代 CSS 选择器支持
PHP 8.4 最强大的新增功能之一是对现代 CSS 选择器的全面支持。现在,您可以使用 querySelector
和 querySelectorAll
,使用您在前端开发中熟悉的选择器查找元素:
- querySelector($selectors)
“返回与 CSS 选择器匹配的第一个后代元素” - querySelectorAll($selectors)
“返回一个 NodeList,其中包含与 CSS 选择器匹配的所有后代元素”
下面是之前获取段落的代码,但用 querySelectorAll
代替了 getElementsByTagName
:
$newDom = Dom\HTMLDocument::createFromString($html);
$paragraphs = $newDom->querySelectorAll('p');
echo "{$paragraphs->length} paragraphs found.";
这样做的结果和以前一样,并不显著。但新的选择器支持可以实现更复杂的查询。让我们来看看一些实际的例子:
查找多种元素类型
获取所有段落和标题元素–按文档顺序返回:
$elements = $dom->querySelectorAll('p, h1, h2, h3, h4, h5, h6');
使用 :is 和 :where 避免重复
获取文章的直接子段落和主要标题:
$elements = $dom->querySelectorAll('article > :is(p, h1, h2)');
您还可以将搜索范围缩小到特定元素:
$elements = $dom->querySelector('article')->querySelectorAll('p, h1, h2');
请注意,这在技术上并不等同于之前的代码,因为我们并没有将结果限制为直接子代。为此,我们需要使用 :scope
选择器,而 Lexbor 目前还不支持该选择器:
$dom->querySelector('article')->querySelectorAll(':scope > :is(p, h1, h2)');
// Throws: DOMException: Invalid selector (Selectors. Not supported: scope)
好消息是,Lexbor 正在审查由 Niels 提供的针对此问题的修复程序。
使用:empty 和 :not查找空或非空元素
获取所有空 p 元素:
$elements = $dom->querySelectorAll('p:empty');
获取所有非空 p 元素:
$elements = $dom->querySelectorAll('p:not(:empty)');
使用:has 匹配父元素或上一个同级元素
获取文章中至少有一个链接的所有段落:
$elements = $dom->querySelectorAll('article p:has(a)');
获取紧接着 h2 标题的 h1 标题:
$elements = $dom->querySelectorAll('h1:has(+ h2)');
属性选择器
获取所有外部链接–以 “http “开头且不包含 “example.com “的 URL,不区分大小写:
$elements = $dom->querySelectorAll(
'a[href ^= "http" i]:not([href *= "example.com" i])'
);
有关可用选择器的更多示例,请参阅 MDN 有关 CSS 选择器和组合器的文档,以及 PHP 的选择器测试文件夹。
你也许感兴趣的:
- 【外评】严重 PHP 漏洞使服务器面临远程代码执行风险
- PHP 不再糟糕
- 短短两年使用率下滑 40%!曾经风靡全球的 PHP 为何逐渐失去优势?
- 短短两年使用率下滑 40%!曾经风靡全球的 PHP 为何逐渐失去优势?
- PHP 8.3 正式发布的主要变化
- PHP 8:类型系统改进
- 为什么在 20 多年后,我仍然爱着 PHP 和 JavaScript
- 全球 77.5% 的网站,都在使用“世界上最好的语言” PHP!
- PHP“垂死”十年
- 微软宣布 Windows 将不提供 PHP 官方支持
你对本文的反应是: