Pojďme jednou provždy rozlousknout tuhle věčnou otázku, která rozděluje komunitu programátorů. Rozhodl jsem se ponořit do temných vod regulárních výrazů, abych přinesl odpověď (spoiler: ano, je to možné).
Takže, co vlastně HTML dokument obsahuje? Jde o mix textu, entit, značek, komentářů a speciální značky doctype. Prozkoumejme nejprve každou ingredienci zvlášť.
Entity
Základem HTML stránky je text, který tvoří obyčejné znaky a
speciální sekvence zvané HTML entity. Ty mohou být buď pojmenované, jako
je
pro nezlomitelnou mezeru, nebo číselné, a to buď
v desítkovém  
nebo šestnáctkovém
 
formátu. Regulární výraz, který zachytí HTML
entitu, by vypadal takto:
(?<entity>
&
(
[a-z][a-z0-9]+ # pojmenovaná entita
|
\#\d+ # desítkové číslo
|
\#x[0-9a-f]+ # hexadecimální číslo
)
;
)
Všechny regulární výrazy jsou zapsány v režimu extended, ignorují
velikost písmen a tečka představuje jakýkoliv znak. Tj. modifikátor
six
.
Značky
Tyto ikonické prvky dělají HTML tím čím je. Tag začíná
<
, následuje název tagu, možná sada atributů a uzavírá se
>
nebo />
. Atributy mohou mít volitelnou
hodnotu a ta může být uvozena do dvojitých, jednoduchých nebo žádných
uvozovek. Regulární výraz zachytávající atribut by vypadal takto:
(?<attribute>
\s+ # alespoň jeden bílý znak před atributem
[^\s"'<>=`/]+ # název atributu
(
\s* = \s* # rovnítko před hodnotou
(
" # hodnota uzavřená ve dvojitých uvozovkách
(
[^"] # libovolný znak kromě dvojité uvozovky
|
(?&entity) # nebo HTML entita
)*
"
|
' # hodnota uzavřená v jednoduchých uvozovkách
(
[^'] # libovolný znak kromě uvozovky
|
(?&entity) # nebo HTML entita
)*
'
|
[^\s"'<>=`]+ # hodnota bez uvozovek
)
)? # hodnota je volitelná
)
Všimněte si, že se odvolávám na pojmenovanou skupinu entity
definovanou dříve.
Elementy
Element může představovat jak samostatná značka (tzv. prázdný element), tak značky párové. Existuje pevný výčet jmen prázdných elementů, podle kterých je rozeznáme. Regulární výraz pro jejich zachytávání by vypadal takto:
(?<void_element>
< # začátek značky
( # název elementu
img|hr|br|input|meta|area|embed|keygen|source|base|col
|link|param|basefont|frame|isindex|wbr|command|track
)
(?&attribute)* # volitelné atributy
\s*
/? # volitelné /
> # konec značky
)
Ostatní značky jsou tedy párové a zachytí je tento regulární výraz
(používám v něm odvolávku na skupinu content
, kterou teprve
nadefinujeme):
(?<element>
< # počáteční značka
(?<element_name>
[a-z][^\s/>]* # název elementu
)
(?&attribute)* # volitelné atributy
\s*
> # konec počáteční značky
(?&content)*
</ # koncová značka
(?P=element_name) # zopakujeme název elementu
\s*
> # konec koncové značky
)
Speciálním případem jsou elementy jako <script>
,
jejichž obsah se musí zpracovávat odlišně od ostatních elementů:
(?<special_element>
< # počáteční značka
(?<special_element_name>
script|style|textarea|title # název elementu
)
(?&attribute)* # volitelné atributy
\s*
> # konec počáteční značky
(?> # atomická skupina
.*? # nejmenší možný počet jakýchkoliv znaků
</ # koncová značka
(?P=special_element_name)
)
\s*
> # konec koncové značky
)
Líný kvantifikátor .*?
zajistí, že se výraz zastaví
u první ukončovací sekvence, a atomická skupina zajistí, že toto
zastavení bude definitivní.
Komentáře
Typický HTML komentář začíná sekvencí <!--
a končí
sekvencí -->
. Regulární výraz pro HTML komentáře může
vypadat takto:
(?<comment>
<!--
(?> # atomická skupina
.*? # nejmenší možný počet jakýchkoliv znaků
-->
)
)
Líný kvantifikátor .*?
opět zajistí, že se výraz zastaví
u první ukončovací sekvence, a atomická skupina zajistí, že toto
zastavení bude definitivní.
Doctype
Jde o historický relikt, který dnes existuje jen proto, aby přepnul
prohlížeč do tzv. standardního režimu. Obvykle vypadá jako
<!doctype html>
, ale může obsahovat i další znaky. Zde
je regulární výraz, který jej zachytí:
(?<doctype>
<!doctype
\s
[^>]* # jakékoliv znaky kromě '>'
>
)
Dejme to dohromady
Když máme hotové regulární výrazy zachytávající každou část HTML, je čas vytvořit výraz pro celý HTML 5 dokument:
\s*
(?&doctype)? # volitelný doctype
(?<content>
(?&void_element) # prázdný element
|
(?&special_element) # speciální element
|
(?&element) # párový element
|
(?&comment) # komentář
|
(?&entity) # entita
|
[^<] # znak
)*
Všechny části můžeme spojit do jednoho komplexního regulárního výrazu. Tohle je on, superhrdina mezi regulárními výrazy se schopností parsovat HTML 5.
Závěrečné poznámky
I když jsme si ukázali, že HTML 5 lze parsovat pomocí regulárních výrazů, uvedený příklad k ničemu užitečný není. Nepomůže vám se zpracováním HTML dokumentu. Vyláme si zuby u nevalidního dokumentu. Bude pomalý. A tak dále. V praxi se používají spíš reguláry jako je tento (pro hledání URL obrázků):
<img.+?src=["'](.+?)["'].*?>
Ale to je opravdu velmi nespolehlivé řešení, která vede k chybám.
Tento regexp chybně matchuje
třeba custom tagy jako například
<imgs-tag src="image.jpg">
, custom atributy jako
<img data-src="custom info">
, nebo selže, když atribut bude
obsahovat uvozovku <img src="mcdonald's.jpg">
. Proto je
doporučeno používat specializované knihovny. Ve světě PHP máme
smolíčka, protože rozšíření DOM podporuje pouze pravěké ztrouchnivělé
HTML 4. Naštěstí PHP 8.4 slibuje parser pro HTML
5.
Komentáře
Tobiáš Potoček #1
#til: Regulární výrazy podporují rekurzi. Bez ní skutečně HTML parsovat nejde.
Napište komentář