Oh sì, puoi usare Regexes per analizzare HTML!
Per l'attività che stai tentando, le regex vanno benissimo!
Si è vero che la maggior parte delle persone sottovaluta la difficoltà di parsing del codice HTML con le espressioni regolari, e quindi lo fanno male.
Ma questo non è un difetto fondamentale legato alla teoria computazionale. Quella stupidità è molto pappagallo da queste parti , ma non ci credi.
Quindi, anche se certamente può essere fatto (questo post serve come prova dell'esistenza di questo fatto incontrovertibile), ciò non significa che dovrebbe essere.
Devi decidere tu stesso se sei pronto a scrivere ciò che equivale a un parser HTML dedicato e speciale da regex. Molte persone non lo sono.
Ma lo sono. ☻
Soluzioni generali di analisi HTML basate su Regex
Per prima cosa mostrerò quanto è facile analizzare arbitrariamente HTML con regex. Il programma completo è alla fine di questo post, ma il cuore del parser è:
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);
}
}
}
Vedi come facile da leggere?
Come scritto, identifica ogni pezzo di HTML e dice dove ha trovato quel pezzo. Puoi facilmente modificarlo per fare qualsiasi altra cosa tu voglia con un dato tipo di pezzo, o per tipi più particolari di questi.
Non ho casi di test non riusciti (a sinistra :): ho eseguito con successo questo codice su oltre 100.000 file HTML, ognuno dei quali ho potuto mettere le mani rapidamente e facilmente. Oltre a questi, l'ho anche eseguito su file appositamente creati per rompere parser ingenui.
Questo non lo è un parser ingenuo.
Oh, sono sicuro che non sia perfetto, ma non sono ancora riuscito a romperlo. Immagino che anche se qualcosa lo facesse, la correzione sarebbe facile da inserire a causa della struttura chiara del programma. Anche i programmi pesanti di regex dovrebbero avere una struttura.
Ora che è fuori mano, lasciami rispondere alla domanda del PO.
Dimostrazione di risolvere il compito del PO utilizzando Regexes
Il piccolo html_input_rx
programma che includo di seguito produce il seguente output, in modo da poter vedere che l'analisi dell'HTML con regex funziona perfettamente per quello che desideri fare:
% 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"
Tag di input di analisi, vedere Nessun input male
Ecco la fonte per il programma che ha prodotto l'output sopra.
#!/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: $!";
}
Ecco qua! Niente da fare! :)
Solo tu puoi giudicare se la tua abilità con le regex dipende da un particolare compito di analisi. Il livello di abilità di ognuno è diverso e ogni nuova attività è diversa. Per i lavori in cui hai un set di input ben definito, le regex sono ovviamente la scelta giusta, perché è banale metterne insieme alcuni quando hai un sottoinsieme limitato di HTML da affrontare. Anche i principianti di regex dovrebbero essere in grado di gestire quei lavori con regex. Qualcos'altro è eccessivo.
Tuttavia , una volta che l'HTML inizia a essere meno inchiodato, una volta che inizia a ramificarsi in modi che non puoi prevedere ma che sono perfettamente legali, una volta che devi abbinare più tipi diversi di cose o con dipendenze più complesse, alla fine raggiungerai un punto in cui devi lavorare di più per effettuare una soluzione che utilizza regex di quanto non dovresti usare una classe di analisi. Dove cade quel punto di pareggio dipende di nuovo dal tuo livello di comfort con le regex.
Quindi cosa dovrei fare?
Non ti dirò cosa devi fare o cosa non puoi fare. Penso che sia sbagliato. Voglio solo presentarti delle possibilità, aprire un po 'gli occhi. Puoi scegliere cosa vuoi fare e come vuoi farlo. Non ci sono assoluti - e nessun altro conosce la tua situazione come tu stesso. Se qualcosa sembra troppo lavoro, beh, forse lo è. La programmazione dovrebbe essere divertente , lo sai. Altrimenti, potresti sbagliare.
Uno può guardare il mio html_input_rx
programma in qualsiasi numero di modi validi. Uno di questi è che, invece, si può analizzare HTML con le espressioni regolari. Ma un altro è che è molto, molto, molto più difficile di quanto quasi nessuno pensi mai. Questo può facilmente portare alla conclusione che il mio programma è una testimonianza di ciò che non dovresti fare, perché è davvero troppo difficile.
Non sarò d'accordo con quello. Certamente se tutto ciò che faccio nel mio programma non ha senso per te dopo qualche studio, non dovresti tentare di usare regex per questo tipo di attività. Per HTML specifico, le regex sono fantastiche, ma per HTML generico, equivalgono alla follia. Uso sempre le classi di analisi, specialmente se è HTML non mi sono generato.
Regexes ottimale per piccoli problemi di analisi HTML, pessimale per quelli di grandi dimensioni
Anche se il mio programma è presa come esempio del perché si dovrebbe non usare espressioni regolari per l'analisi generale HTML - che è OK, perché ho un pò intendevo per essere che ☺ - è ancora dovrebbe essere una rivelazione così che più persone rompere il terribilmente comuni e cattiva, cattiva abitudine di scrivere schemi illeggibili, non strutturati e non mantenibili.
I modelli non devono essere brutti e non devono essere duri. Se crei brutti schemi, è una riflessione su di te, non su di loro.
Fenomenale linguaggio regex squisito
Mi è stato chiesto di sottolineare che la mia proficua soluzione al tuo problema è stata scritta in Perl. Sei sorpreso? Non l'hai notato? Questa rivelazione è una bomba?
È vero che non tutti gli altri strumenti e linguaggi di programmazione sono altrettanto convenienti, espressivi e potenti quando si tratta di regex come Perl. C'è un grande spettro là fuori, con alcuni più adatti di altri. In generale, le lingue che hanno espresso regex come parte del linguaggio principale anziché come libreria sono più facili da lavorare. Non ho fatto nulla con regex in cui non potevi fare, diciamo, PCRE, anche se struttureresti il programma in modo diverso se stessi usando C.
Alla fine, altre lingue saranno al passo con il momento in cui Perl si trova in termini di regex. Dico questo perché quando Perl ha iniziato, nessun altro aveva qualcosa di simile alle regex di Perl. Di 'tutto quello che ti piace, ma è qui che Perl ha vinto chiaramente: tutti hanno copiato le regex di Perl, sebbene in varie fasi del loro sviluppo. Perl è stato il pioniere di quasi (non del tutto, ma quasi) tutto ciò su cui sei arrivato a fare affidamento oggi nei modelli moderni, indipendentemente dallo strumento o dalla lingua che usi. Quindi alla fine gli altri li raggiungeranno.
Ma raggiungeranno solo dove Perl era in passato, proprio come è ora. Tutto avanza. Nelle regex se non altro, dove conduce Perl, altri seguono. Dove sarà Perl una volta che tutti gli altri finalmente raggiungeranno il punto in cui si trova Perl adesso? Non ne ho idea, ma so che anche noi ci saremo trasferiti. Probabilmente saremo più vicini allo stile di creazione di modelli di Perl .
Se ti piace quel genere di cose ma ti piacerebbe usarlo in Perl₅, potresti essere interessato al meraviglioso modulo Regexp :: Grammars di Damian Conway . È completamente fantastico e fa sembrare quello che ho fatto qui nel mio programma tanto primitivo quanto il mio, rendendo i modelli che le persone si raggruppano senza spazi bianchi o identificatori alfabetici. Controlla!
Chunker HTML semplice
Ecco la fonte completa del parser da cui ho mostrato il centrotavola all'inizio di questo post.
Io non suggerendo che si dovrebbe usare questo su una classe di analisi rigorosamente testati. Ma sono stanco delle persone che fingono che nessuno possa analizzare l'HTML con regex solo perché non possono. È chiaramente possibile e questo programma è la prova di tale affermazione.
Certo, non è facile, ma si è possibile!
E provare a farlo è una terribile perdita di tempo, perché esistono buone classi di analisi che dovresti usare per questo compito. La risposta giusta alle persone che cercano di analizzare HTML arbitrario non è impossibile. Questa è una risposta facile e disingenua. La risposta corretta e onesta è che non dovrebbero tentare perché è troppo fastidioso capire da zero; non dovrebbero spezzarsi la schiena cercando di reinventare una ruota che funziona perfettamente bene.
D'altra parte, l'HTML che rientra in un sottoinsieme prevedibile è estremamente facile da analizzare con regex. Non sorprende che le persone provino ad usarli, perché per piccoli problemi, forse per i giocattoli, niente potrebbe essere più facile. Ecco perché è così importante distinguere i due compiti - specifico e generico - in quanto questi non richiedono necessariamente lo stesso approccio.
Spero in futuro qui di vedere un trattamento più equo e onesto delle domande su HTML e regex.
Ecco il mio lexer HTML. Non tenta di eseguire un'analisi di convalida; identifica solo gli elementi lessicali. Potresti pensarlo più come un grosso pezzo di HTML che un parser HTML. Non perdona molto l'HTML non funzionante, anche se fa delle ridottissime riduzioni in quella direzione.
Anche se non analizzi mai l'HTML completo da solo (e perché dovresti? È un problema risolto!), Questo programma ha molti bit regex interessanti che credo che molte persone possano imparare molto. Godere!
#!/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 }