Компонент DomCrawler
Дата обновления перевода: 2024-07-03
Компонент 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,
сбросив её .
Фильтрация узлов
Использование выражений 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): bool {
// фильтрует узлы через один
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 17 18 19 20 21 22 23 24
// если узел не существует, вызов 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'
// если есть несколько текстовых узлов между другими дочерними узлами, вроде
// <p>Foo <span>Bar</span> Baz</p>
// innerText() возвращает только первый текстовый узел 'Foo'
// как и text(), innerText() также обрезает символы пробелов по умолчанию, но
// вы можете получить неизменённый текст, передав FALSE в качестве аргумента
$text = $crawler->filterXPath('//body/p')->innerText(false);
Доступ к значению атрибута первого узла текущего выбора:
1
$class = $crawler->filterXPath('//body/p')->attr('class');
Tip
Вы можете определить значение по умолчанию, которое будет использоваться, если узел
или атрибут пустой, используя второй аргумент метода attr()
:
1
$class = $crawler->filterXPath('//body/p')->attr('class', 'my-default-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): string {
return $node->text();
});
Анонимная функция получает узел (как Crawler) и позицию аргументов. Результатом является массив значений, возвращённых анонимными функциональными вызовами.
При использовании вложенного краулера, имейте в виду, что filterXPath()
оценивается в контексте краулера:
1 2 3 4 5 6 7 8
$crawler->filterXPath('parent')->each(function (Crawler $parentCrawler, $i): avoid {
// НЕ ДЕЛАЙТЕ ТАК: невозможно найти прямую дочь
$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);
Оценка выражений
Метод 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();
Формы
Особое внимание уделяется также и формам. Метод 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/
Использование парсера HTML5
Если вам нужно использовать Crawler как парсер
HTML5, установите его аргумент конструктора useHtml5Parser
как true
:
1 2 3
use Symfony\Component\DomCrawler\Crawler;
$crawler = new Crawler(null, $uri, useHtml5Parser: true);
Когда вы это сделаете, кроулер будет использовать парсер HTML5, предоставленный библиотекой masterminds/html5 для анализа документов.