Компонент DomCrawler

Компонент DomCrawler облегчает DOM навигацию для документов HTML и XML.

Note

Несмотря на то, что это возможно, компонент DomCrawler не был создан для управления DOM или повторного сброса HTML/XML.

Установка

1
$ composer require symfony/dom-crawler

Также вы можете клонировать репозиторий https://github.com/symfony/dom-crawler.

Note

If you install this component outside of a Symfony application, you must require the vendor/autoload.php file in your code to enable the class autoloading mechanism provided by Composer. Read this article for more details.

Применение

Эта статья объясняет как использовать функции DomCrawler как независимого компонента в любом приложении PHP. Прочитайте статью Функциональные тесты 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.

Note

DomCrawler попробует автоматически исправить ваш HTML, чтобы он совпадал с официальной спецификацией. Например, если вы вложите тег <p> в другой тег <p>, он будет перемещён, чтобы быть сестрой родительского тега. Это ожидаемо и является частью спецификации HTML5. И хотя DomCrawler не предназначен для сброса содержания,вы можете увидеть "исправленную" версию вашего HTML, сбросив её.

Фильтрация узлов

Использование выражений XPath очень просто:

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

Tip

DOMXPath::query используется внутренне, чтобы выполнять запрос 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) {
        // фильтрует узлы через один
        return ($i % 2) == 0;
    });

Чтобы удалить узел, анонимная функция должна вернуть "false".

Note

Все методы фильтрации возвращают новый экземпляр Crawler с отфильтрованным содержанием.

Оба метода 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');

Note

Пространство имён по умолчанию зарегистрировано с префиксом "default". Оно может быть изменено методом setDefaultNamespacePrefix().

Пространство имён по умолчанию удаляется при загрузке содержимого, если оно является единственым пространством имён в нём. Это делается для упрощения запросов xpath.

Пространства имён могут быть ясно зарегистрированы с методом registerNamespace():

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

Траверсирование узлов

Получите доступ к узлу поего позиции в списке:

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

Note

Все траверсионные методы возвращают новый экзепмпляр Crawler.

Доступ к значениям узлов

Доступ к имени узла (имени HTML тега) первого узла текущего выбора (например, "p" или "div"):

1
2
// возвращает имя узла (имя HTML тега) первого дочеренго элемента под <body>
$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'))
;

Note

Специальный атрибут _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) и позицию аргументов. Результатом является массив значений, возвращённых анонимными функциональными вызовами.

Добавление содержания

Сrawler поддерживает несколько способов добавления содержания:

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

Note

Методы 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(array($node));
$crawler->addNode($node);
$crawler->add($domDocument);

Эти методы в Crawler предназначаются для первоначального наполнения вашего Crawler, и не предназначены для дальнейшего управления DOM (хотя это возможно). Однако, так как Crawler - это набор объектов DOMElement, вы можете использовать любой доступный метод или свойство в DOMElement, DOMNode или DOMDocument. Например, вы можете получить HTML Crawler с чем-то вроде этого:

$html = '';

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

}

Или вы можете получить HTML первого узла, используя html():

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

оценка выражений

Метод 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
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, "-")');
/* array:3 [
     0 => "100"
     1 => "101"
     2 => "102"
   ]
 */

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

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

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

$crawler->evaluate('//span[1]');
// Экземпляр Symfony\Component\DomCrawler\Crawler

Ссылки

Чтобы найти ссылку по имени (или нажимаемое изображение по его атрибуту alt), используйте метод selectLink() в существующем crawler. Это вернёт экземпляр Crawler только с выбранной(ыми) ссылкой(ами). Вызов link() даст вам специальный объект Link:

1
2
3
4
5
$linksCrawler = $crawler->selectLink('Go elsewhere...');
$link = $linksCrawler->link();

// или сделать это всё одновременно
$link = $crawler->selectLink('Go elsewhere...')->link();

Объект Link имеет несколько полезных методов, чтобы получить больше информации о самой выбранной ссылке:

1
2
// возвращает соответствующий URL, который может быть использован для создания другого запроса
$uri = $link->getUri();

Note

getUri() особенно полезен, так как очищает значение href и преобразует его в то, как оно действительно должно быть обработано. Например, для ссылки с href="#foo", будет возвращён полный URI текущей страницы, с суффиксом #foo. Возврат из getUri() - всегда полный URI, с которым вы можете работать.

Изображения

Чтобы найти изображения по его атрибуту alt, используйте метод selectImage в существующем crawler. Это вернёт экземпляр Crawler только с выбранным(и) изображением(ями). Вызов image() даёт вам специальный объект Image:

1
2
3
4
5
$imagesCrawler = $crawler->selectImage('Kitten');
$image = $imagesCrawler->image();

// или сделать это всё одновременно
$image = $crawler->selectImage('Kitten')->image();

Объект Image имеет тот же метод getUri(), что и Link.

Формы

Особое внимание уделяется также и формам. Метод selectButton() доступен в Crawler, и возвращает другой Crawler, который совпадает с кнопкой (input[type=submit], input[type=image], или button) с заданным текстом. Этот метод особенно полезен, так как вы можете использовать его, чтобы вернуть объект a Form, который представляет форму, в которой живёт кнопка:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// пример кнопки: <button id="my-super-button" type="submit">Моя супер-кнопка</button>

// вы можете получить кнопку по её ярлыку
$form = $crawler->selectButton('My super button')->form();

// или по id кнопки (#my-super-button), если ярлыка нет
$form = $crawler->selectButton('my-super-button')->form();

// или вы можете отфильтровать всю форму, например, форма имеет атрибут класса: <form class="form-vertical" method="POST">
$crawler->filter('.form-vertical')->form();

// или "заполнить" поля формы данными
$form = $crawler->selectButton('my-super-button')->form(array(
    'name' => 'Ryan',
));

Объект Form имеет множество очень полезных методов для работы с формами:

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

$method = $form->getMethod();

Метод getUri() делает больше, чем просто возвращает атрибут формы action. Если метод формы - GET, то он имитирует поведение браузера и возвращает атрибут action, за которым следует строка запроса всех значений формы.

Note

Поддерживаются необязательные атрибуты кнопки formaction и formmethod. Методы getUri() и getMethod() принимают во внимание эти атрибуты, чтобы всегда возвращать правильные действие и метод, в зависимости от кнопки, использованной для получения формы.

Вы можете виртуально установить и получить значения формы:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// устанавливает значения формы внутренне
$form->setValues(array(
    'registration[username]' => 'symfonyfan',
    'registration[terms]'    => 1,
));

// получает обратно массив значений - в "чистом" массиве, как выше
$values = $form->getValues();

// возвращает значения такими, какими их будет видеть PHP,
// где "регистрация" - отдельный массив
$values = $form->getPhpValues();

Для работы с многомерными полями:

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

Передать массив значений:

1
2
3
4
5
6
7
8
// Устанавливает одно поле
$form->setValues(array('multi' => array('value')));

// Устанавливает несколько полей одновременно
$form->setValues(array('multi' => array(
    1             => 'value',
    'dimensional' => 'an other value'
)));

Это всё прекрасно, но дальше - лучше! Объект Form позволяет вам взаимодействовать с вашей формой, как браузером, выбирая значения радиокнопок, отмечая чекбоксы, и загружая файлы:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$form['registration[username]']->setValue('symfonyfan');

// отмечает или убирает отметку чекбокса
$form['registration[terms]']->tick();
$form['registration[terms]']->untick();

// выбирает опцию
$form['registration[birthday][year]']->select(1984);

// выбирает несколько опций в множественном "селекте"
$form['registration[interests]']->select(array('symfony', 'cookies'));

// имитирует загрузку файла
$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();

// теперь используйте некоторый HTTP клиент и сделайте пост, используя эту информацию

Прекрасный пример интегрированной системы, использующей всё это - Goutte. Goutte понимает объект Symfony Crawler и может использовать его для отправки форм напрямую:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use Goutte\Client;

// делает настоящий запрос к внешнему сайту
$client = new Client();
$crawler = $client->request('GET', 'https://github.com/login');

// выбирает форму и заполнить некоторые значения
$form = $crawler->selectButton('Sign in')->form();
$form['login'] = 'symfonyfan';
$form['password'] = 'anypass';

// отправляет эту форму
$crawler = $client->submit($form);

Выбор недопустимых значений выбора

По умолчанию, поля выбора (селект, радио) имеют внутренню валидацию, которая активируется, чтобы уберечь вас от установки недопустимых значений. Если вы хотите иметь возможность устанавливать недопустимые значения, вы можете использовать метод disableValidation() либо во всей форме, либо в отдельном(ых) поле(ях):

1
2
3
4
5
6
// Отключает валидацию для конкретного поля
$form['country']->disableValidation()->select('Invalid value');

// Отключает валидацию для всей формы
$form->disableValidation();
$form['country']->select('Invalid value');

Узнайте больше

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.