futureplc/html-dom-document

用于处理HTML5文档的DOMDocument的替代品。

v1.0.0 2024-08-30 12:06 UTC

README

Latest Version on Packagist Tests Total Downloads

HTMLDocument包的主要目的是作为一个替代品来替换PHP的核心DOMDocument和相关DOM类。

⚠️ 如果您只需要爬取DOM而不需要就地操作,请考虑使用Symfony DOM Crawler组件

虽然PHP内置的DOM相关类是解析XML的好方法,但当尝试解析现代HTML5标记时,它们会迅速崩溃。此包使操作更加直观,并处理了一些幕后的问题。

此包提供了一系列类,以向后兼容的方式替换DOM类,但具有更紧密的接口,并捆绑了额外的实用程序,以便轻松处理HTML。这些类将返回等效的HTML*类的实例,而不是DOM*

  • DOMDocument -> HTMLDocument
  • DOMElement -> HTMLElement
  • DOMNode -> HTMLElement
  • DOMText -> HTMLText
  • DOMNodeList -> HTMLNodeList
  • DOMXPath -> HTMLXPath

安装

您可以通过Composer安装此包

composer require futureplc/html-dom-document

功能

合理的返回值

由于PHP使用虚假返回类型的传统,每次操作都需要检查联合类型,这真是令人烦恼。我们已经通过确保有合理的默认值来解决这个问题。

  • 如果返回值期望DOMNodeListfalse,则在没有返回值的情况下返回一个空的DOMNodeList
  • 如果返回值可以是stringfalse,则失败时抛出异常或返回一个空字符串
  • 不再区分DOMNodeDOMElement;我们有一个单一的HTMLElement类来处理这两种情况的组合

您会在整个接口中注意到这种理念 - 如果有合理的类型可以返回,我们将确保您得到它,而不是处理联合。

轻松创建HTML文档和元素

DOMDocument通常具有简短、过时的接口,需要大量的设置和重复操作才能完成基本和常用任务,例如从纯HTML字符串创建DOMElement类。

所有旧的DOMDocument风格方法仍然有效,因此您可以将此包作为现有DOMDocument实现的替代品。但是,我们已经添加了新的方法来创建HTML文档和元素,而无需通常用于某些操作的冗长性。

$dom = new HTMLDocument(); $dom->loadHTML($html);
$dom = HTMLDocument::fromHTML($html);
$dom = HTMLDocument::loadFromFile($filePath);

$element = HTMLElement::fromNode($domNode);
$element = HTMLElement::fromHTML($html);

$element = $dom->createElement('p', 'This is a paragraph.');
$element = $dom->createElementFromNode($domNode);
$element = $dom->createElementFromHTML('<p>This is a paragraph.</p>');

支持HTML5的附加行为

允许DOMDocument解析任何HTML字符串的大多数自定义行为来自一系列“中间件”类,它们在加载之前和再次作为纯HTML字符串发布之前操作HTML。

这些中间件执行各种操作,例如

  • 如果没有<!doctype>,则假定HTML5行为,通过添加一个
  • 忽略LibXML错误(因为LibXML会对某些HTML5标签提出异议,尽管它可以正确解析它们)
  • <template><script>标签作为字面量处理,以便它们的内容不会受到整个文档的影响

如果您使用HTMLDocument类,这些功能将默认启用,但根据需要您可以禁用它们。

  • 在加载HTML之前,不带任何参数调用->withoutMiddleware()将导致没有任何中间件应用,实际上只应用了额外的实用方法,而没有应用任何额外的HTML5支持。
  • 使用中间件类的名称调用->withoutMiddleware(MiddlewareName::class),将禁用该特定的中间件。

DOMDocument获取纯HTML字符串可能会有些棘手,如果您需要特定元素,我们添加了一些选项以简化操作。

$html = (string) $dom; // Cast the HTMLDocument to a string
$html = $dom->saveHTML();

$html = (string) $element; // Cast the HTMLElement to a string
$html = $element->saveHTML();
$html = $element->getInnerHTML(); // Gets the HTML of the element without the wrapping node
$html = $element->getOuterHTML(); // Gets the HTML of the element with the wrapping node

检查HTML5

如果您需要知道是否正在处理HTML5文档,isHTML5()方法将告诉您。

$dom->isHtml5(); // true

空元素

如果您正在使用HTML5,您可能想知道一个给定的节点是否是“空元素”,这意味着它不需要关闭标签。这可以通过isVoidElement()方法进行检查。

$element->isVoidElement(); // true

通常在保存HTML时,DOMDocument会将空元素输出为<example></example>,但此包将它们输出为<example>,即使是自定义元素,也保持它们原始的输入方式。

处理属性

HTMLElement类有一系列方法来帮助您处理元素上的属性。

$element->getAttributes(); // Returns an array of all attributes
$element->getAttribute('class'); // Returns the value of the class attribute

$element->setAttribute('class', 'foo'); // Sets the class attribute to "foo"
$element->addAttribute('class', 'foo'); // Adds the "foo" value as a space-separated value to the class attribute, appending it if the attribute already exists

$element->removeAttribute('ref'); // Removes the ref attribute entirely
$element->removeAttribute('ref', 'noreferrer'); // Removes the "noreferrer" value from the ref attribute if it exists - if the attribute is now empty, it will be removed entirely

$element->toggleAttribute('checked'); // Toggles the "checked" attribute

由于我们经常在HTML中使用CSS类,因此也有一些方法可以帮助处理。

$element->getClassList(); // Returns an array of CSS classes
$element->setClassList(['foo', 'bar']); // Sets the CSS classes
$element->hasClass('foo'); // Returns true if the element has the class "foo"
$element->addClass('baz'); // Adds the class "baz"
$element->removeClass('bar'); // Removes the class "bar"

删除文档的部分

有一些实用的工具可以快速删除文档的特定部分。

$element->wihoutSelector('p'); // Removes all child `<p>` element
$element->withoutComments(); // Removes all HTML comments

实用方法

有一些额外的实用方法可以帮助从PHP数组构建属性字符串。

Utility::attribute()将单个键/值对转换为HTML属性,无论值是字符串、数组还是布尔值。布尔值可以用于有条件地添加属性。

Utility::attribute('class', ['foo', 'bar']); // class="foo bar"
Utility::attribute('id', 'baz'); // id="baz"
Utility::attribute('required', true); // disabled

Utility::attributes()将更进一步,将键/值对数组转换为HTML属性字符串。

Utility::attributes([
    'class' => ['foo', 'bar'],
    'id' => 'baz',
    'required' => true,
    'checked' => false,
]);

// class="foo bar" id="baz" required

Utility::nodeMapRecursive()允许对文档中的每个节点(包括所有子节点)运行回调。您可以使用此回调检查节点、修改节点、用另一个节点完全替换节点或从文档中删除节点。

此功能也适用于HTMLElementHTMLDocument对象,通过mapRecursive方法实现。

$dom = HTMLDocument::fromHTML('<p><span>foo</span></p>');

// Make sure every element has a class of "bar"
$dom->mapRecursive(function ($node) {
    if ($node instanceof HTMLElement) {
        $node->setAttribute('class', 'bar');
    }
});

// <p class="bar"><span class="bar">foo</span></p>

Utility::countRootNodes()将告诉您文档中有多少根节点。

Utility::countRootNodes('<p>foo</p>'); // 1
Utility::countRootNodes('<p>foo</p><p>bar</p>'); // 2

如果您正在处理包含多个根节点的源HTML,您可以使用Utility::wrap($html)Utility::unwrap($html)方法确保有一个根节点或删除根节点。

处理CSS类

HTMLElement类有几种方法可以帮助您处理CSS类。

$element->setClassList(['foo', 'bar']);
$element->getClassList(); // ['foo', 'bar']
$element->hasClass('foo'); // true
$element->addClass('foo'); // ['foo', 'bar', 'baz']
$element->removeClass('baz'); // ['foo', 'bar']

切换布尔属性

如果您需要切换某些布尔属性的开/关,则可以使用toggleAttribute()方法。

$element = HTMLElement::fromString('<input type="checkbox">');
$element->toggleAttribute('checked'); // <input type="checkbox" checked>
$element->toggleAttribute('checked'); // <input type="checkbox">

基于CSS选择器和XPath进行查询

大多数处理HTML的人都知道如何使用大多数CSS选择器,但许多人从未接触过XPath。我们已向HTMLDocumentHTMLElement类添加了方便的querySelector()querySelectorAll()方法,允许您直接使用CSS选择器获取所需元素,这是由Symfony CSS Selector包提供的。

$dom->querySelector('head > title'); // Returns the first `<title>` element
$dom->querySelectorAll('.foo'); // Returns all elements with the class `foo`

如果您仍然需要使用XPath,则在HTMLDocumentHTMLElement类上都有方便的xpath()方法。

$dom->xpath('//a'); // Returns all `<a>` elements

处理文本节点

如果要在文本中更改某些内容为另一个节点,处理文本节点可能会很棘手。在HTMLText上的replaceTextWithNode()方法允许您做到这一点。

如果您使用Utility::nodeMapRecursive()函数,这将特别有用,该函数将遍历文本节点。

$textNode->replaceTextWithNode('example', HTMLElement::fromHTML('<strong>example</strong>'));

其他说明

HTMLDocument 还有一些比 DOMDocument 更多的好处

  • 具有 XML 风格命名空间的标签会被保留,而 DOMDocument 通常只会保留标签名的最后部分。这在处理边侧包含(edge-side-includes)等标准以及包含如 <esi:include src="..." /> 的标记时很有用
  • @ 开头的属性会被保留,而 DOMDocument 通常会移除它们。当处理包含 Alpine.js 或 Vue.js 标记的 HTML 时(例如 <button @click="doSomething">)非常有用
  • 输入 HTML 中的任何空标签也将被输出为空标签

缺点

由于所有额外的检查和数据类型转换,这个包比本机的 DOMDocument 类稍微慢一些。然而,在大多数情况下,这种差异微不足道,额外功能和易用性的好处远远超过了性能损失,除非您一次处理数百万个大型 HTML 文档。

测试

composer test

变更日志

请参阅 变更日志 了解最近有哪些变化。

贡献

请参阅 贡献指南 了解详细信息。

安全漏洞

请查看 我们的安全策略 了解如何报告安全漏洞。

致谢

许可证

MIT 许可证(MIT)。请参阅 许可证文件 了解更多信息。