Компонент DomCrawler

Дата обновления перевода 2023-01-20

Компонент DomCrawler

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

Note

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

Установка

1
$ composer require symfony/dom-crawler

Note

Если вы устанавливаете этот компонент вне приложения Symfony, вам нужно подключить файл vendor/autoload.php в вашем коде для включения механизма автозагрузки классов, предоставляемых Composer. Детальнее читайте в этой статье.

Применение

See also

Эта статья объясняет как использовать функции 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, сбросив её .

Note

Если вам нужна лучшая поддержка для содержания HTML5 или вы хотите избавиться от неточностей расширения PHP DOM, установите библиотеку html5-php. Компонент DomCrawler будет использовать его автоматически, если контент будет иметь тип документа HTML5.

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

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

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

Tip

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) {
        // фильтрует узлы через один
        return ($i % 2) == 0;
    });

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

Note

Все методы фильтрации возвращают новый экземпляр 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');

Note

Пространство имён по умолчанию зарегистрировано с префиксом "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')->parents();

Получите все прямые дочерние узлы, соответствующие CSS-селектору:

1
$crawler->filter('body')->children('p.lorem');

Получите первого родителя (в направлении к корню документа) элемента, соответствующего данному селектору:

1
$crawler->closest('p.lorem');

Note

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

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

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

1
2
// возвращает имя узла (имя HTML тега) первого дочеренго элемента под <body>
$tag = $crawler->filterXPath('//body/*')->nodeName();

Доступ к значению первого узла текущего выбора:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// если узел не существует, вызов text() приведет к исключению
$message = $crawler->filterXPath('//body/p')->text();

// избегайте того, чтобы исключение передавало аргумент, который возвращает text(), если узел не существует
$message = $crawler->filterXPath('//body/p')->text('Default text content');

// по умолчанию, text() усекает лишние пробелы, включая внутрение
// (например, "  foo\n  bar    baz \n " возвращается как "foo bar baz")
// передайте FALSE в качестве второго аргумента, чтобы вернуть текст оригинала без изменений
$crawler->filterXPath('//body/p')->text('Default text content', false);

// innerText() похож на text(), но возвращает только текст, который является
// прямым наследником текущего узла, исключая все дочерние узлы
$text = $crawler->filterXPath('//body/p')->innerText();
// если содержание - <p>Foo <span>Bar</span></p>
// innerText() возвращает 'Foo', а text() - 'Foo Bar'

Доступ к значению атрибута первого узла текущего выбора:

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

Извлечь значения атрибута и / или узлов из списка узлов:

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

Note

Специальный атрибут _text представляет значение узла, в то время как _name - название элемента (имя тега HTML).

Вызвать анонимную функцию в каждом узле списка:

1
2
3
4
5
6
use Symfony\Component\DomCrawler\Crawler;
// ...

$nodeValues = $crawler->filter('p')->each(function (Crawler $node, $i) {
    return $node->text();
});

Анонимная функция получает узел (как Crawler) и позицию аргументов. Результатом является массив значений, возвращённых анонимными функциональными вызовами.

При использовании вложенного краулера, имейте в виду, что filterXPath() оценивается в контексте краулера:

1
2
3
4
5
6
7
8
$crawler->filterXPath('parent')->each(function (Crawler $parentCrawler, $i) {
    // НЕ ДЕЛАЙТЕ ТАК: невозможно найти прямую дочь
    $subCrawler = $parentCrawler->filterXPath('sub-tag/sub-child-tag');

    // ДЕЛАЙТЕ ТАК: указывайте также и родительский тег
    $subCrawler = $parentCrawler->filterXPath('parent/sub-tag/sub-child-tag');
    $subCrawler = $parentCrawler->filterXPath('node()/sub-tag/sub-child-tag');
});

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

Сrawler поддерживает несколько способов добавления содержания, но они взаимоисключающие, поэтому вы можете использовать только один из них (например, если вы передаете содержание конструктору 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>');

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([$node]);
$crawler->addNode($node);
$crawler->add($domDocument);

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

1
2
3
4
5
$html = '';

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

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

1
2
3
4
5
// если узел не существует, вызов html() приведет к исключению
$html = $crawler->html();

// избегайте того, чтобы исключение передавало аргумент, который возвращает html(), если узел не существует
$html = $crawler->html('Default <strong>HTML</strong> content');

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

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]');
// Экземпляр Symfony\Component\DomCrawler\Crawler

Ссылки

Используйте метод filter(), чтобы найти ссылки по их атрибутам id или class, и используйте метод selectLink(), чтобы найти ссылки по их содержанию (также так вы можете найти активные изображения, имеющие это содержание в своем атрибуте alt).

Оба метода возвращают экземпляр Crawler с одной выбранной ссылкой. Используйте метод link(), чтобы получить объект Link, представляющий ссылку:

1
2
3
4
5
6
7
8
9
10
11
12
// сначала, выберите ссылку по id, классу или содержанию...
$linkCrawler = $crawler->filter('#sign-up');
$linkCrawler = $crawler->filter('.user-profile');
$linkCrawler = $crawler->selectLink('Log in');

// ...затем, получите объект ссылки:
$link = $linkCrawler->link();

// или сделайте это все одновременно:
$link = $crawler->filter('#sign-up')->link();
$link = $crawler->filter('.user-profile')->link();
$link = $crawler->selectLink('Log in')->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();
$name = $form->getName();

Метод 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
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
// Устанавливает одно поле
$form->setValues(array('multi' => array('value')));

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

// Устанавливает несколько флажков одновременно
$form->setValues(['multi' => [
    'dimensional' => [1, 3] // использует значение ввода, чтобы определить, какие флажки установить
]]);

Это всё прекрасно, но дальше - лучше! Объект 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 клиент и сделайте пост, используя эту информацию

Прекрасный пример интегрированной системы, который использует это все - 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;

// делает реальный запрос внешнему сайту
$browser = new HttpBrowser(HttpClient::create());
$crawler = $browser->request('GET', 'https://github.com/login');

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

// отправляет данную форму
$crawler = $browser->submit($form);

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

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

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

// Отключает валидацию для всей формы
$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', 'http://localhost/bar/foo/'); // http://localhost/foo
UriResolver::resolve('?a=b', 'http://localhost/bar#foo'); // http://localhost/bar?a=b
UriResolver::resolve('../../', 'http://localhost/'); // http://localhost/