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:

  1. Atribut href nemusí být uveden jako první.
  2. Hodnota atributu nemusí být uzavřena do uvozovek, ale do apostrofů, nebo taky vůbec.
  3. Odkazy v HTML komentářích nás určitě nezajímají.
  4. 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:

  1. Znak > může být v hodnotě atributu neošetřený, takže se špatně najde konec značky.
  2. 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.

Jakub Vrána, Dobře míněné rady, 10.11.2010, diskuse: 15 (nové: 0)

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.?

mcmatak:

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.

Petr Herma:

Docela se mi osvědčil tento html parser, poradil si i z ne právě validní stránkou

http://simplehtmldom.sourceforge.net/manual.htm

ikona Radek Šimko:

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ý.

ikona Jakub Vrána OpenID:

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.

HosipLan:

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";
}

HosipLan:

copak se to stalo s lomítkem mezi phpQuery phpQuery ? :(

ikona Jakub Vrána OpenID:

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.

HosipLan:

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ů :)

ikona Jakub Vrána OpenID:

Jak tak koukám na zdroják strip_tags (http://svn.php.net/viewvc/php/php-src/tags/…=markup#l4245), tak zrovna k tomuhle účelu by se to použít dalo. Dobrý nápad!

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

ikona 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.

avatar © 2005-2024 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.