Oh oui, vous pouvez utiliser des expressions régulières pour analyser le HTML!
Pour la tâche que vous tentez, les expressions régulières sont parfaitement bien!
Il est vrai que la plupart des gens sous-estiment la difficulté d'analyser le HTML avec des expressions régulières et le font donc mal.
Mais ce n'est pas un défaut fondamental lié à la théorie computationnelle. Cette sottise est souvent évoquée ici , mais ne les croyez pas.
Donc, même si cela peut certainement être fait (cette publication sert de preuve d'existence de ce fait incontestable), cela ne signifie pas que cela devrait l' être.
Vous devez décider par vous-même si vous êtes à la hauteur de la tâche d'écrire ce qui équivaut à un analyseur HTML dédié et spécial à partir des expressions rationnelles. La plupart des gens ne le sont pas.
Mais je le suis. ☻
Solutions d'analyse HTML générales basées sur les expressions régulières
Je vais d'abord montrer à quel point il est facile d'analyser du HTML arbitraire avec des expressions rationnelles. Le programme complet est à la fin de cette publication, mais le cœur de l'analyseur est:
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);
}
}
}
Vous voyez à quel point c'est facile à lire?
Tel qu'il est écrit, il identifie chaque morceau de HTML et indique où il l'a trouvé. Vous pouvez facilement le modifier pour faire tout ce que vous voulez avec n'importe quel type de pièce donné, ou pour des types plus particuliers que ceux-ci.
Je n'ai aucun cas de test défaillant (à gauche :): j'ai exécuté avec succès ce code sur plus de 100 000 fichiers HTML - chacun d'entre eux que je pourrais rapidement et facilement mettre la main sur. Au-delà de ceux-ci, je l'ai également exécuté sur des fichiers spécialement conçus pour briser les analyseurs naïfs.
Ce n'est pas un analyseur naïf.
Oh, je suis sûr que ce n'est pas parfait, mais je n'ai pas encore réussi à le casser. Je pense que même si quelque chose se produisait, le correctif serait facile à intégrer en raison de la structure claire du programme. Même les programmes contenant beaucoup de regex devraient avoir une structure.
Maintenant que ce n'est plus le cas, permettez-moi de répondre à la question du PO.
Démo de la résolution de la tâche du PO à l'aide des expressions rationnelles
Le petit html_input_rx
programme que j'inclus ci-dessous produit la sortie suivante, afin que vous puissiez voir que l'analyse HTML avec des expressions régulières fonctionne parfaitement pour ce que vous souhaitez faire:
% 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"
Analyser les balises d'entrée, voir aucune entrée mauvaise
Voici la source du programme qui a produit la sortie ci-dessus.
#!/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: $!";
}
Voilà! Rien pour le faire! :)
Vous seul pouvez juger si votre compétence avec les regex est à la hauteur d'une tâche d'analyse particulière. Le niveau de compétence de chacun est différent et chaque nouvelle tâche est différente. Pour les travaux où vous avez un ensemble d'entrées bien défini, les expressions régulières sont évidemment le bon choix, car il est trivial d'en rassembler lorsque vous avez un sous-ensemble restreint de HTML à gérer. Même les débutants en regex devraient gérer ces tâches avec des regex. Tout le reste est exagéré.
Cependant , une fois que le HTML commence à devenir moins cloué, une fois qu'il commence à se ramifier d'une manière que vous ne pouvez pas prédire mais qui est parfaitement légale, une fois que vous devez faire correspondre des sortes de choses plus différentes ou avec des dépendances plus complexes, vous finirez par atteindre un point où vous devez travailler plus dur pour effectuer une solution qui utilise des expressions régulières que vous auriez à utiliser une classe d'analyse. L'endroit où ce seuil de rentabilité tombe dépend encore une fois de votre propre niveau de confort avec les expressions régulières.
Donc qu'est ce que je devrais faire?
Je ne vais pas vous dire ce que vous devez faire ou ce que vous ne pouvez pas faire. Je pense que c'est faux. Je veux juste vous présenter des possibilités, ouvrez un peu les yeux. Vous pouvez choisir ce que vous voulez faire et comment vous voulez le faire. Il n'y a pas d'absolus - et personne d'autre ne connaît votre propre situation aussi bien que vous-même. Si quelque chose semble être trop de travail, eh bien, c'est peut-être le cas. La programmation doit être amusante , vous savez. Si ce n'est pas le cas, vous le faites peut-être mal.
On peut regarder mon html_input_rx
programme de plusieurs manières valables. L'une d'elles est que vous pouvez en effet analyser le HTML avec des expressions régulières. Mais une autre est que c'est beaucoup, beaucoup, beaucoup plus difficile que presque tout le monde ne le pense. Cela peut facilement conduire à la conclusion que mon programme témoigne de ce que vous ne devriez pas faire, car c'est vraiment trop difficile.
Je ne suis pas en désaccord avec cela. Certes, si tout ce que je fais dans mon programme n'a pas de sens pour vous après quelques études, alors vous ne devriez pas essayer d'utiliser des regex pour ce genre de tâche. Pour du HTML spécifique, les expressions régulières sont excellentes, mais pour du HTML générique, elles équivalent à de la folie. J'utilise des classes d'analyse tout le temps, surtout si c'est du HTML que je n'ai pas généré moi-même.
Les expressions régulières optimales pour les petits problèmes d'analyse HTML, pessimales pour les gros problèmes
Même si mon programme est considéré comme une illustration de la raison pour laquelle vous ne devriez pas utiliser les expressions régulières pour analyser le HTML général - ce qui est OK, parce que je voulais en quelque sorte que ce soit cela ☺ - cela devrait quand même être une révélation afin que plus de gens cassent le terriblement commun et la mauvaise habitude d'écrire des motifs illisibles, non structurés et non maintenables.
Les motifs ne doivent pas être laids et ils ne doivent pas nécessairement être durs. Si vous créez des motifs laids, c'est une réflexion sur vous, pas sur eux.
Langage Regex phénoménalement exquis
On m'a demandé de signaler que ma solution proposée à votre problème a été écrite en Perl. Êtes-vous surpris? N'avez-vous pas remarqué? Cette révélation est-elle une bombe?
Il est vrai que tous les autres outils et langages de programmation ne sont pas aussi pratiques, expressifs et puissants en matière d'expressions rationnelles que Perl. Il existe un large spectre, certains étant plus adaptés que d'autres. En général, les langages qui ont exprimé des expressions rationnelles comme faisant partie du langage de base plutôt que comme une bibliothèque sont plus faciles à utiliser. Je n'ai rien fait avec les expressions régulières que vous ne pourriez pas faire dans, par exemple, PCRE, même si vous structureriez le programme différemment si vous utilisiez C.
Finalement, d'autres langages seront rattrapés par la situation actuelle de Perl en termes de regex. Je dis cela parce qu'à l'époque où Perl a commencé, personne d'autre n'avait rien de tel que les expressions régulières de Perl. Dites ce que vous voulez, mais c'est là que Perl a clairement gagné: tout le monde a copié les expressions rationnelles de Perl, bien qu'à des stades variables de leur développement. Perl a été le pionnier de presque (pas tout à fait, mais presque) tout ce sur quoi vous vous êtes habitué dans les modèles modernes, quel que soit l'outil ou le langage que vous utilisez. Donc , finalement les autres vont rattraper leur retard.
Mais ils ne rattraperont que la position de Perl dans le passé, comme c'est le cas maintenant. Tout avance. Dans les expressions régulières, si rien d'autre, là où Perl mène, d'autres suivent. Où sera Perl une fois que tout le monde aura enfin compris où Perl est maintenant? Je n'en ai aucune idée, mais je sais que nous aussi nous aurons déménagé. Nous serons probablement plus proches du style de création de motifs de Perl₆ .
Si vous aimez ce genre de chose mais que vous souhaitez l'utiliser en Perl₅, vous pourriez être intéressé par le merveilleux module Regexp :: Grammars de Damian Conway . C'est complètement génial, et ce que j'ai fait ici dans mon programme semble tout aussi primitif que le mien, ce qui fait que les gens s'entassent sans espaces ni identificateurs alphabétiques. Vérifiez-le!
Chunker HTML simple
Voici la source complète de l'analyseur dont j'ai montré la pièce maîtresse au début de cette publication.
Je ne suggère pas que vous devriez utiliser ceci sur une classe d'analyse rigoureusement testée. Mais je suis fatigué des gens qui prétendent que personne ne peut analyser du HTML avec des expressions régulières simplement parce qu'ils ne le peuvent pas. Vous pouvez clairement, et ce programme est la preuve de cette affirmation.
Bien sûr, il est pas facile, mais il est possible!
Et essayer de le faire est une terrible perte de temps, car il existe de bonnes classes d'analyse que vous devriez utiliser pour cette tâche. La bonne réponse aux personnes qui essaient d'analyser du HTML arbitraire n'est pas que ce soit impossible. C'est une réponse facile et malhonnête. La réponse correcte et honnête est qu'ils ne devraient pas essayer parce que c'est trop ennuyeux de partir de zéro; ils ne doivent pas se casser le dos en cherchant à réinventer une roue qui fonctionne parfaitement.
D'un autre côté, le HTML qui appartient à un sous - ensemble prévisible est ultra-facile à analyser avec les expressions rationnelles. Il n'est pas étonnant que les gens essaient de les utiliser, car pour de petits problèmes, des problèmes de jouets peut-être, rien de plus facile. C'est pourquoi il est si important de distinguer les deux tâches - spécifiques et génériques - car elles ne nécessitent pas nécessairement la même approche.
J'espère à l'avenir voir ici un traitement plus juste et plus honnête des questions sur le HTML et les expressions régulières.
Voici mon lexer HTML. Il n'essaye pas de faire une analyse de validation; il identifie simplement les éléments lexicaux. Vous pourriez le considérer plus comme un bloc HTML que comme un analyseur HTML. Il ne pardonne pas très bien le HTML cassé, bien que cela fasse de très petites allocations dans cette direction.
Même si vous n'analysez jamais le code HTML complet vous-même (et pourquoi devriez-vous? C'est un problème résolu!), Ce programme a beaucoup de bits regex sympas dont je pense que beaucoup de gens peuvent apprendre beaucoup. Prendre plaisir!
#!/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 }