DomCrawler组件

3.3 version
维护中的版本

DomCrawler组件使HTML和XML文档的导览(navigation)变得容易。

安装 

你可以通过下述两种方式安装:

然后,包容vendor/autoload.php文件,以开启Composer提供的自动加载机制。否则,你的程序将无法找到这个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, ImageForm 类,在你遍历HTML文档树的过程中操作html链接、图片以及表单时有用。

DomCrawler将尝试自动修复你的HTML以令其匹配官方协议。例如,如果你把一个 <p> 标签嵌套在另一个 <p> 标签中,它会被移动到成为父标签的sibling(的位置)。这是预期行为,也是HTML5协议的一部分。但如果你得到的是预想以外的行为,这也许是个诱因。尽管DomCrawler并不代表要剥离内容,通过剥离它(dumping it),你仍然可以看到你的“被修复过的”HTML。

节点过滤 

使用Xpath表达式十分简单:

1
$crawler = $crawler->filterXPath('descendant-or-self::body/p');

DOMXPath::query 用于在内部真正操作Xpath查询。

如果你安装了CssSelector组件,过滤(filtering)甚至更加容易。它可以让你使用类似Jquery的选择器(selector)来进行遍历:

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) {
        // filter every other node / 过滤每一个其他的节点
        return ($i % 2) == 0;
    });

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

所有filter方法返回一个带有已过滤内容的 Crawler 实例。

filterXPath()filter() 方法都使用XML namespaces,命令空间可以被自动发现或显式注册。

考虑以下代码:

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() 方法来修改。

命名空间可以通过 registerNamespace() 方法来显式注册:

1
2
$crawler->registerNamespace('m', 'http://search.yahoo.com/mrss/');
$crawler = $crawler->filterXPath('//m:group//yt:aspectRatio');

遍历节点 

通过其在列表(list)中的位置来访问节点:

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')->parents();

所有tranveral方法返回一个新的 Crawler 实例。

访问节点的值 

访问当前选中内容 (如 "p" 或 "div") 的第一个节点的名称 (HTML标签名):

1
2
3
// will return the node name (HTML tag name) of the first child element under <body>
// 将返回 <body> 下的第一个子元素的节点名称(HTML标签名)
$tag = $crawler->filterXPath('//body/*')->nodeName();

访问当前选中内容的第一个节点的值:

1
$message = $crawler->filterXPath('//body/p')->text();

访问当前选中内容的第一个节点的属性值:

1
$class = $crawler->filterXPath('//body/p')->attr('class');

从节点列表中提取属性 和/或 节点值:

1
2
3
4
$attributes = $crawler
    ->filterXpath('//body/p')
    ->extract(array('_text', 'class'))
;

特殊的 _text 属性,代表的是节点值。

对列表中的每个节点应用匿名函数:

1
2
3
4
5
6
use Symfony\Component\DomCrawler\Crawler;
// ...
 
$nodeValues = $crawler->filter('p')->each(function (Crawler $node, $i) {
    return $node->text();
});

匿名函数接收节点(一个Crawler)和位置作为参数。结果是一个由匿名函数返回的值所构成的数组。

添加内容 

crawler支持以多种方式来添加内容:

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

当处理 ISO-8859-1 以外的字符集(character set)时,组件始终用 addHtmlContent() 方法添加内容,你可以指定第二个参数来设置目标字符集。

由于Crawler的实现是基于DOM extension的,它也可以与原生的 DOMDocument, DOMNodeListDOMNode 对象进行互动:

1
2
3
4
5
6
7
8
9
10
$document = new \DOMDocument();
$document->loadXml('<root><node /><node /></root>');
$nodeList = $document->getElementsByTagName('node');
$node = $document->getElementsByTagName('node')->item(0);
 
$crawler->addDocument($document);
$crawler->addNodeList($nodeList);
$crawler->addNodes(array($node));
$crawler->addNode($node);
$crawler->add($document);

操作并剥离出Crawler

这些 Crawler 的方法意在初始化地装载(populate)你的 Crawler 而不是要进一步的操作DOM (虽然这完全可行)。然而,由于 Crawler 是一组 DOMElement 对象,你也可以使用 DOMElement, DOMNodeDOMDocument 中的任何方法或属性。例如,你可以像下面这样去获取一个 Crawler 的HTML内容:

1
2
3
4
5
$html = '';
 
foreach ($crawler as $domElement) {
    $html .= $domElement->ownerDocument->saveHTML($domElement);
}

或者使用 html() 来获取第一个节点里的HTML内容:

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

链接 

要通过name (或一张可点击的图片的 alt 属性) 来找到一个链接,对当前已存在的Crawler使用 selectLink 方法。 这将返回一个仅包含所选择链接的 Crawler 。调用 link() 可以取得一个特殊的 Link 对象:

1
2
3
4
5
$linksCrawler = $crawler->selectLink('Go elsewhere...');
$link = $linksCrawler->link();
 
// or do this all at once / 或者一次做完所有事
$link = $crawler->selectLink('Go elsewhere...')->link();

Link 对象有一些有用的方法来获取关于“所选中的链接自身”的更多信息:

1
2
3
// return the proper URI that can be used to make another request
// 返回适当的URL,以便用于下一次请求中
$uri = $link->getUri();

getUri() 极为有用,因为它清除了 href 值,并且把它转换成“能够被真正执行”的程度。例如,对于一个带有 href="#foo" 的链接,这将返回带有 #foo 后缀的当前页面的完整URI。 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() 方法可供使用,它返回另一个匹配了那个“包含了给定text”的按钮 (input[type=submit], input[type=image], 或者是 button) 的Crawler。这个方法特别有用,因为你可以用它取出一个 Form 对象,该对象正是那个按钮所在的表单:

1
2
3
4
5
6
7
$form = $crawler->selectButton('validate')->form();
 
// or "fill" the form fields with data
// 或者用数据去 “填充” 表单字段
$form = $crawler->selectButton('validate')->form(array(
    'name' => 'Ryan',
));

Form 对象有很多用于操作表单的方法:

1
2
3
$uri = $form->getUri();
 
$method = $form->getMethod();

getUri() 方法所能做的,不仅是单纯返回表单的 action 属性。如果表单的method是GET,那么它会模拟浏览器行为并返回这个 action 属性,后面跟着(提交来的)全部表单值的query string。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// set values on the form internally
// 在内部设置表单的值
$form->setValues(array(
    'registration[username]' => 'symfonyfan',
    'registration[terms]'    => 1,
));
 
// get 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
// 返回在PHP中可见的值, 即 "registration" 就是它自己的数组
$values = $form->getPhpValues();

操作多维字段(multi-dimensional fields):

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

传入一个“值数组”( an array of values):

1
2
3
4
5
6
7
8
// Set a single field / 设置单一字段
$form->setValues(array('multi' => array('value')));
 
// Set multiple fields at once / 一次设置多个字段
$form->setValues(array('multi' => array(
    1             => 'value',
    'dimensional' => 'an other value'
)));

这很强大,但它还可以做得更好!Form 对象允许你像浏览器一样与表单互动,选择radio值,点选checkboxes,上传文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$form['registration[username]']->setValue('symfonyfan');
 
// check or uncheck a checkbox / 复选框的选中或反选
$form['registration[terms]']->tick();
$form['registration[terms]']->untick();
 
// select an option / 下拉选择
$form['registration[birthday][year]']->select(1984);
 
// select many options from a "multiple" select / 下拉多选
$form['registration[interests]']->select(array('symfony', 'cookies'));
 
// even fake a file upload / 模拟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
7
$uri = $form->getUri();
$method = $form->getMethod();
$values = $form->getValues();
$files = $form->getFiles();
 
// now use some HTTP client and post using this information
// 现在可以使用某些HTTP客户端,并且在提交时使用这些信息

一个整合了所有这些方法的完美系统是 Goutte。Goutte理解Symfony 的Crawler对象并且能够用它来直接提交表单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Goutte\Client;
 
// make a real request to an external site
// 制造一个对外部网站的真实请求
$client = new Client();
$crawler = $client->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';
 
// submit that form
// 提交此表单
$crawler = $client->submit($form);

选择无效的choices值 

默认时,(Symfony的)choice字段(select、radio)带有已激活的内部验证,为的是防止你的表单设置无效值。如果你希望能够设置无效值,可以对整个表单或特定字段使用 disableValidation() 方法:

1
2
3
4
5
6
7
8
// Disable validation for a specific field
// 禁用特定字段的验证
$form['country']->disableValidation()->select('Invalid value');
 
// Disable validation for the whole form
// 禁用整个表单的验证
$form->disableValidation();
$form['country']->select('Invalid value');

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)