主页/PHP笔记/PHP问答/框架Symfony/Symfony DomCrawler 安装用法及核心功能详解

Symfony DomCrawler 安装用法及核心功能详解

Symfony DomCrawler 是一个强大的 PHP 组件,用于解析和操作 HTML/XML 文档。它支持 XPath 和 CSS 选择器,并集成了 HTML5 解析能力。本教程将涵盖安装、核心功能及实际应用场景。

背景

本教程于2025年4月27日整理定稿,以下教程是基于这个时间点 Symfony DomCrawler 官方说明及网络教程整理。对于本教程未提及的方法不建议使用(或使用前鉴定或验证有效性)。

本教程已经将一些疑点验证,并备注说明。


一、安装

通过 Composer 安装组件:

composer require symfony/dom-crawler

若需支持 HTML5 解析(推荐):

composer require masterminds/html5

若需要使用css选择器(推荐):

composer require symfony/css-selector

二、基础用法

1. 初始化 Crawler

use SymfonyComponentDomCrawlerCrawler;

$html = <<<HTML
<!DOCTYPE html>
<html>
  <body>
    <h1 class="title">Symfony DomCrawler</h1>
  </body>
</html>
HTML;

$crawler = new Crawler($html);

三、节点筛选

1. CSS 选择器

注:关于 Symfony DomCrawler 使用 CSS 选择器必须额外安装 symfony/css-selector

$titles = $crawler->filter('h1.title');

2. XPath 表达式

$titles = $crawler->filterXPath('//h[contains(@class, "title")]');

3.筛选更复杂的条件

匿名函数可用于筛选更复杂的条件:

use SymfonyComponentDomCrawlerCrawler;
// ...

$crawler = $crawler
    ->filter('body > p')
    ->reduce(function (Crawler $node, $i): bool {
        // filters every other node
        return ($i % 2) === 0;
    });

要删除节点,匿名函数必须返回 false

所有筛选方法都会返回一个包含筛选内容的新 Crawler 实例。要检查过滤器是否真的 找到了一些东西,使用 $crawler->count() > 0 来判断


filterXPath() 和 filter() 方法都可以使用 XML 命名空间,可以自动发现或注册目标。

四、节点遍历

Access 节点(按其在列表中的位置):

$crawler->filter('body > p')->eq(0);

获取当前选择的第一个或最后一个节点:

$crawler->filter('body > p')->first(); // 备注:first() 的括号中建议留空,因为不支持再进行进一步定位
$crawler->filter('body > p')->last(); // 备注:last() 的括号中建议留空,因为不支持再进行进一步定位

获取与当前选择相同级别的节点:

$crawler->filter('body > p')->siblings(); // 备注:siblings() 的括号中建议留空,因为不支持再进行进一步定位

在当前选择之后或之前获取相同级别的节点:

$crawler->filter('body > p')->nextAll(); // 备注:nextAll() 的括号中建议留空,因为不支持再进行进一步定位
$crawler->filter('body > p')->previousAll(); // 备注:previousAll() 的括号中建议留空,因为不支持再进行进一步定位

获取所有子节点或上级节点:

$crawler->filter('body')->children();
$crawler->filter('body > p')->ancestors(); 
// 备注:ancestors() 的括号中建议留空,因为不支持再进行进一步定位;并且 ancestors() 方法是获取所有上级节点(并非仅仅是直接上级节点)

获取与 CSS 选择器匹配的所有直接子节点:

$crawler->filter('body')->children('p.lorem');

获取与提供的选择器匹配的元素的第一个父元素(朝向文档根目录):

$crawler->closest('p.lorem');

closest() 方法加强理解使用示例:

$html = <<<'HTML'
<div class="parent">
    <div class="b1">第一个块</div>
    <div class="b2">
        <h2>第二个块</h2>
        <div class="ddf">同级节点1</div>
        <div class="other">不应该出现的<span>节点(上)</span></div>
        <div class="current-node">当前节点</div>
        <div class="other">不应该出现的<span class="dd">节点(下)</span></div>
        <div class="ddf">同级节点2</div>
    </div>
    <div class="b2">
        <h2>模仿第二个块</h2>
        <div class="ddf">模仿第二个块:同级节点1</div>
        <div class="other">模仿第二个块:不应该出现的<span>节点(上)</span></div>
        <div class="current-node">模仿第二个块:当前节点</div>
        <div class="other">模仿第二个块:不应该出现的<span class="dd">节点(下)</span></div>
        <div class="ddf">模仿第二个块:同级节点2</div>
    </div>
</div>
HTML;

$crawler = new Crawler($html);

//示例1
$currentNode = $crawler->filter('.b2');
$siblingTexts = $currentNode->each(function (Crawler $node, $i) {
    return $node->closest('.dd')->text();
});
print_r($siblingTexts);
//打印结果:报错!!!(未捕获的错误:在 “return $node->closest('.dd')” 中的 null 上调用成员函数 text() )

//示例2
$currentNode = $crawler->filter('.current-node');
$siblingTexts = $currentNode->each(function (Crawler $node, $i) {
    return $node->closest('.b2')->text();
});
print_r($siblingTexts);
//打印结果:Array ( [0] => 第二个块 同级节点1 不应该出现的节点(上) 当前节点 不应该出现的节点(下) 同级节点2 [1] => 模仿第二个块 模仿第二个块:同级节点1 模仿第二个块:不应该出现的节点(上) 模仿第二个块:当前节点 模仿第二个块:不应该出现的节点(下) 模仿第二个块:同级节点2 )


/**
 * 总结:
 * $node->closest('.b2')
 * closest() 中的选择器必须是当前选择($node)的父元素的css定位
 * */

所有遍历方法都返回一个新的 Crawler 实例。

上方“备注”由本站长测验所得。


五、访问节点值

// 返回 body 下第一个子元素的节点名称(HTML 标签名称)
$tag = $crawler->filterXPath('//body/*')->nodeName();
// 如果节点不存在,则调用 text() 将导致异常
$message = $crawler->filterXPath('//body/p')->text();

// 避免在 node 不存在时传递 text() 返回的参数的异常
$message = $crawler->filterXPath('//body/p')->text('Default text content');

//默认情况下,text() 会修剪空格字符,包括内部字符
//(例如,"foon bar baz n" 被返回为 “foo bar baz”)
//将 FALSE 作为第二个参数传递,以返回原始文本不变
$crawler->filterXPath('//body/p')->text('Default text content', false);

//innerText() 类似于 text(),但仅返回当前节点的直接后代文本,不包括子节点中的文本.
//如果内容为 <p>Foo <span>Bar</span></p> 或 <p><span>Bar</span> Foo</p> 
// innerText() 在这两种情况下都返回 'Foo';text() 分别返回 'Foo Bar' 和 'Bar Foo'
$text = $crawler->filterXPath('//body/p')->innerText();

//如果有多个文本节点,则在其他子节点之间,例如 <p>Foo <span>Bar</span> Baz</p> ,innerText() 仅返回第一个文本节点 'Foo'
//与 text() 一样,innerText() 也默认修剪空白字符,但是您可以通过将 FALSE 作为参数传递来获取未更改的文本
$text = $crawler->filterXPath('//body/p')->innerText(false);
$class = $crawler->filterXPath('//body/p')->attr('class');
$class = $crawler->filterXPath('//body/p')->attr('class', 'my-default-class');
$attributes = $crawler
    ->filterXpath('//body/p')
    ->extract(['_name', '_text', 'class'])
;

Special 属性表示节点值,而表示元素名称(HTML 标记名称)。_text_name

在列表的每个节点上调用匿名函数:

use SymfonyComponentDomCrawlerCrawler;
// ...

$nodeValues = $crawler->filter('p')->each(function (Crawler $node, $i): string {
    return $node->text();
});

匿名函数接收节点(作为 Crawler)和位置作为参数。 结果是匿名函数调用返回的值数组。

使用嵌套爬网程序时,请注意,在 爬网程序的上下文:filterXPath()

$crawler->filterXPath('parent')->each(function (Crawler $parentCrawler, $i): void {
    // DON'T DO THIS: direct child can not be found (DON'T DO THIS: 无法找到直接子项)
    $subCrawler = $parentCrawler->filterXPath('sub-tag/sub-child-tag');

    // DO THIS: specify the parent tag too(执行此作:也指定父标签)
    $subCrawler = $parentCrawler->filterXPath('parent/sub-tag/sub-child-tag');
    $subCrawler = $parentCrawler->filterXPath('node()/sub-tag/sub-child-tag');
});

六、添加内容

添加动态内容(需结合 DOMDocument)

$dom = new DOMDocument();
$dom->loadHTML($html);
$newNode = $dom->createElement('p', 'New content');
$dom->appendChild($newNode);
$crawler->add($dom);

爬网程序支持多种添加内容的方式,但它们是相互的 exclusive,因此您只能使用其中一个来添加内容(例如,如果您将 content 添加到构造函数中,则以后不能调用):CrawleraddContent()

$crawler = new Crawler('<html><body/></html>');

$crawler->addHtmlContent('<html><body/></html>');
$crawler->addXmlContent('<root><node/></root>');

$crawler->addContent('<html><body/></html>');
$crawler->addContent('<root><node/></root>', 'text/xml');

$crawler->add('<html><body/></html>');
$crawler->add('<root><node/></root>');

addHtmlContent() 和 addXmlContent() 方法 默认为 UTF-8 编码,但您可以使用其第二个 optional 参数。

addContent() 方法 根据给定的内容猜测最佳字符集,并默认为在无法猜出字符集的情况下。ISO-8859-1

由于 Crawler 的实现基于 DOM 扩展,因此它也能够 与原生 DOMDocumentDOMNodeList 和 DOMNode 对象交互:

$domDocument = new DOMDocument();
$domDocument->loadXml('<root><node/><node/></root>');
$nodeList = $domDocument->getElementsByTagName('node');
$node = $domDocument->getElementsByTagName('node')->item(0);

$crawler->addDocument($domDocument);
$crawler->addNodeList($nodeList);
$crawler->addNodes([$node]);
$crawler->addNode($node);
$crawler->add($domDocument);

纵和转储

上的这些方法最初用于填充您的,而不是用于进一步作 DOM (尽管这是可能的)。但是,由于 是一组 DOMElement 对象,因此您可以使用任何可用的方法或属性 在 DOMElementDOMNode 或 DOMDocument 上。 例如,您可以获取 a 的 HTML,如下所示 这:CrawlerCrawlerCrawlerCrawler

$html = '';

foreach ($crawler as $domElement) {
    $html .= $domElement->ownerDocument->saveHTML($domElement);
}

或者你可以使用 html() 获取第一个节点的 HTML:

// 如果节点不存在,则调用 html() 将导致异常
$html = $crawler->html();

// 避免在 node 不存在时传递 html() 返回的参数的异常
$html = $crawler->html('Default <strong>HTML</strong> content');

或者你可以使用 outerHtml() 获取第一个节点的外部 HTML:

$html = $crawler->outerHtml();

七、表达式评估

使用 evaluate() 进行复杂计算

$totalLinks = $crawler->evaluate('count(//');

该方法计算给定的 XPath 表达式。回归 值取决于 XPath 表达式。如果表达式的计算结果为标量 value (e.g. HTML attributes) 时,将返回一个结果数组。如果 expression 的计算结果为 DOM 文档,则新实例将为 返回。evaluate()Crawler

此行为最好通过示例来说明:

use SymfonyComponentDomCrawlerCrawler;

$html = '<html>
<body>
    <span id="article-100" class="article">Article 1</span>
    <span id="article-101" class="article">Article 2</span>
    <span id="article-102" class="article">Article 3</span>
</body>
</html>';

$crawler = new Crawler();
$crawler->addHtmlContent($html);

$crawler->filterXPath('//span[contains(@id, "article-")]')->evaluate('substring-after(@id, "-")');
/* Result:
[
    0 => '100',
    1 => '101',
    2 => '102',
];
*/

$crawler->evaluate('substring-after(//span[contains(@id, "article-")]/@id, "-")');
/* Result:
[
    0 => '100',
]
*/

$crawler->filterXPath('//span[@class="article"]')->evaluate('count(@id)');
/* Result:
[
    0 => 1.0,
    1 => 1.0,
    2 => 1.0,
]
*/

$crawler->evaluate('count(//span[@class="article"])');
/* Result:
[
    0 => 3.0,
]
*/

$crawler->evaluate('//span[1]');
// A SymfonyComponentDomCrawlerCrawler instance

八、链接处理

提取并解析链接

use SymfonyComponentDomCrawlerUriResolver;

$links = $crawler->filter('a')->links();
foreach ($links as $link) {
    $absoluteUrl = UriResolver::resolve($link->getUri(), 'https://example.com');
    echo $absoluteUrl;
}

使用 该方法按其 or 属性查找链接 和 使用 该方法按其内容查找链接 (它还会查找其属性中包含该内容的可点击图像)。filter()idclassselectLink()alt

这两种方法都返回仅包含所选链接的实例。使用该方法获取 Link 对象 ,这表示链接:Crawlerlink()

// 首先,按 ID、类或内容选择链接...
$linkCrawler = $crawler->filter('#sign-up');
$linkCrawler = $crawler->filter('.user-profile');
$linkCrawler = $crawler->selectLink('Log in');

// ...然后,获取 Link 对象:
$link = $linkCrawler->link();

//或者一次性执行所有这些作:
$link = $crawler->filter('#sign-up')->link();
$link = $crawler->filter('.user-profile')->link();
$link = $crawler->selectLink('Log in')->link();

Link 对象有几个有用的 获取有关所选链接本身的更多信息的方法:

// 返回可用于发出另一个请求的正确 URI
$uri = $link->getUri();

这特别有用,因为它会清理值和 将其转化为它真正应该如何处理。例如,对于 link 替换为 ,这将返回当前 以 .return from 始终是完整的 URI 中执行作。getUri()hrefhref="#foo"#foogetUri()


九、图像处理

要按属性查找图像,请使用 现有爬网程序。这将返回一个实例,其中仅包含选定的 图片。调用 会得到一个特殊的 Image 对象:altselectImageCrawlerimage()

$imagesCrawler = $crawler->selectImage('Kitten');
$image = $imagesCrawler->image();

// or do this all at once
$image = $crawler->selectImage('Kitten')->image();

Image 对象与 Link 具有相同的方法。getUri()

$images = $crawler->filter('img')->each(function (Crawler $node) {
    return $node->attr('src');
});

十、表单处理

1. 获取表单字段

$form = $crawler->filter('form')->form();
$values = $form->getValues();

2. 模拟提交

$form['username'] = 'admin';
$form['password'] = 'pass123';
$submittedData = $form->getPhpValues();

表格也受到特殊处理。方法是 available 在 Crawler 上,它将返回另一个匹配 or 或 元素(或其中的元素)的 Crawler。作为参数给出的字符串在 、 和 属性以及 那些元素。selectButton()<button><input type="submit"><input type="button"><img>idaltnamevalue

此方法特别有用,因为您可以使用它来返回 一个 Form 对象,表示 按钮所在的形式:

// button example: <button id="my-super-button" type="submit">My super button</button>

// you can get button by its label
$form = $crawler->selectButton('My super button')->form();

// or by button id (#my-super-button) if the button doesn't have a label
$form = $crawler->selectButton('my-super-button')->form();

// or you can filter the whole form, for example a form has a class attribute: <form class="form-vertical" method="POST">
$crawler->filter('.form-vertical')->form();

// or "fill" the form fields with data
$form = $crawler->selectButton('my-super-button')->form([
    'name' => 'Ryan',
]);

Form 对象有很多非常 使用表单的有用方法:

$uri = $form->getUri();
$method = $form->getMethod();
$name = $form->getName();

getUri() 方法执行更多作 而不仅仅是返回表单的属性。如果 form 方法 是 GET,则它会模拟浏览器的行为并返回属性,后跟包含所有表单值的查询字符串。actionaction

optional 和 button 属性包括 支持。和 方法考虑了 这些属性始终返回正确的作和方法,具体取决于 用于获取表单的按钮。formactionformmethodgetUri()getMethod()

您可以在表单上虚拟设置和获取值:

// sets values on the form internally
$form->setValues([
    'registration[username]' => 'symfonyfan',
    'registration[terms]'    => 1,
]);

// gets back an array of values - in the "flat" array like above
$values = $form->getValues();

// returns the values like PHP would see them,
// where "registration" is its own array
$values = $form->getPhpValues();

要使用多维字段:

<form>
    <input name="multi[]">
    <input name="multi[]">
    <input name="multi[dimensional]">
    <input name="multi[dimensional][]" value="1">
    <input name="multi[dimensional][]" value="2">
    <input name="multi[dimensional][]" value="3">
</form>

传递值数组:

// sets a single field
$form->setValues(['multi' => ['value']]);

// sets multiple fields at once
$form->setValues(['multi' => [
    1             => 'value',
    'dimensional' => 'an other value',
]]);

// tick multiple checkboxes at once
$form->setValues(['multi' => [
    'dimensional' => [1, 3] // it uses the input value to determine which checkbox to tick
]]);

这很好,但它会变得更好!该对象允许您进行交互 像浏览器一样使用表单,选择单选值,勾选复选框, 和上传文件:Form

$form['registration[username]']->setValue('symfonyfan');

// checks or unchecks a checkbox
$form['registration[terms]']->tick();
$form['registration[terms]']->untick();

// selects an option
$form['registration[birthday][year]']->select(1984);

// selects many options from a "multiple" select
$form['registration[interests]']->select(['symfony', 'cookies']);

// fakes a file upload
$form['registration[photo]']->upload('/path/to/lucas.jpg');

使用表单数据

做这一切有什么意义呢?如果您在内部进行测试,则 可以从表单中获取信息,就像它刚刚提交一样 通过使用 PHP 值:

$values = $form->getPhpValues();
$files = $form->getPhpFiles();

如果您使用的是外部 HTTP 客户端,则可以使用表单来获取所有内容 中,您需要为表单创建 POST 请求:

$uri = $form->getUri();
$method = $form->getMethod();
$values = $form->getValues();
$files = $form->getFiles();

// now use some HTTP client and post using this information

使用所有这些的集成系统的一个很好的例子是  BrowserKit 组件。 它理解 Symfony Crawler 对象,并可以使用它来提交表单 径直:

use SymfonyComponentBrowserKitHttpBrowser;
use SymfonyComponentHttpClientHttpClient;

// makes a real request to an external site
$browser = new HttpBrowser(HttpClient::create());
$crawler = $browser->request('GET', 'https://github.com/login');

// select the form and fill in some values
$form = $crawler->selectButton('Sign in')->form();
$form['login'] = 'symfonyfan';
$form['password'] = 'anypass';

// submits the given form
$crawler = $browser->submit($form);

选择无效的选择值

默认情况下,选择字段 (select, radio) 已激活内部验证 以防止您设置无效值。如果您希望能够设置 invalid values 的 API 中,您可以在 整个表单或特定字段:disableValidation()

// disables validation for a specific field
$form['country']->disableValidation()->select('Invalid value');

// disables validation for the whole form
$form->disableValidation();
$form['country']->select('Invalid value');

十一、解析 URI

相对路径转绝对路径

$relativeUrl = '/about';
$baseUrl = 'https://example.com';
$absoluteUrl = UriResolver::resolve($relativeUrl, $baseUrl); // https://example.com/about

UriResolver 类采用 URI (相对、绝对、片段等)并将其转换为针对 另一个给定的基 URI:

use SymfonyComponentDomCrawlerUriResolver;

UriResolver::resolve('/foo', 'http://localhost/bar/foo/'); // http://localhost/foo
UriResolver::resolve('?a=b', 'http://localhost/bar#foo'); // http://localhost/bar=b
UriResolver::resolve('../../', 'http://localhost/'); // http://localhost/

十二、HTML5 解析器集成

使用 HTML5 解析器

use SymfonyComponentDomCrawlerCrawler;

$html5 = new MastermindsHTML5();
$dom = $html5->parse($htmlContent);
$crawler = new Crawler($dom);

如果您需要 Crawler 使用 HTML5 parser 中,将其 constructor 参数设置为:useHtml5Parsertrue

use SymfonyComponentDomCrawlerCrawler;

$crawler = new Crawler(null, $uri, useHtml5Parser: true);

这样,爬虫将使用 masterminds/html5 库提供的 HTML5 解析器来解析文档。


实战示例:抓取页面链接和图片

$crawler = new Crawler(file_get_contents('https://example.com'));

// 提取所有链接
$links = $crawler->filter('a')->each(function (Crawler $node) {
    return $node->attr('href');
});

// 提取所有图片
$images = $crawler->filter('img')->each(function (Crawler $node) {
    return [
        'src' => $node->attr('src'),
        'alt' => $node->attr('alt')
    ];
});

注意事项

  1. 编码问题:确保文档编码与解析器一致
  2. 性能优化:避免在循环中重复创建 Crawler 对象
  3. 错误处理:使用 count() 检查节点是否存在:
if ($crawler->filter('.not-exists')->count() > 0) {
    // 处理节点
}

通过本教程,您已掌握 Symfony DomCrawler 的核心功能。该组件特别适用于网页抓取、自动化测试和内容分析场景。