Компонент DomCrawler

Компонент DomCrawler в Symfony 4 облегчает навигацию по DOM для документов HTML и XML. Зачастую используется для разбора и парсинга содержимого таких документов.

Установка

Установка легко осуществляется при помощи composer:

 composer require symfony/dom-crawler

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

Использование

В этой статье объясняется, как использовать функции DomCrawler в качестве независимого компонента в любом приложении PHP. Прочтите статью о функциональных тестах в Symfony, чтобы узнать, как использовать DomCrawler при создании тестов Symfony.

Класс Crawler предоставляет методы для запроса и манипулирования документами HTML и XML.

Экземпляр Crawler представляет набор объектов DOMElement, которые обычно являются узлами, по которым вы можете перемещаться:

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.

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

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

Используя выражения XPath, вы можете выбрать конкретные узлы в документе:

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

DOMXPath::query используется внутренне, чтобы фактически выполнить запрос XPath.

Если вы предпочитаете использовать CSS-селекторы, вместо XPath, то установите компонент CssSelector. Это позволяет вам использовать jQuery-подобные селекторы для обхода:

$crawler = $crawler->filter('body > p');

Для фильтрации по более сложным критериям, можно использовать анонимную функцию:

use Symfony\Component\DomCrawler\Crawler;
// ...

$crawler = $crawler
    ->filter('body > p')
    ->reduce(function (Crawler $node, $i) {
        // фильтрует каждый второй узел
        return ($i % 2) == 0;
    });

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

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

Оба метода filterXPath() и filter() работают с пространствами имен XML, которые могут быть автоматически обнаружены или зарегистрированы явно.

Рассмотрим XML ниже:

<?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():

$crawler = $crawler->filterXPath('//default:entry/media:group//yt:aspectRatio');

и метод filter() :

$crawler = $crawler->filter('default|entry media|group yt|aspectRatio');

Пространство имен по умолчанию зарегистрировано с префиксом «default». Его можно изменить с помощью метода setDefaultNamespacePrefix().
Пространство имен по умолчанию удаляется при загрузке содержимого, если это единственное пространство имен в документе. Это сделано для упрощения запросов xpath.

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

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

Перемещение по узлам

Доступ к узлу по его позиции в списке:

$crawler->filter('body > p')->eq(0);

Получить первый или последний узел текущего выбора:

$crawler->filter('body > p')->first();
$crawler->filter('body > p')->last();

Получить узлы того же уровня, что и текущий выбранный:

$crawler->filter('body > p')->siblings();

Получите узлы того же уровня после или до текущего выбранного узла:

$crawler->filter('body > p')->nextAll();
$crawler->filter('body > p')->previousAll();

Получить все дочерние или родительские узлы:

$crawler->filter('body')->children();
$crawler->filter('body > p')->parents();

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

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

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

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

Получить доступ к имени узла (имя тега HTML) первого текущего узла (например, «p» или «div»):

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

Получить доступ к значению первого узла текущей ноды:

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

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

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

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

$attributes = $crawler
    ->filterXpath('//body/p')
    ->extract(['_text', 'class'])
;

Специальный атрибут _text представляет значение узла.

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

use Symfony\Component\DomCrawler\Crawler;
// ...

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

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

Добавление контента.

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

$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, он также может взаимодействовать с нативными объектами DOMDocument, DOMNodeList и DOMNode:

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

Управление и сохранение HTML кодом с помощью Crawler.

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

$html = '';

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

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

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

Ссылки (Links)

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

$linksCrawler = $crawler->selectLink('Go elsewhere...');
$link = $linksCrawler->link();

// or do this all at once
$link = $crawler->selectLink('Go elsewhere...')->link();

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

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

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

Изображения (Images).

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

$imagesCrawler = $crawler->selectImage('Kitten');
$image = $imagesCrawler->image();

// or do this all at once
$image = $crawler->selectImage('Kitten')->image();

Объект Image имеет тот же метод getUri(), что и объект Link (как в примере выше).

Работа с формами.

Особое внимание также уделяется формам. В Crawler доступен метод selectButton(), который возвращает другой Crawler, который соответствует элементам <button> или <input type=»submit»> или <input type=»button»> (или элементу <img> внутри них). Строка, заданная в качестве аргумента, ищется в атрибутах id, alt, name и value и в текстовом содержимом этих элементов.

Этот метод особенно полезен, потому что вы можете использовать его для возврата объекта Form, представляющего форму, в которой находится кнопка:

// button example: <button id="my-super-button" type="submit">My super button</button>

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

// или по id кнопки (# my-super-button), если кнопка не имеет метки - label
$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([
    'name' => 'Ryan',
]);

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

$uri = $form->getUri();

$method = $form->getMethod();

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

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

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

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

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

<form>
    <input name="multi[]"/>
    <input name="multi[]"/>
    <input name="multi[dimensional]"/>
</form>

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

// sets a single field
$form->setValues(['multi' => ['value']]);

// sets multiple fields at once
$form->setValues(['multi' => [
    1             => 'value',
    'dimensional' => 'an other value',
]]);

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

$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:

$values = $form->getPhpValues();
$files = $form->getPhpFiles();

Если вы используете внешний HTTP-клиент, вы можете использовать форму, чтобы получить всю информацию, необходимую для создания запроса POST для формы:

$uri = $form->getUri();
$method = $form->getMethod();
$values = $form->getValues();
$files = $form->getFiles();

// now use some HTTP client and post using this information

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

use Goutte\Client;

// makes 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';

// submits the given form
$crawler = $client->submit($form);

Выбор неверных значений в полях формы.

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

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

Добавить комментарий

Ваш адрес email не будет опубликован.