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 实例。
上方“备注”由本站长测验所得。
五、访问节点值
访问当前所选内容的第一个节点的节点名称(HTML 标记名称)(例如 “p” 或 “div”):
// 返回 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');您可以定义节点或属性为空时要使用的默认值 通过使用该方法的第二个参数:attr()
$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 扩展,因此它也能够 与原生 DOMDocument、DOMNodeList 和 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 对象,因此您可以使用任何可用的方法或属性 在 DOMElement、DOMNode 或 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();
提取图像源:
$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,则它会模拟浏览器的行为并返回属性,后跟包含所有表单值的查询字符串。
actionactionoptional 和 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/aboutUriResolver 类采用 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 参数设置为:
useHtml5Parsertrueuse 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')
];
});注意事项
- 编码问题:确保文档编码与解析器一致
- 性能优化:避免在循环中重复创建 Crawler 对象
- 错误处理:使用
count()检查节点是否存在:
if ($crawler->filter('.not-exists')->count() > 0) {
// 处理节点
}通过本教程,您已掌握 Symfony DomCrawler 的核心功能。该组件特别适用于网页抓取、自动化测试和内容分析场景。

