Компонент DomCrawler

Дата оновлення перекладу 2022-12-12

Компонент 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 лище з обраним(и) зображенням(и). Виклик 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, що співставляє елементи <button> або <input type="submit"> або <input type="button"> (або елемент <img> всередині них) Рядок, даний як аргумент, розшукується в атрибутах id, alt, name та value і текстовому змісті цих елементів.

Цей метод особливо корисний, так як ви можете використовувати його, щоб повертати обʼєкт 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/