Symfony po krůčkách – DomCrawler a CssSelector

Symfony poskytuje nástroje nejen pro tvorbu webů, ale také pro jejich procházení a dolování informací. Dnes si ukážeme, jak se lze jednoduše šťourat v DOMu stránek pomocí komponent DomCrawler a CssSelector. A na závěr jsem připravil malé překvapení, k čemu všemu se to dá použít.

Seriál: Symfony po krůčkách (18 dílů)

  1. Symfony po krůčkách – Event Dispatcher 30.11.2015
  2. Symfony Console jako první rande se Symfony 7.12.2015
  3. Symfony po krůčkách – Filesystem a Finder 14.12.2015
  4. Symfony po krůčkách – Paralýza možností? OptionsResolver tě zachrání 21.12.2015
  5. Symfony po krůčkách – spouštíme procesy 4.1.2016
  6. Symfony po krůčkách – Translation – překlady jednoduše 11.1.2016
  7. Symfony po krůčkách – Validator (1) 18.1.2016
  8. Symfony po krůčkách – Validator (2) 25.1.2016
  9. Symfony po krůčkách – Routing 1.2.2016
  10. Symfony po krůčkách – MicroKernel 9.2.2016
  11. Konfigurujeme Symfony pomocí YAMLu 16.2.2016
  12. Symfony po krůčkách – oblékáme MicroKernel 23.2.2016
  13. Symfony po krůčkách – ClassLoader 29.2.2016
  14. Symfony po krůčkách – Twig 8.3.2016
  15. Symfony po krůčkách – Twig II. 15.3.2016
  16. Symfony po krůčkách – DomCrawler a CssSelector 23.3.2016
  17. Symfony po krůčkách – HTTP fundamentalista 12.4.2016
  18. Symfony po krůčkách – ušli jsme pořádný kus 19.4.2016

Komponenta DomCrawler je něco jako jQuery v PHP. Elegantně prochází DOM různých HTML a XML dokumentů, pomocí CSS3 selektorů nalezne vše, co se dá a dokonce umí vyplnit HTML formulář. Zkrátka je to velmi chytrá nadstavba nad PHP DOM rozšířením.

Příklad na začátek

Knihovnu DomCrawler nainstaluje pomocí Composeru

composer require symfony/dom-crawler

Hned si ukážeme, co dovede. Vypíšeme názvy všech dílů seriálu o Symfony, které jsou uvedeny na začátku této stránky.

use Symfony\Component\DomCrawler\Crawler;

require_once __DIR__.'/vendor/autoload.php';

$url = 'https://www.zdrojak.cz/clanky/symfony-po-kruckach-domcrawler-cssselector/';
$html = file_get_contents($url);

$crawler = new Crawler($html);

foreach ($crawler->filterXPath('//*[@id="serial"]/li/*[1]') as $domElement) {
    var_dump(trim($domElement->textContent));
}

Kód stáhne HTML stránku, předá ji třídě Crawler a pomocí metody filterXPath vyhledá nadpisy jednotlivých dílů seriálu. Velmi snadné.

Procházíme DOM

Komponenta DomCrawler obsahuje užitečné metody pro procházení DOM stromu. Chceme-li získat například třetí článek ze seznamu použijeme metodu eq()

$thirdArticle = $crawler->filterXPath('//*[@id="serial"]/li')->eq(2);

První a poslední prvek seznamu nám vrátí metody first() a last(). Metoda children() vrátí přímé potomky aktuálního elementu. Zavoláním metody parents() dostaneme pole všech rodičů aktuálního prvku. Předchozí sousedy vrací metoda previousAll(), následující nextAll() a všechny sousedy získáme přessiblings(). Sousedské vztahy jsou vždycky trochu komplikované. Pro přehlednost jsou výsledky těchto metod uvedeny v následující tabulce:

Kód Výsledek
$thirdArticle 3. Symfony po krůčkách – Filesystem a Finder
$thirdArticle->previousAll() 2. Symfony Console jako první rande se Symfony
1. Symfony po krůčkách – Event Dispatcher
$thirdArticle->nextAll() 4. Symfony po krůčkách – Paralýza možností? OptionsResolver tě zachrání
5. Symfony po krůčkách – spouštíme procesy
6. …
$thirdArticle->siblings() 1. Symfony po krůčkách – Event Dispatcher
2. Symfony Console jako první rande se Symfony
4. Symfony po krůčkách – Paralýza možností? OptionsResolver tě zachrání
5. …

Všimněme si, že metoda previousAll() vrací prvky v relativním pořadí vůči aktuálnímu elementu. Proto jsou v opačném pořadí, než jak jsou uvedeny v HTML kódu.

Pojďme na další užitečné metody. Aktuální HTML kód vrátí metoda html() a všechen zanořený text vyzobe metoda text(). Hodnotu atributu získáme pomocí attr(). Pokud chceme dostat hodnotu atributů pro všechny prvky, použijeme extract()

$crawler->filterXPath('//*[@id="serial"]/li/*[2]')->attr('class'); 
// vrátí "date"

$crawler->filterXPath('//*[@id="serial"]/li/*[1]')->extract(['href', '_text']);
/* vrátí
[
    0 => [
        0 => "https://www.zdrojak.cz/clanky/symfony-po-kruckach-event-dispatcher/",
        1 => "Symfony po krůčkách – Event Dispatcher",
    ],
    1 => ...
    ...
]
*/

Speciální atribut _text označuje textovou hodnotu DOM elementu.

Crawler versus DOMNode

Všechny výše uvedené metody pro procházení DOMu vrací vždy novou instanci třídy Crawler. Díky tomu lze filtrovat a procházet vybrané podčásti HTML dokumentů. Pokud se ale podíváme na úplně první příklad, všimneme si, že při procházení v cyklu dostáváme instance třídy DOMNode.

Pokud chceme v cyklu pracovat s objektem Crawler, musíme použít metodu each() s anonymní funkcí

$crawler->filterXPath('//*[@id="serial"]/li/*[1]')->each(function(Crawler $node) {
    return trim($node->text());
});
// vrátí pole s názvy jednotlivých článků

A pokud chceme vrátit objekt DOMNode, zavoláme metodu getNode

$thirdArticle = $crawler->filterXPath('//*[@id="serial"]/li')->getNode(2);

CssSelector

Pro výběr prvků v XML a HTML dokumentech PHP podporuje jazyk XPath. Je to silný nástroj, bohužel je příliš komplikovaný a stručnost zápisu rozhodně nepatří mezi jeho silné stránky. Elementy se daleko snáze vybírají pomocí CSS selektorů. V javascriptu stačí napsat document.querySelectorAll('h1 span') nebo $('h1 span') a prvek je vybraný. Stejnou funkčnost do PHP přináší Symfony komponenta CssSelector. Využívá toho, že téměř všechny CSS selektory jsou převeditelné na XPath, kterému už PHP rozumí. Instaluje se přes Composer.

composer require symfony/css-selector

Obsahuje jedinou veřejnou třídu CssSelectorConverter, která má jedinou metodu toXPath. Snadnější to už ani nemůže být.

use Symfony\Component\CssSelector\CssSelectorConverter;

$converter = new CssSelectorConverter();
$xpath = $converter->toXPath('div.item > h4 a');

Komponentu stačí pouze nainstalovat, nemusíme ji nijak registrovat a v DomCrawleru ji můžeme hned využívat voláním metody filter(). Místo $crawler->filterXPath('//*[@id="serial"]/li/*[1]') zavoláme

$crawler->filter('#serial li *:first-child');

Přesný převod XPath zápisu //*[@id="serial"]/li/*[1] na CSS je #serial > li > *:first-child. Je vidět, že CSS nemusí být vždy kratší a přehlednější než XPath.

Komponenta CssSelector rozumí CSS1, CSS2 a dokonce i CSS3 selektorům.

Perličkou pro příznivce znovupoužitelnosti kódu a interoperability je, že knihovna vznikla jako věrná kopie Python balíčku cssselectec.

Odkazy a url adresy

Vyhledat odkaz pomocí selektoru už umíme $crawler->filter('a:contains("Přidat komentář")'). Ale Symfony jde ještě dál a hledání odkazů podle jejich textu maximálně zjednodušuje. Metoda selectLink()

$links = $crawler->selectLink('Přidat komentář');

nalezne nejenom textové odkazy <a href="...">Přidat komentář</a>, ale také obrázkové <a href="..."><img src="..." alt="Přidat komentář"></a>, které vyhledává podle textu v atributu alt.

Jak se ale dostat k správné URL adrese odkazu? Vždyť v html kódu může být uvedena buď absolutní nebo relativní adresa. S tímto problémem si umí poradit objekt třídy Link, který získáme zavoláním metody link()

$link = $crawler->selectLink('Přidat komentář')->link();

Nejdůležitější metodou tohoto objektu je getUri(), která vrátí kompletní url adresu odkazu

$addCommentUrl = $link->getUri();

Aby to celé fungovalo i pro relativní adresy, musí komponenta znát aktuální url adresu zkoumané stránky nebo bázovou url adresu. Obě adresy se předávají přes konstruktor třídy Crawler

use Symfony\Component\DomCrawler\Crawler;

$crawler = new Crawler($html, $currentUri, $baseUri);

Jsou to nepovinné argumenty. Pokud je oba zapomeneme vyplnit, skončí veškeré pokusy o získání kompletní url adresy relativního odkazu vyhozením výjimky InvalidArgumentException('Current URI must be an absolute URL.'). Stejně jako se nám stalo se získáním URL tlačítka pro zobrazení komentářů. Upravený kód vypadá takto.

$crawler = new Crawler($html, $url);

$addCommentUrl = $crawler->selectLink('Přidat komentář')->link()->getUri();

Bázové URL a ani URL stránky nemusíme zadávat, pokud stránka obsahuje vyplněný HTML tag <base>. Symfony ho při parsování HTML správně interpretuje a nastaví podle něho bázovou URL.

Nyní známe URL adresu pro přidání komentáře ke článku. Pojďme na tuto adresu a zkusme vyplnit formulář přes DomCrawler.

$crawler = new Crawler(file_get_contents($addCommentUrl), $addCommentUrl);

Formuláře

Pro formuláře nabízí komponenta DomCrawler speciální třídu Form. Stačí nad vybraným HTML tagem formuláře zavolat metodu form()

$form = $crawler->filter('form#commentform')->form();

Přímý výběr tagu formuláře vyžaduje zkoumání HTML kódu, zda formulář má třeba id atribut nebo nějakou třídu. Symfony nabízí lepší způsob. Dotázat se na text submitovacího tlačítka, protože to každý hned vidí.

$form = $crawler->selectButton('Odeslat komentář')->form();

Metoda selectButton() rozpozná všechny možné způsoby zápisu tlačítek i obrázková tlačítka.

Data do formuláře dostaneme hromadně přes metodu setValues()

$form->setValues([
    'title' => 'DomCrawler je skvělá Symfony komponenta',
    'author' => 'Symfony DomCrawler'
]);

Ke konkrétnímu prvku formuláře můžeme přistoupit metodou get() nebo jako k prvku pole. Třída Form totiž implementuje rozhraní ArrayAccess

$form->get('comment')->setValue('Testovací komentář');
$form['email']->setValue('test@example.org');

S prvky formuláře jde dělat spoustu zajímavých věcí.

// zaškrtnout/odškrtnout checkbox
$form['robot']->tick();
$form['robot']->untick();

// vybrat položku(y) ze seznamu
$form['year']->select(2016);
$form['interests']->select(['symfony', 'doctrine']);

// nahrát soubor
$form['photo']->upload('/path/to/gravatar.png');

Nakonec data formuláře $form->getValues() odešleme na URL $form->getUri() požadovanou HTTP metodou $form->getMethod()

$context = stream_context_create([
    'http' => [
        'header'  => "Content-type: application/x-www-form-urlencoded\r\n",
        'method'  => $form->getMethod(),
        'content' => http_build_query($form->getValues()),
    ]
]);

var_dump(file_get_contents($form->getUri(), false, $context)));

Povedlo se? A nebo jste byli odhaleni, že jste roboti? :)

Co jsme si dnes ukázali

  • komponentu DomCrawler pro snadné procházení DOMu HTML stránek.
  • komponentu CssSelector pro převod CSS selektorů na XPath.
  • že je dobré se inspirovat knihovnami z ostatních programovacích jazyků.

A slíbené překvapení na závěr?

V jednotlivých městech se nám pěkně rozběhly Symfony srazy. Protože stránky http://srazy.info, na kterých jednotlivá setkání evidujeme, nemají funkční API, vytvořil jsem knihovnu webuni/srazy-api-client. Knihovna pomocí komponenty DomCrawler vyzobává data z HTML stránek a servíruje je jako PHP objekty.

Zdroj: https://www.zdrojak.cz/?p=17851