跳到内容

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);
}

专门的 LinkImageForm 类对于在遍历 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 扩展,因此它也能够与原生 DOMDocumentDOMNodeListDOMNode 对象进行交互

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);

Crawler 上的这些方法旨在最初填充你的 Crawler,并非旨在用于进一步操作 DOM(尽管这是可能的)。但是,由于 Crawler 是一组 DOMElement 对象,因此你可以使用 DOMElementDOMNodeDOMDocument 上可用的任何方法或属性。例如,你可以使用如下代码获取 Crawler 的 HTML

1
2
3
4
5
$html = '';

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

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

1
2
3
4
5
// if the node does not exist, calling to html() will result in an exception
$html = $crawler->html();

// avoid the exception passing an argument that html() returns when node does not exist
$html = $crawler->html('Default <strong>HTML</strong> content');

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

1
$html = $crawler->outerHtml();

表达式求值

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() 方法按 idclass 属性查找链接,并使用 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();

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

表单

表单也得到了特殊处理。Crawler 上有一个 selectButton() 方法,它返回另一个 Crawler,该 Crawler 匹配 <button><input type="submit"><input type="button"> 元素(或其中的 <img> 元素)。在 idaltnamevalue 属性以及这些元素的文本内容中查找作为参数给定的字符串。

此方法特别有用,因为你可以使用它返回一个 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 属性,后跟包含所有表单值的查询字符串。

注意

支持可选的 formactionformmethod 按钮属性。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 解析器来解析文档。

这项工作,包括代码示例,根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本