O tak, możesz użyć Regexes do parsowania HTML!
W przypadku zadania, które próbujesz, wyrażenia regularne są w porządku!
Prawdą jest, że większość ludzi nie docenia trudności w przetwarzaniu HTML za pomocą wyrażeń regularnych i dlatego robi to słabo.
Ale to nie jest jakaś podstawowa wada związana z teorią obliczeniową. Ta głupota jest tutaj dużo papugowana , ale nie wierzcie im.
Chociaż z pewnością można to zrobić (ten post służy jako dowód istnienia tego niezaprzeczalnego faktu), to nie znaczy, że tak powinno być.
Musisz sam zdecydować, czy masz zamiar napisać, co stanowi dedykowany, specjalny parser HTML z wyrażeń regularnych. Większość ludzi nie jest.
Ale ja jestem. ☻
Ogólne oparte na regeksie rozwiązania do analizowania HTML
Najpierw pokażę, jak łatwo można parsować dowolny kod HTML za pomocą wyrażeń regularnych. Pełny program znajduje się na końcu tego ogłoszenia, ale sercem parsera jest:
for (;;) {
given ($html) {
last when (pos || 0) >= length;
printf "\@%d=", (pos || 0);
print "doctype " when / \G (?&doctype) $RX_SUBS /xgc;
print "cdata " when / \G (?&cdata) $RX_SUBS /xgc;
print "xml " when / \G (?&xml) $RX_SUBS /xgc;
print "xhook " when / \G (?&xhook) $RX_SUBS /xgc;
print "script " when / \G (?&script) $RX_SUBS /xgc;
print "style " when / \G (?&style) $RX_SUBS /xgc;
print "comment " when / \G (?&comment) $RX_SUBS /xgc;
print "tag " when / \G (?&tag) $RX_SUBS /xgc;
print "untag " when / \G (?&untag) $RX_SUBS /xgc;
print "nasty " when / \G (?&nasty) $RX_SUBS /xgc;
print "text " when / \G (?&nontag) $RX_SUBS /xgc;
default {
die "UNCLASSIFIED: " .
substr($_, pos || 0, (length > 65) ? 65 : length);
}
}
}
Widzisz, jak łatwo to odczytać?
Jak napisano, identyfikuje każdy fragment HTML i informuje, gdzie go znalazł. Możesz łatwo go zmodyfikować, aby zrobić cokolwiek chcesz z dowolnym rodzajem elementu lub dla bardziej konkretnych typów niż te.
Nie mam nieudanych przypadków testowych (po lewej :): Udało mi się uruchomić ten kod na ponad 100 000 plików HTML - każdego z nich mogłem szybko i łatwo zdobyć. Poza tym uruchomiłem go również na plikach specjalnie skonstruowanych w celu przełamania naiwnych parserów.
To nie jest naiwny parser.
Och, jestem pewien, że nie jest idealny, ale nie udało mi się go jeszcze złamać. Wydaje mi się, że nawet gdyby coś zrobiło, poprawka byłaby łatwa do dopasowania ze względu na przejrzystą strukturę programu. Nawet programy z dużym regexem powinny mieć strukturę.
Teraz, gdy to już nie przeszkadza, pozwolę sobie odpowiedzieć na pytanie OP.
Demo rozwiązania zadania PO przy użyciu Regexes
Mały html_input_rx
program, który zamieszczam poniżej, generuje następujące dane wyjściowe, dzięki czemu można zobaczyć, że analizowanie kodu HTML za pomocą wyrażeń regularnych działa dobrze dla tego, co chcesz zrobić:
% html_input_rx Amazon.com-_Online_Shopping_for_Electronics,_Apparel,_Computers,_Books,_DVDs_\&_more.htm
input tag #1 at character 9955:
class => "searchSelect"
id => "twotabsearchtextbox"
name => "field-keywords"
size => "50"
style => "width:100%; background-color: #FFF;"
title => "Search for"
type => "text"
value => ""
input tag #2 at character 10335:
alt => "Go"
src => "http://g-ecx.images-amazon.com/images/G/01/x-locale/common/transparent-pixel._V192234675_.gif"
type => "image"
Analizuj tagi wejściowe, patrz: Brak zła
Oto źródło programu, który wygenerował wynik powyżej.
#!/usr/bin/env perl
#
# html_input_rx - pull out all <input> tags from (X)HTML src
# via simple regex processing
#
# Tom Christiansen <tchrist@perl.com>
# Sat Nov 20 10:17:31 MST 2010
#
################################################################
use 5.012;
use strict;
use autodie;
use warnings FATAL => "all";
use subs qw{
see_no_evil
parse_input_tags
input descape dequote
load_patterns
};
use open ":std",
IN => ":bytes",
OUT => ":utf8";
use Encode qw< encode decode >;
###########################################################
parse_input_tags
see_no_evil
input
###########################################################
until eof(); sub parse_input_tags {
my $_ = shift();
our($Input_Tag_Rx, $Pull_Attr_Rx);
my $count = 0;
while (/$Input_Tag_Rx/pig) {
my $input_tag = $+{TAG};
my $place = pos() - length ${^MATCH};
printf "input tag #%d at character %d:\n", ++$count, $place;
my %attr = ();
while ($input_tag =~ /$Pull_Attr_Rx/g) {
my ($name, $value) = @+{ qw< NAME VALUE > };
$value = dequote($value);
if (exists $attr{$name}) {
printf "Discarding dup attr value '%s' on %s attr\n",
$attr{$name} // "<undef>", $name;
}
$attr{$name} = $value;
}
for my $name (sort keys %attr) {
printf " %10s => ", $name;
my $value = descape $attr{$name};
my @Q; given ($value) {
@Q = qw[ " " ] when !/'/ && !/"/;
@Q = qw[ " " ] when /'/ && !/"/;
@Q = qw[ ' ' ] when !/'/ && /"/;
@Q = qw[ q( ) ] when /'/ && /"/;
default { die "NOTREACHED" }
}
say $Q[0], $value, $Q[1];
}
print "\n";
}
}
sub dequote {
my $_ = $_[0];
s{
(?<quote> ["'] )
(?<BODY>
(?s: (?! \k<quote> ) . ) *
)
\k<quote>
}{$+{BODY}}six;
return $_;
}
sub descape {
my $string = $_[0];
for my $_ ($string) {
s{
(?<! % )
% ( \p{Hex_Digit} {2} )
}{
chr hex $1;
}gsex;
s{
& \043
( [0-9]+ )
(?: ;
| (?= [^0-9] )
)
}{
chr $1;
}gsex;
s{
& \043 x
( \p{ASCII_HexDigit} + )
(?: ;
| (?= \P{ASCII_HexDigit} )
)
}{
chr hex $1;
}gsex;
}
return $string;
}
sub input {
our ($RX_SUBS, $Meta_Tag_Rx);
my $_ = do { local $/; <> };
my $encoding = "iso-8859-1"; # web default; wish we had the HTTP headers :(
while (/$Meta_Tag_Rx/gi) {
my $meta = $+{META};
next unless $meta =~ m{ $RX_SUBS
(?= http-equiv )
(?&name)
(?&equals)
(?= (?"e)? content-type )
(?&value)
}six;
next unless $meta =~ m{ $RX_SUBS
(?= content ) (?&name)
(?&equals)
(?<CONTENT> (?&value) )
}six;
next unless $+{CONTENT} =~ m{ $RX_SUBS
(?= charset ) (?&name)
(?&equals)
(?<CHARSET> (?&value) )
}six;
if (lc $encoding ne lc $+{CHARSET}) {
say "[RESETTING ENCODING $encoding => $+{CHARSET}]";
$encoding = $+{CHARSET};
}
}
return decode($encoding, $_);
}
sub see_no_evil {
my $_ = shift();
s{ <! DOCTYPE .*? > }{}sx;
s{ <! \[ CDATA \[ .*? \]\] > }{}gsx;
s{ <script> .*? </script> }{}gsix;
s{ <!-- .*? --> }{}gsx;
return $_;
}
sub load_patterns {
our $RX_SUBS = qr{ (?(DEFINE)
(?<nv_pair> (?&name) (?&equals) (?&value) )
(?<name> \b (?= \pL ) [\w\-] + (?<= \pL ) \b )
(?<equals> (?&might_white) = (?&might_white) )
(?<value> (?"ed_value) | (?&unquoted_value) )
(?<unwhite_chunk> (?: (?! > ) \S ) + )
(?<unquoted_value> [\w\-] * )
(?<might_white> \s * )
(?<quoted_value>
(?<quote> ["'] )
(?: (?! \k<quote> ) . ) *
\k<quote>
)
(?<start_tag> < (?&might_white) )
(?<end_tag>
(?&might_white)
(?: (?&html_end_tag)
| (?&xhtml_end_tag)
)
)
(?<html_end_tag> > )
(?<xhtml_end_tag> / > )
) }six;
our $Meta_Tag_Rx = qr{ $RX_SUBS
(?<META>
(?&start_tag) meta \b
(?:
(?&might_white) (?&nv_pair)
) +
(?&end_tag)
)
}six;
our $Pull_Attr_Rx = qr{ $RX_SUBS
(?<NAME> (?&name) )
(?&equals)
(?<VALUE> (?&value) )
}six;
our $Input_Tag_Rx = qr{ $RX_SUBS
(?<TAG> (?&input_tag) )
(?(DEFINE)
(?<input_tag>
(?&start_tag)
input
(?&might_white)
(?&attributes)
(?&might_white)
(?&end_tag)
)
(?<attributes>
(?:
(?&might_white)
(?&one_attribute)
) *
)
(?<one_attribute>
\b
(?&legal_attribute)
(?&might_white) = (?&might_white)
(?:
(?"ed_value)
| (?&unquoted_value)
)
)
(?<legal_attribute>
(?: (?&optional_attribute)
| (?&standard_attribute)
| (?&event_attribute)
# for LEGAL parse only, comment out next line
| (?&illegal_attribute)
)
)
(?<illegal_attribute> (?&name) )
(?<required_attribute> (?#no required attributes) )
(?<optional_attribute>
(?&permitted_attribute)
| (?&deprecated_attribute)
)
# NB: The white space in string literals
# below DOES NOT COUNT! It's just
# there for legibility.
(?<permitted_attribute>
accept
| alt
| bottom
| check box
| checked
| disabled
| file
| hidden
| image
| max length
| middle
| name
| password
| radio
| read only
| reset
| right
| size
| src
| submit
| text
| top
| type
| value
)
(?<deprecated_attribute>
align
)
(?<standard_attribute>
access key
| class
| dir
| ltr
| id
| lang
| style
| tab index
| title
| xml:lang
)
(?<event_attribute>
on blur
| on change
| on click
| on dbl click
| on focus
| on mouse down
| on mouse move
| on mouse out
| on mouse over
| on mouse up
| on key down
| on key press
| on key up
| on select
)
)
}six;
}
UNITCHECK {
load_patterns();
}
END {
close(STDOUT)
|| die "can't close stdout: $!";
}
Proszę bardzo! Nic do tego! :)
Tylko Ty możesz ocenić, czy Twoja umiejętność wyrażeń regularnych zależy od konkretnego zadania analizy. Poziom umiejętności każdego jest inny, a każde nowe zadanie jest inne. W przypadku zadań, w których masz dobrze zdefiniowany zestaw danych wejściowych, wyrażenia regularne są oczywiście właściwym wyborem, ponieważ składanie ich razem jest trywialne, gdy masz do czynienia z ograniczonym podzbiorem HTML. Nawet początkujący wyrażenia regularne powinni obsługiwać te zadania za pomocą wyrażeń regularnych. Wszystko inne to przesada.
Jednak gdy HTML zacznie być mniej dopracowany, gdy zacznie rozgryźć w sposób, którego nie można przewidzieć, ale które są całkowicie legalne, gdy będziesz musiał dopasować więcej różnych rzeczy lub bardziej skomplikowane zależności, w końcu osiągniesz punkt, w którym musisz pracować ciężej, aby uzyskać rozwiązanie wykorzystujące wyrażenia regularne, niż musiałbyś użyć klasy parsującej. To, gdzie spada ten próg rentowności, zależy ponownie od własnego poziomu komfortu z wyrażeniami regularnymi.
Więc co powinienem zrobić?
Nie powiem ci, co musisz zrobić, a czego nie . Myślę, że to źle. Chcę tylko przedstawić Ci możliwości, otwórz trochę oczy. Możesz wybrać, co chcesz zrobić i jak chcesz to zrobić. Nie ma absolutów - i nikt inny nie zna twojej sytuacji tak dobrze jak ty sam. Jeśli wydaje się, że to za dużo pracy, to może tak jest. Wiesz, programowanie powinno być zabawne . Jeśli tak nie jest, być może robisz to źle.
Na mój html_input_rx
program można patrzeć na wiele ważnych sposobów. Jednym z nich jest to, że rzeczywiście możesz parsować HTML z wyrażeniami regularnymi. Ale innym jest to, że jest o wiele, wiele, znacznie trudniejsze niż prawie ktokolwiek myśli, że tak jest. Może to prowadzić do wniosku, że mój program jest dowodem na to, co powinno nie robić, bo to naprawdę jest zbyt trudne.
Nie będę się z tym nie zgadzać. Z pewnością jeśli wszystko, co robię w moim programie, nie ma dla ciebie sensu po jakimś badaniu, nie powinieneś próbować używać wyrażeń regularnych do tego rodzaju zadań. W przypadku określonego HTML wyrażenia regularne są świetne, ale w przypadku standardowego HTML są one równoznaczne z szaleństwem. Cały czas używam klas parsujących, zwłaszcza jeśli to HTML, którego sam nie wygenerowałem.
Regeksy optymalne dla małych problemów z parsowaniem HTML, pesymalne dla dużych
Nawet jeśli mój program jest traktowana jako ilustrację dlaczego należy nie używać regexes w celu analizowania ogólnych HTML - co jest OK, bo trochę rozumie ona być że ☺ - to nadal powinna być niespodzianka więc więcej ludzi złamać strasznie powszechne i paskudny, nieprzyjemny nawyk pisania nieczytelnych, nieustrukturyzowanych i niemożliwych do utrzymania wzorów.
Wzory nie muszą być brzydkie i nie muszą być trudne. Jeśli tworzysz brzydkie wzory, jest to odbicie ciebie, a nie ich.
Fenomenalnie wykwintny język regex
Poproszono mnie o wskazanie, że moje profesjonalne rozwiązanie twojego problemu zostało napisane w Perlu. Czy jesteś zaskoczony? Nie zauważyłeś? Czy to objawienie to bomba?
Prawdą jest, że nie wszystkie inne narzędzia i języki programowania są tak wygodne, wyraziste i potężne, jeśli chodzi o wyrażenia regularne, jak Perl. Istnieje duże spektrum, z których niektóre są bardziej odpowiednie niż inne. Ogólnie rzecz biorąc, łatwiej jest pracować z językami, które wyrażają wyrażenia regularne jako część języka podstawowego zamiast jako bibliotekę. Nie zrobiłem nic z wyrażeniami regularnymi, których nie można zrobić, powiedzmy, w PCRE, chociaż program miałby inną strukturę, gdybyś używał C.
W końcu inne języki będą nadążać za tym, gdzie Perl jest teraz pod względem wyrażeń regularnych. Mówię to, ponieważ kiedy Perl zaczął, nikt inny nie miał takich wyrażeń regularnych jak Perl. Mów co chcesz, ale Perl wyraźnie wygrał: wszyscy skopiowali wyrażenia regularne Perla, choć na różnych etapach ich rozwoju. Perl był pionierem prawie (nie do końca, ale prawie) wszystkiego, na czym dzisiaj polegasz w nowoczesnych wzorach, bez względu na to, jakiego narzędzia lub języka używasz. Więc ostatecznie pozostali będą dogonić.
Ale dotrą tylko do miejsca, w którym Perl był kiedyś, tak jak teraz. Wszystko idzie naprzód. W wyrażeniach regularnych, jeśli nic więcej, do czego prowadzi Perl, inni podążają za nimi. Gdzie będzie Perl, gdy wszyscy w końcu dotrą do miejsca, w którym teraz jest Perl? Nie mam pojęcia, ale wiem, że my też się przeprowadzimy. Prawdopodobnie zbliżymy się do stylu tworzenia wzorów Perla .
Jeśli lubisz tego rodzaju rzeczy, ale chciałbyś użyć ich w Perlu, możesz zainteresować się wspaniałym modułem Regexp :: Grammars Damiana Conwaya . Jest całkowicie niesamowity i sprawia, że to, co zrobiłem tutaj w moim programie, wydaje się tak samo prymitywne, jak moje sprawia, że wzorce, które ludzie łączą ze sobą bez białych znaków i alfabetycznych identyfikatorów. Sprawdź to!
Prosty fragment kodu HTML
Oto pełne źródło parsera, który pokazałem na środku tego wpisu na początku tego postu.
Ja nie sugeruje, że należy korzystać z tego ponad rygorystycznie testowane klasy parsowania. Ale mam dość ludzi udających, że nikt nie może analizować HTML za pomocą wyrażeń regularnych tylko dlatego, że nie. Oczywiście możesz, a ten program jest dowodem tego twierdzenia.
Pewnie, że nie jest to łatwe, ale to jest możliwe!
A próba zrobienia tego jest straszną stratą czasu, ponieważ istnieją dobre klasy analizujące, których powinieneś użyć do tego zadania. Prawidłowa odpowiedź dla osób próbujących parsować dowolny kod HTML nie polega na tym, że jest to niemożliwe. To łatwa i nieuczciwa odpowiedź. Prawidłowa i uczciwa odpowiedź jest taka, że nie powinni próbować, ponieważ zbyt trudno jest zrozumieć od zera; nie powinni łamać sobie pleców, starając się odkryć koło, które działa doskonale.
Z drugiej strony HTML, który mieści się w przewidywalnym podzbiorze, jest wyjątkowo łatwy do parsowania za pomocą wyrażeń regularnych. Nic dziwnego, że ludzie próbują ich używać, ponieważ w przypadku drobnych problemów, być może problemów z zabawkami, nic nie może być łatwiejsze. Dlatego tak ważne jest rozróżnienie dwóch zadań - specyficznego od ogólnego - ponieważ niekoniecznie wymagają tego samego podejścia.
Mam nadzieję, że w przyszłości zobaczę bardziej sprawiedliwe i uczciwe traktowanie pytań dotyczących HTML i wyrażeń regularnych.
Oto mój leksykon HTML. Nie próbuje wykonać sprawdzania poprawności; po prostu identyfikuje elementy leksykalne. Możesz myśleć o tym bardziej o chunkrze HTML niż parserze HTML. Nie wybacza bardzo zepsutego HTML, choć wprowadza pewne bardzo niewielkie poprawki w tym kierunku.
Nawet jeśli nigdy nie parsujesz pełnego kodu HTML (a dlaczego miałbyś? To rozwiązany problem!), Ten program ma wiele fajnych bitów wyrażeń regularnych, z których, jak sądzę, wiele osób może się wiele nauczyć. Cieszyć się!
#!/usr/bin/env perl
#
# chunk_HTML - a regex-based HTML chunker
#
# Tom Christiansen <tchrist@perl.com
# Sun Nov 21 19:16:02 MST 2010
########################################
use 5.012;
use strict;
use autodie;
use warnings qw< FATAL all >;
use open qw< IN :bytes OUT :utf8 :std >;
MAIN: {
$| = 1;
lex_html(my $page = slurpy());
exit();
}
########################################################################
sub lex_html {
our $RX_SUBS; ###############
my $html = shift(); # Am I... #
for (;;) { # forgiven? :)#
given ($html) { ###############
last when (pos || 0) >= length;
printf "\@%d=", (pos || 0);
print "doctype " when / \G (?&doctype) $RX_SUBS /xgc;
print "cdata " when / \G (?&cdata) $RX_SUBS /xgc;
print "xml " when / \G (?&xml) $RX_SUBS /xgc;
print "xhook " when / \G (?&xhook) $RX_SUBS /xgc;
print "script " when / \G (?&script) $RX_SUBS /xgc;
print "style " when / \G (?&style) $RX_SUBS /xgc;
print "comment " when / \G (?&comment) $RX_SUBS /xgc;
print "tag " when / \G (?&tag) $RX_SUBS /xgc;
print "untag " when / \G (?&untag) $RX_SUBS /xgc;
print "nasty " when / \G (?&nasty) $RX_SUBS /xgc;
print "text " when / \G (?&nontag) $RX_SUBS /xgc;
default {
die "UNCLASSIFIED: " .
substr($_, pos || 0, (length > 65) ? 65 : length);
}
}
}
say ".";
}
#####################
# Return correctly decoded contents of next complete
# file slurped in from the <ARGV> stream.
#
sub slurpy {
our ($RX_SUBS, $Meta_Tag_Rx);
my $_ = do { local $/; <ARGV> }; # read all input
return unless length;
use Encode qw< decode >;
my $bom = "";
given ($_) {
$bom = "UTF-32LE" when / ^ \xFf \xFe \0 \0 /x; # LE
$bom = "UTF-32BE" when / ^ \0 \0 \xFe \xFf /x; # BE
$bom = "UTF-16LE" when / ^ \xFf \xFe /x; # le
$bom = "UTF-16BE" when / ^ \xFe \xFf /x; # be
$bom = "UTF-8" when / ^ \xEF \xBB \xBF /x; # st00pid
}
if ($bom) {
say "[BOM $bom]";
s/^...// if $bom eq "UTF-8"; # st00pid
# Must use UTF-(16|32) w/o -[BL]E to strip BOM.
$bom =~ s/-[LB]E//;
return decode($bom, $_);
# if BOM found, don't fall through to look
# for embedded encoding spec
}
# Latin1 is web default if not otherwise specified.
# No way to do this correctly if it was overridden
# in the HTTP header, since we assume stream contains
# HTML only, not also the HTTP header.
my $encoding = "iso-8859-1";
while (/ (?&xml) $RX_SUBS /pgx) {
my $xml = ${^MATCH};
next unless $xml =~ m{ $RX_SUBS
(?= encoding ) (?&name)
(?&equals)
(?"e) ?
(?<ENCODING> (?&value) )
}sx;
if (lc $encoding ne lc $+{ENCODING}) {
say "[XML ENCODING $encoding => $+{ENCODING}]";
$encoding = $+{ENCODING};
}
}
while (/$Meta_Tag_Rx/gi) {
my $meta = $+{META};
next unless $meta =~ m{ $RX_SUBS
(?= http-equiv ) (?&name)
(?&equals)
(?= (?"e)? content-type )
(?&value)
}six;
next unless $meta =~ m{ $RX_SUBS
(?= content ) (?&name)
(?&equals)
(?<CONTENT> (?&value) )
}six;
next unless $+{CONTENT} =~ m{ $RX_SUBS
(?= charset ) (?&name)
(?&equals)
(?<CHARSET> (?&value) )
}six;
if (lc $encoding ne lc $+{CHARSET}) {
say "[HTTP-EQUIV ENCODING $encoding => $+{CHARSET}]";
$encoding = $+{CHARSET};
}
}
return decode($encoding, $_);
}
########################################################################
# Make sure to this function is called
# as soon as source unit has been compiled.
UNITCHECK { load_rxsubs() }
# useful regex subroutines for HTML parsing
sub load_rxsubs {
our $RX_SUBS = qr{
(?(DEFINE)
(?<WS> \s * )
(?<any_nv_pair> (?&name) (?&equals) (?&value) )
(?<name> \b (?= \pL ) [\w:\-] + \b )
(?<equals> (?&WS) = (?&WS) )
(?<value> (?"ed_value) | (?&unquoted_value) )
(?<unwhite_chunk> (?: (?! > ) \S ) + )
(?<unquoted_value> [\w:\-] * )
(?<any_quote> ["'] )
(?<quoted_value>
(?<quote> (?&any_quote) )
(?: (?! \k<quote> ) . ) *
\k<quote>
)
(?<start_tag> < (?&WS) )
(?<html_end_tag> > )
(?<xhtml_end_tag> / > )
(?<end_tag>
(?&WS)
(?: (?&html_end_tag)
| (?&xhtml_end_tag) )
)
(?<tag>
(?&start_tag)
(?&name)
(?:
(?&WS)
(?&any_nv_pair)
) *
(?&end_tag)
)
(?<untag> </ (?&name) > )
# starts like a tag, but has screwed up quotes inside it
(?<nasty>
(?&start_tag)
(?&name)
.*?
(?&end_tag)
)
(?<nontag> [^<] + )
(?<string> (?"ed_value) )
(?<word> (?&name) )
(?<doctype>
<!DOCTYPE
# please don't feed me nonHTML
### (?&WS) HTML
[^>]* >
)
(?<cdata> <!\[CDATA\[ .*? \]\] > )
(?<script> (?= <script ) (?&tag) .*? </script> )
(?<style> (?= <style ) (?&tag) .*? </style> )
(?<comment> <!-- .*? --> )
(?<xml>
< \? xml
(?:
(?&WS)
(?&any_nv_pair)
) *
(?&WS)
\? >
)
(?<xhook> < \? .*? \? > )
)
}six;
our $Meta_Tag_Rx = qr{ $RX_SUBS
(?<META>
(?&start_tag) meta \b
(?:
(?&WS) (?&any_nv_pair)
) +
(?&end_tag)
)
}six;
}
# nobody *ever* remembers to do this!
END { close STDOUT }