DomCrawler 组件
DomCrawler 组件简化了 HTML 和 XML 文档的 DOM 导航。
注意
虽然有可能,但 DomCrawler 组件并非设计用于操作 DOM 或重新转储 HTML/XML。
安装
1
$ composer require symfony/dom-crawler
注意
如果在 Symfony 应用程序外部安装此组件,则必须在代码中引入 vendor/autoload.php
文件,以启用 Composer 提供的类自动加载机制。阅读 这篇文章了解更多详情。
用法
另请参阅
本文介绍了如何在任何 PHP 应用程序中将 DomCrawler 功能用作独立组件。阅读 Symfony 功能测试文章,了解如何在创建 Symfony 测试时使用它。
Crawler 类提供了查询和操作 HTML 和 XML 文档的方法。
Crawler 的实例代表一组 DOMElement 对象,这些对象是可以按如下方式遍历的节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
use Symfony\Component\DomCrawler\Crawler;
$html = <<<'HTML'
<!DOCTYPE html>
<html>
<body>
<p class="message">Hello World!</p>
<p>Hello Crawler!</p>
</body>
</html>
HTML;
$crawler = new Crawler($html);
foreach ($crawler as $domElement) {
var_dump($domElement->nodeName);
}
专门的 Link、Image 和 Form 类对于在遍历 HTML 树时与 html 链接、图像和表单进行交互非常有用。
注意
DomCrawler 将尝试自动修复你的 HTML 以匹配官方规范。例如,如果你在一个 <p>
标签内嵌套另一个 <p>
标签,它将被移动为父标签的同级标签。这是预期的,并且是 HTML5 规范的一部分。但是,如果你遇到意外行为,这可能是一个原因。虽然 DomCrawler 并非旨在转储内容,但你可以通过 转储它来查看 HTML 的“修复”版本。
节点过滤
使用 XPath 表达式,你可以在文档中选择特定节点
1
$crawler = $crawler->filterXPath('descendant-or-self::body/p');
提示
DOMXPath::query
在内部用于实际执行 XPath 查询。
如果你更喜欢 CSS 选择器而不是 XPath,请安装 CssSelector 组件。它允许你使用类似 jQuery 的选择器
1
$crawler = $crawler->filter('body > p');
匿名函数可以用于使用更复杂的条件进行过滤
1 2 3 4 5 6 7 8 9
use Symfony\Component\DomCrawler\Crawler;
// ...
$crawler = $crawler
->filter('body > p')
->reduce(function (Crawler $node, $i): bool {
// filters every other node
return ($i % 2) === 0;
});
要删除节点,匿名函数必须返回 false
。
注意
所有过滤器方法都返回一个新的 Crawler 实例,其中包含过滤后的内容。要检查过滤器是否实际找到任何内容,请在此新 crawler 上使用 $crawler->count() > 0
。
filterXPath() 和 filter() 方法都适用于 XML 命名空间,这些命名空间可以自动发现或显式注册。
考虑以下 XML
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<?xml version="1.0" encoding="UTF-8" ?>
<entry
xmlns="http://www.w3.org/2005/Atom"
xmlns:media="http://search.yahoo.com/mrss/"
xmlns:yt="http://gdata.youtube.com/schemas/2007"
>
<id>tag:youtube.com,2008:video:kgZRZmEc9j4</id>
<yt:accessControl action="comment" permission="allowed"/>
<yt:accessControl action="videoRespond" permission="moderated"/>
<media:group>
<media:title type="plain">Chordates - CrashCourse Biology #24</media:title>
<yt:aspectRatio>widescreen</yt:aspectRatio>
</media:group>
</entry>
可以使用 Crawler
进行过滤,而无需使用 filterXPath() 注册命名空间别名
1
$crawler = $crawler->filterXPath('//default:entry/media:group//yt:aspectRatio');
和 filter()
1
$crawler = $crawler->filter('default|entry media|group yt|aspectRatio');
注意
默认命名空间注册时带有前缀“default”。可以使用 setDefaultNamespacePrefix() 方法更改它。
如果默认命名空间是文档中唯一的命名空间,则在加载内容时会将其删除。这样做是为了简化 XPath 查询。
可以使用 registerNamespace() 方法显式注册命名空间
1 2
$crawler->registerNamespace('m', 'http://search.yahoo.com/mrss/');
$crawler = $crawler->filterXPath('//m:group//yt:aspectRatio');
验证当前节点是否匹配选择器
1
$crawler->matches('p.lorem');
节点遍历
通过节点在列表中的位置访问节点
1
$crawler->filter('body > p')->eq(0);
获取当前选择的第一个或最后一个节点
1 2
$crawler->filter('body > p')->first();
$crawler->filter('body > p')->last();
获取与当前选择相同级别的节点
1
$crawler->filter('body > p')->siblings();
获取当前选择之后或之前的同级别节点
1 2
$crawler->filter('body > p')->nextAll();
$crawler->filter('body > p')->previousAll();
获取所有子节点或祖先节点
1 2
$crawler->filter('body')->children();
$crawler->filter('body > p')->ancestors();
获取与 CSS 选择器匹配的所有直接子节点
1
$crawler->filter('body')->children('p.lorem');
获取与提供的选择器匹配的元素的第一个父元素(朝文档根方向)
1
$crawler->closest('p.lorem');
注意
所有遍历方法都返回一个新的 Crawler 实例。
访问节点值
访问当前选择的第一个节点的节点名称(HTML 标签名称),例如“p”或“div”
1 2
// returns the node name (HTML tag name) of the first child element under <body>
$tag = $crawler->filterXPath('//body/*')->nodeName();
访问当前选择的第一个节点的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// if the node does not exist, calling to text() will result in an exception
$message = $crawler->filterXPath('//body/p')->text();
// avoid the exception passing an argument that text() returns when node does not exist
$message = $crawler->filterXPath('//body/p')->text('Default text content');
// by default, text() trims whitespace characters, including the internal ones
// (e.g. " foo\n bar baz \n " is returned as "foo bar baz")
// pass FALSE as the second argument to return the original text unchanged
$crawler->filterXPath('//body/p')->text('Default text content', false);
// innerText() is similar to text() but returns only text that is a direct
// descendant of the current node, excluding text from child nodes
$text = $crawler->filterXPath('//body/p')->innerText();
// if content is <p>Foo <span>Bar</span></p> or <p><span>Bar</span> Foo</p>
// innerText() returns 'Foo' in both cases; and text() returns 'Foo Bar' and 'Bar Foo' respectively
// if there are multiple text nodes, between other child nodes, like
// <p>Foo <span>Bar</span> Baz</p>
// innerText() returns only the first text node 'Foo'
// like text(), innerText() also trims whitespace characters by default,
// but you can get the unchanged text by passing FALSE as argument
$text = $crawler->filterXPath('//body/p')->innerText(false);
访问当前选择的第一个节点的属性值
1
$class = $crawler->filterXPath('//body/p')->attr('class');
提示
你可以通过使用 attr()
方法的第二个参数来定义在节点或属性为空时使用的默认值
1
$class = $crawler->filterXPath('//body/p')->attr('class', 'my-default-class');
从节点列表中提取属性和/或节点值
1 2 3 4
$attributes = $crawler
->filterXpath('//body/p')
->extract(['_name', '_text', 'class'])
;
注意
特殊属性 _text
表示节点值,而 _name
表示元素名称(HTML 标签名称)。
在列表中的每个节点上调用匿名函数
1 2 3 4 5 6
use Symfony\Component\DomCrawler\Crawler;
// ...
$nodeValues = $crawler->filter('p')->each(function (Crawler $node, $i): string {
return $node->text();
});
匿名函数接收节点(作为 Crawler)和位置作为参数。结果是由匿名函数调用返回的值数组。
当使用嵌套 crawler 时,请注意 filterXPath()
在 crawler 的上下文中进行评估
1 2 3 4 5 6 7 8
$crawler->filterXPath('parent')->each(function (Crawler $parentCrawler, $i): void {
// DON'T DO THIS: direct child can not be found
$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');
});
添加内容
crawler 支持多种添加内容的方式,但它们是互斥的,因此你只能使用其中一种方式来添加内容(例如,如果你将内容传递给 Crawler
构造函数,则不能稍后调用 addContent()
)
1 2 3 4 5 6 7 8 9 10
$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 编码,但你可以使用它们的第二个可选参数更改此行为。
addContent() 方法根据给定的内容猜测最佳字符集,如果无法猜测字符集,则默认使用 ISO-8859-1
。
由于 Crawler 的实现基于 DOM 扩展,因此它也能够与原生 DOMDocument、DOMNodeList 和 DOMNode 对象进行交互
1 2 3 4 5 6 7 8 9 10
$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);
表达式求值
evaluate()
方法评估给定的 XPath 表达式。返回值取决于 XPath 表达式。如果表达式评估为标量值(例如 HTML 属性),则将返回结果数组。如果表达式评估为 DOM 文档,则将返回一个新的 Crawler
实例。
此行为最好用示例说明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
use Symfony\Component\DomCrawler\Crawler;
$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 Symfony\Component\DomCrawler\Crawler instance
链接
使用 filter()
方法按 id
或 class
属性查找链接,并使用 selectLink()
方法按内容查找链接(它还会查找在其 alt
属性中包含该内容的可点击图像)。
这两种方法都返回一个 Crawler
实例,其中仅包含选定的链接。使用 link()
方法获取表示链接的 Link 对象
1 2 3 4 5 6 7 8 9 10 11 12
// first, select the link by id, class or content...
$linkCrawler = $crawler->filter('#sign-up');
$linkCrawler = $crawler->filter('.user-profile');
$linkCrawler = $crawler->selectLink('Log in');
// ...then, get the Link object:
$link = $linkCrawler->link();
// or do all this at once:
$link = $crawler->filter('#sign-up')->link();
$link = $crawler->filter('.user-profile')->link();
$link = $crawler->selectLink('Log in')->link();
Link 对象有几个有用的方法来获取有关所选链接本身的更多信息
1 2
// returns the proper URI that can be used to make another request
$uri = $link->getUri();
注意
getUri()
特别有用,因为它清理了 href
值并将其转换为真正应该处理的方式。例如,对于 href="#foo"
的链接,这将返回当前页面的完整 URI,并附加 #foo
。来自 getUri()
的返回值始终是一个你可以操作的完整 URI。
图片
要按 alt
属性查找图像,请在现有 crawler 上使用 selectImage
方法。这将返回一个 Crawler
实例,其中仅包含所选图像。调用 image()
会给你一个特殊的 Image 对象
1 2 3 4 5
$imagesCrawler = $crawler->selectImage('Kitten');
$image = $imagesCrawler->image();
// or do this all at once
$image = $crawler->selectImage('Kitten')->image();
表单
表单也得到了特殊处理。Crawler 上有一个 selectButton()
方法,它返回另一个 Crawler,该 Crawler 匹配 <button>
或 <input type="submit">
或 <input type="button">
元素(或其中的 <img>
元素)。在 id
、alt
、name
和 value
属性以及这些元素的文本内容中查找作为参数给定的字符串。
此方法特别有用,因为你可以使用它返回一个 Form 对象,该对象表示按钮所在的表单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// 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 对象有很多非常有用的方法来处理表单
1 2 3
$uri = $form->getUri();
$method = $form->getMethod();
$name = $form->getName();
getUri() 方法不仅仅返回表单的 action
属性。如果表单方法是 GET,则它会模拟浏览器的行为,并返回 action
属性,后跟包含所有表单值的查询字符串。
注意
支持可选的 formaction
和 formmethod
按钮属性。getUri()
和 getMethod()
方法会考虑这些属性,以始终根据用于获取表单的按钮返回正确的操作和方法。
你可以在表单上虚拟地设置和获取值
1 2 3 4 5 6 7 8 9 10 11 12
// 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();
要处理多维字段
1 2 3 4 5 6 7 8
<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>
传递一个值数组
1 2 3 4 5 6 7 8 9 10 11 12 13
// 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
对象允许你像浏览器一样与表单交互,选择单选按钮值、勾选复选框和上传文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14
$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 值从表单中获取信息
1 2
$values = $form->getPhpValues();
$files = $form->getPhpFiles();
如果你正在使用外部 HTTP 客户端,则可以使用表单来获取创建表单的 POST 请求所需的所有信息
1 2 3 4 5 6
$uri = $form->getUri();
$method = $form->getMethod();
$values = $form->getValues();
$files = $form->getFiles();
// now use some HTTP client and post using this information
使用所有这些功能的集成系统的一个很好的例子是 HttpBrowser,它由 BrowserKit 组件提供。它理解 Symfony Crawler 对象,并且可以使用它直接提交表单
1 2 3 4 5 6 7 8 9 10 11 12 13 14
use Symfony\Component\BrowserKit\HttpBrowser;
use Symfony\Component\HttpClient\HttpClient;
// 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)已激活内部验证,以防止你设置无效值。如果你希望能够设置无效值,则可以在整个表单或特定字段上使用 disableValidation()
方法
1 2 3 4 5 6
// 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
UriResolver 类接受 URI(相对、绝对、片段等),并将其转换为相对于另一个给定基本 URI 的绝对 URI
1 2 3 4 5
use Symfony\Component\DomCrawler\UriResolver;
UriResolver::resolve('/foo', 'https://127.0.0.1/bar/foo/'); // https://127.0.0.1/foo
UriResolver::resolve('?a=b', 'https://127.0.0.1/bar#foo'); // https://127.0.0.1/bar?a=b
UriResolver::resolve('../../', 'https://127.0.0.1/'); // https://127.0.0.1/
使用 HTML5 解析器
如果你需要 Crawler 使用 HTML5 解析器,请将其 useHtml5Parser
构造函数参数设置为 true
1 2 3
use Symfony\Component\DomCrawler\Crawler;
$crawler = new Crawler(null, $uri, useHtml5Parser: true);
通过这样做,crawler 将使用 masterminds/html5 库提供的 HTML5 解析器来解析文档。