Kawałek Kodu

Jeśli miałeś kiedyś potrzebę przeszukiwania kod HTML pod kątem wyłuskania jakichś informacji, to pewnie korzystałeś z wyrażeń regularnych, a potem trafiłeś na drogę prowadzącą do narzędzi typu PHP Simple HTML Dom Parser.

Wyrażenia regularne mogą być kłopotliwe same w sobie, a powiązanie ich ze strukturą HTML może przyprawić o ból głowy. Wspomniany parser przenosi nas o poziom wyżej pod względem wygody użytkowania, ale i tak nie spełnia wszystkich pokładanych weń nadziei. Wyszukiwanie elementów, których nie znamy dokładnego położenia (może być zmienne) staje się wprost niemożliwe, trafiamy więc w ślepy zaułek.

Którą z dróg wybrać?

Przejdźmy do konkretów i weźmy za przykład poniższy kod. Chcemy uzyskać wartość parametru kolor, czyli zawartość DIV następującego po elemencie SPAN o zawartości kolor. Ponieważ dla każdego produktu lista parametrów jest inna (nasze założenie), szukany parametr może być w innym miejscu (na innej pozycji) w strukturze UL. Teraz jest on pierwszy, ale może być ostatni lub też w środku, pomiędzy innymi parametrami.

<ul class="parametry">

<li class="parametr">
<span class="nazwa">kolor</span>
<div class="wartosc">czerwony</div>
</li>

<li class="parametr">
<span class="nazwa">waga</span>
<div class="wartosc">1 kg</div>
</li>

<li class="parametr">
<span class="nazwa">cena</span>
<div class="wartosc">20 zł</div>
</li>

</ul>
$dom = new DOMDocument;
$dom->loadHTML($html);
$dom->encoding = 'UTF-8';
$xpath = new DOMXPath($dom);
$divs = $xpath->query('//span[text()="kolor"]/following-sibling::div');
foreach($divs as $div){
    echo $div->nodeValue;
}

To dziwne coś w linii 5 to ścieżka XPath. Zasada podążania nią jest następująca:

// div / a[@href=ścieżka"menu.html"]
ścieżka ścieżka

Nas interesuje co się tam dokładnie dzieje (w linii 5). Wyszukujemy ścieżkę zawierającą SPAN z zawartością tekstową "kolor", a następnie bezpośredniego sąsiada, czyli DIV. Wartość koloru mamy pod właściwością nodeValue. Wygląda chyba klarowniej niż wyrażenie regularne, prawda?

Ten fragment możemy nieco zmienić dzięki bezpośredniego dostępowi do węzła tekstowego, choć i tak "posiadając" węzeł tekstowy musimy pobrać jego wartość jak w przykładzie powyższym:

$texts = $xpath->query('//span[text()="kolor"]/following-sibling::div/text()');
foreach($texts $text){
    echo $text->nodeValue;
}

W tym przykładzie paradoksalnie to chyba wyrażenie regularne byłoby prostsze do użycia niż wspominany na początku parser. W parserze nie mamy możliwości pobrania węzła z ustaloną wartością tekstową - trzeba wybrać wszystkie SPAN, w pętli wyselekcjonować te, które zawierają tekst "kolor", a następnie dla każdego takiego elementu pobrać sąsiada (next_sibling). Natomiat dzięki DOMXPath nie musimy filtrować elementów korzystając z funkcji text.

Może nie jest to oczywiste, ale oprócz czytania danych jest możliwa modyfikacja drzewa DOM. Tak więc możemy usuwać elementy, zmieniać im atrybuty czy też modyfikować węzły tekstowe.

Chcę iść o krok dalej!

Na początek wystarczy Ci znajomość kilku poniższych wyrażeń. Dalej znajdziesz przykłady popularnych selektorów z CSS i JavaScript.

// dziecko
/ bezpośrednie dziecko
@ atrybut
.. rodzic

Porównajmy przykładowe selektory używane w CSS z ich odpowiednikami dla DOMXPath:

CSS DOMXPath
* //*
div //div
div, span //div | //span
div.sekcja //div[@class="sekcja"]
span#data //span[@id="data"]
h1 img //h1//img
h1>img //h1/img
section+h2 //section/following-sibling::h2[1]
h2 ~ div //h2/following-sibling::div
[disabled] //[@disabled]
[data-id="1"] //[@data-id="1"]
[title*="torba"] //*[contains(@title,'torba')]
[name^="pole"] //*[starts-with(@name,'pole')]
[href$=".pdf"]

//a[ends-with(@href='.pdf')]
//a[substring(@href,string-length(@href)-3)='.pdf']

div>p:first-child //div/p[1]
div>p:nth-child(3) //div/p[3]
table tr:nth-child(even) //table//tr[(position()-1) mod 2=1]
table tr:nth-child(odd) //table//tr[(position()-1) mod 2=0]
div>p:last-child //div/p[last()]
div>p:only-child //div[count(*[not(self::p)])=0]/p
div>p:only-of-type //div[count(*)=1]/p
:not(div) //*[not(self::div)]
:root /

W popularnym wciąż framework'u jQuery dostępne są specyficzne selektory spoza specyfikacji dotyczącej CSS. Zestawmy te możliwe do odwzorowania w DOMXPath:

jQuery lub JavaScript DOMXPath
ul li:eq(3) //ul//li[4]
ul li:gt(2) //ul//li[position()>3]
ul li:lt(4) //ul//li[position()<5]
:header //h1 | //h2 | //h3 | //h4 //h5 //h6
span:contains('kwiatek') //span[contains(text(),'kwiatek')]
div:has(p) //p/ancestor::div
p:empty //p[count(*)=0 and string-length()=0]
span:parent //span/..
img[title!="telefon"] //img[@title!='telefon']
   
el.hasClass('klasa') (el=DIV) //div[contains(concat(' ',normalize-space(@class),' '),' klasa ')]
el.closest('fieldset') (el=INPUT) //input/ancestor-or-self::fieldset[1]
el.parents() (el=INPUT) //input/ancestor::*
el.parents('li') (el=SPAN) //span/ancestor::li

Może się zdarzyć, że w wyniku wyboru elementów według ścieżki XPath otrzymamy również tagi HTML i BODY, choć we fragmencie, który poddajemy obróbce ich nie było. W takim przypadku należy przekazać metodzie loadHTML jako drugi parametr wartość: LIBXML_HTML_NOIMPLIED, która spowoduje niedodawanie wspomnianych elementów. Flaga LIBXML_HTML_NODEFDTD wyłącza dodanie DOCTYPE jeśli takiego było brak w dokumencie.

W następnej części podążając króliczą norą, postaram się przekazać Ci więcej praktycznej wiedzy w czerwonej pigułce.

 

Przydatne linki:
Dokumentacja XPath
"The tag is out there", czyli DOMXPath S01E02.
"The tag is out there", czyli DOMXPath S01E03.
"The tag is out there", czyli DOMXPath S01E04.