使用 Symfony 的 CssSelector 组件将 CSS 选择器转换为 XPath 表达式

2025-4-07 19:29:40

为了实现类似 jQuery 的 CSS 选择器语法(如 p.container .para1),我们需要引入一个工具将 CSS 选择器转换为 XPath 表达式。PHP 中可以使用 Symfony 的 CssSelector 组件来完成这一任务。

以下是重写后的 Query 类,支持 CSS 选择器风格的查询:


1. 引入 Symfony CssSelector

首先,确保安装了 Symfony 的 CssSelector 组件。可以通过 Composer 安装:

composer require symfony/css-selector

2. 修改 Query 类以支持 CSS 选择器

下面是修改后的完整代码:

<?php

require_once 'vendor/autoload.php'; // 确保加载 Composer 自动加载文件

use SymfonyComponentCssSelectorCssSelectorConverter;

class Query {
    private $dom;
    private $xpath;
    private $nodes = [];

    public function __construct($html = null) {
        $this->dom = new DOMDocument();
        libxml_use_internal_errors(true); // 防止 HTML 解析错误抛出异常
        if ($html) {
            $this->dom->loadHTML($html);
        }
        $this->xpath = new DOMXPath($this->dom);
        libxml_clear_errors();
    }

    // 查找节点,支持 CSS 选择器
    public function find($selector) {
        $converter = new CssSelectorConverter();
        $xpathExpression = $converter->toXPath($selector);
        $this->nodes = $this->xpath->query($xpathExpression);
        return $this; // 支持链式调用
    }

    // 获取所有匹配节点的文本内容
    public function text() {
        $texts = [];
        foreach ($this->nodes as $node) {
            $texts[] = $node->nodeValue;
        }
        return $texts;
    }

    // 获取第一个匹配节点的文本内容
    public function firstText() {
        if ($this->nodes->length > 0) {
            return $this->nodes->item(0)->nodeValue;
        }
        return null;
    }

    // 获取或设置 HTML 内容
    public function html($newHtml = null) {
        if ($newHtml !== null) {
            foreach ($this->nodes as $node) {
                // 清空节点现有内容
                while ($node->firstChild) {
                    $node->removeChild($node->firstChild);
                }
                // 添加新的 HTML 内容
                $fragment = $this->dom->createDocumentFragment();
                $fragment->appendXML($newHtml);
                $node->appendChild($fragment);
            }
            return $this;
        }
        $htmls = [];
        foreach ($this->nodes as $node) {
            $htmls[] = $this->dom->saveHTML($node);
        }
        return $htmls;
    }

    // 获取或设置属性值
    public function attr($attribute, $value = null) {
        if ($value !== null) {
            foreach ($this->nodes as $node) {
                $node->setAttribute($attribute, $value);
            }
            return $this;
        }
        $attrs = [];
        foreach ($this->nodes as $node) {
            if ($node->hasAttribute($attribute)) {
                $attrs[] = $node->getAttribute($attribute);
            }
        }
        return $attrs;
    }

    // 向匹配节点追加内容
    public function append($html) {
        foreach ($this->nodes as $node) {
            $fragment = $this->dom->createDocumentFragment();
            $fragment->appendXML($html);
            $node->appendChild($fragment);
        }
        return $this;
    }

    // 删除匹配的节点
    public function remove() {
        foreach ($this->nodes as $node) {
            if ($node->parentNode) {
                $node->parentNode->removeChild($node);
            }
        }
        return $this;
    }

    // 输出完整的 HTML
    public function saveHTML() {
        return $this->dom->saveHTML();
    }
}

3. 使用示例

假设我们有以下 HTML 内容:

$html = <<<HTML
<html>
<body>
    <div class="container">
        <p id="para1" class="text">Hello, World!</p>
        <p id="para2" class="text">This is a test.</p>
        <a href="https://example.com" title="Example Link">Click Me</a>
    </div>
</body>
</html>
HTML;

$query = new Query($html);

// 示例 1: 查找所有 p 标签并获取它们的文本内容
$texts = $query->find('p')->text();
echo "All paragraph texts:n";
print_r($texts);

// 示例 2: 查找第一个 p 标签的文本内容
$firstText = $query->find('p')->firstText();
echo "First paragraph text: $firstTextn";

// 示例 3: 通过类名查找段落
$textsByClass = $query->find('.container .text')->text();
echo "Texts by class selector:n";
print_r($textsByClass);

// 示例 4: 通过 ID 查找段落
$textById = $query->find('#para1')->text();
echo "Text by ID selector:n";
print_r($textById);

// 示例 5: 修改第一个 p 标签的内容
$query->find('p#para1')->html('<strong>New Content</strong>');
echo "Modified HTML:n";
echo $query->saveHTML();

// 示例 6: 获取所有链接的 href 属性
$hrefs = $query->find('a')->attr('href');
echo "All href attributes:n";
print_r($hrefs);

// 示例 7: 删除第二个 p 标签
$query->find('p#para2')->remove();
echo "HTML after removing second paragraph:n";
echo $query->saveHTML();

4. 结果解释

示例 1: 查找所有 p 标签

All paragraph texts:
Array
(
    [0] => Hello, World!
    [1] => This is a test.
)

示例 2: 查找第一个 p 标签

First paragraph text: Hello, World!

示例 3: 通过类名 .container .text 查找

Texts by class selector:
Array
(
    [0] => Hello, World!
    [1] => This is a test.
)

示例 4: 通过 ID #para1 查找

Text by ID selector:
Array
(
    [0] => Hello, World!
)

示例 5: 修改第一个段落内容

修改后:

<p id="para1" class="text"><strong>New Content</strong></p>

示例 6: 获取所有链接的 href 属性

All href attributes:
Array
(
    [0] => https://example.com
)

示例 7: 删除第二个段落

删除后:

<div class="container">
    <p id="para1" class="text"><strong>New Content</strong></p>
    <a href="https://example.com" title="Example Link">Click Me</a>
</div>

5. 总结

通过引入 Symfony 的 CssSelector 组件,我们可以轻松地将 CSS 选择器转换为 XPath 表达式,从而实现类似 jQuery 的查询方式。这个改进让 Query 类更加灵活和易用,支持以下功能:

  • CSS 选择器:如 p.class#id.container .text
  • 文本提取text()firstText()
  • HTML 操作html()
  • 属性操作attr()
  • 节点添加/删除append()remove()

这种封装方式极大地简化了 PHP 中的 DOM 操作!

版权声明:本文内容结合人工智能完成,对于内容的准确性和完整性我们不做保证,也不代表本站的态度或观点。本文内容版权归属相关权利人(第三方权利人或找找网)。如若内容造成侵权/违法违规,请联系我们删除!

文章标签: