Hledání HTML odkazů pomocí regulárních výrazů
Školení, která pořádám
Řekněme, že máme v HTML kódu najít všechny odkazy. To je přece hračka, ne?
<?php
preg_match_all('~<a href="([^"]*)"~i', $html, $matches);
$hrefs = array_map('html_entity_decode', $matches[1]);
?>
Bohužel to tak jednoduché není, vidím tady nejméně čtyři problémy:
- Atribut
href
nemusí být uveden jako první.
- Hodnota atributu nemusí být uzavřena do uvozovek, ale do apostrofů, nebo taky vůbec.
- Odkazy v HTML komentářích nás určitě nezajímají.
- Kolem rovnítka mohou být mezery.
Zkusme tedy druhý pokus:
<?php
$hrefs = array();
preg_match_all('~<a\\s[^>]+~i', preg_replace('~<!--.*-->~sU', '', $html), $matches);
foreach ($matches[0] as $tag) {
if (preg_match('~\\shref\\s*=\\s*(?|"([^"]*)"|\'([^\']*)\'|(\\S*))~i', $tag, $match)) {
$hrefs[] = html_entity_decode($match[1], ENT_QUOTES);
}
}
?>
Radikálně se to zkomplikovalo, navíc bohužel vznikly jiné problémy:
- Znak
>
může být v hodnotě atributu neošetřený, takže se špatně najde konec značky.
- Text
href=
může být náhodou uvnitř jiné hodnoty atributu (např. <a title="href=">
).
Takhle to prostě dělat nejde. Zkusme výhrady tedy zohlednit. Vznikne obludný regulární výraz, který bych nikomu nepřál luštit:
<?php
$hrefs = array();
preg_match_all('~<a\\b([^\'">]|\'[^\']*\'|"[^"]*")*\\shref\\s*=\\s*(?|"([^"]*)"|\'([^\']*)\'|(\\S*))~i', preg_replace('~<!--.*-->~sU', '', $html), $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$hrefs[] = html_entity_decode($match[2], ENT_QUOTES);
}
?>
Ani toto řešení ale není dokonalé. Jde o to, že i znak <
se může nacházet uvnitř hodnoty atributu neošetřený, takže hledání <a
může ve skutečnosti najít <span title="<a href=" class="konec">
. A to je prakticky konec. Abychom problém tímto způsobem vyřešili, museli bychom vytvořit jeden monstrózní regulární výraz, který by přeskakoval nezajímavé značky a komentáře (protože ty jsou platné taky jen mimo hodnoty atributu) a strefil by jen onu značku <a href="">
.
Řešení je v tomto případě naštěstí jednoduché a jmenuje se DOMDocument::loadHTML
. Jak postupovat v případě, že potřebujeme zpracovat dokument ve formátu, pro který nemáme parser, popíšu příště. Pointa je, že i když je dokument velmi jednoduchý, tak to bez sledování kontextu obvykle nejde.
Diskuse
petasan:
Dobrý den, znamená to tedy, že po zavolání parseru a znovu uložení html (které by mělo být v pořdáku) již stačí použít 1. řešení nebo spíše 2.?
Tak ono už to není nutné, parser poskytne všechny potřebné informace pomocí xpath. Já bych spíš chtěl vidět v praxi to načtení html, snažím se parsovat pár stovek html stránek denně, a možná je to moje neznalost, ale dělám psí kusy aby parser ty html načetl.
Od kódování až po různé opravy neuzavřených značek atd. Kvalita html stránek na ČR netu je děsivá, meta tag s kódováním často neodpovídá samotnému kódování (otázka co s tím), dost často neuzavřené tagy, křížící se tagy atd.
DOMDocument si také poradí s nevalidním HTML... stačí volat @$dom->loadHTML(); :))
A mimo jiné SimpleHTMLDom má pro některá využití docela zásadní nevýhodu... plýtvá dost pamětí... tedy nahraje celý DOM do paměti a pak v něm hledá.
SHD je pro jednotlivé malé parsery možná šikovný a easy-to-use, ale jinak nepoužitelný.
Rozhodně ne. Je nutné vždy použít pořádné parsování, v PHP nejčastěji právě pomocí DOMDocument::loadHTML. Konstrukce <a title="<!--"> je totiž úplně v pořádku, jen je trochu nezvyklá.
Jan Tvrdík:
Pokud už máš ten dokument naparsovaný, tak není problém použít třídu DOMXPath.
doporučuji projekt phpQuery, nebo moji osekanou-namespaced verzi https://github.com/HosipLan/phpQueryLite (původní obsahuje bůhvíproč emulaci ajaxu a navíc nefunguje :)
$query = phpQuery\phpQuery::newDocument($mojeHTML);
foreach($query['a'] as $link){
echo $link->attr('href'), "<br>\n";
}
copak se to stalo s lomítkem mezi phpQuery phpQuery ? :(
Lomítko zmizelo, protože používám vestavěný zvýrazňovač syntaxe PHP staršího než 5.3. Omlouvám se za komplikace, zatím jsem to vyřešil odstraněním PHP značek, časem přejdu na JUSH.
Teď mě tak ještě napadlo, co takhle použít
<?php
$html = strip_tags($html, '<a>');
?>
čímž snad zůstanou pouze tagy odkazu,
pak nějak ořezat text co je mimo elementy a zůstane hromádka tagů :)
Martin Kopta:
Kodovani uvedene v meta nemusi odpovidat kodovani dokumentu, pokud bylo upraveno hlavickou HTTP. Ze ani na HTTP se neda spolehnout, je vec jina. K identifikaci znakove sady by se dala pouzit analyza znaku v dokumentu.
starenka:
A co je špatnýho na použití nenažranosti?
<?php
preg_match_all('@<a.*?href=["\'](.*?)["\'].*?>@i',file_get_contents('http://www.starenka.net'),$moo);
var_dump($moo);
?>
David Grudl:
Ten regulár je špatný asi tak z milionu důvodů.
- vezme <a stejně jako <abbr
- obdobně nerozlišuje mezi href="" a nohref="..."
- za href nesmí být mezera
- nezvládne mix uvozovek href="John O'Connor"
- nezvládne atribut bez uvozovek href=index
- chybí modifikátor s
- a především: matchne <abbr title="href='haha'">
starenka:
Pravda. Lidi jsou zli, Esterko :(
Diskuse je zrušena z důvodu spamu.