¡Oh, sí, puedes usar expresiones regulares para analizar HTML!
Para la tarea que está intentando, ¡las expresiones regulares están perfectamente bien!
Que es cierto que la mayoría de la gente subestima la dificultad de análisis de HTML con expresiones regulares y por lo tanto hacen tan mal.
Pero este no es un defecto fundamental relacionado con la teoría computacional. Esa estupidez se repite mucho por aquí , pero no les creas.
Entonces, aunque ciertamente se puede hacer (esta publicación sirve como una prueba de existencia de este hecho incontrovertible), eso no significa que deba serlo.
Debes decidir por ti mismo si estás a la altura de la tarea de escribir lo que equivale a un analizador HTML dedicado y de propósito especial a partir de expresiones regulares. La mayoría de las personas no lo son.
Pero yo si . ☻
Soluciones generales de análisis HTML basadas en expresiones regulares
Primero, mostraré lo fácil que es analizar HTML arbitrario con expresiones regulares. El programa completo está al final de esta publicación, pero el corazón del analizador es:
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);
}
}
}
¿Ves lo fácil que es leer?
Tal como está escrito, identifica cada pieza de HTML y le dice dónde encontró esa pieza. Puede modificarlo fácilmente para hacer lo que quiera con cualquier tipo de pieza, o para tipos más particulares que estos.
No tengo fallas en los casos de prueba (izquierda :): he ejecutado con éxito este código en más de 100,000 archivos HTML, cada uno de los cuales pude tener en mis manos rápida y fácilmente. Más allá de eso, también lo ejecuté en archivos construidos específicamente para romper analizadores ingenuos.
Este no es un analizador ingenuo.
Oh, estoy seguro de que no es perfecto, pero aún no he logrado romperlo. Me imagino que incluso si algo sucediera, la solución sería fácil de encajar debido a la estructura clara del programa. Incluso los programas con expresiones regulares deberían tener estructura.
Ahora que está fuera del camino, permítanme abordar la pregunta del OP.
Demostración de cómo resolver la tarea del OP utilizando expresiones regulares
El pequeño html_input_rx
programa que incluyo a continuación produce el siguiente resultado, para que pueda ver que analizar HTML con expresiones regulares funciona bien para lo que desea hacer:
% 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"
Analizar etiquetas de entrada, ver No hay entrada malvada
Aquí está la fuente del programa que produjo el resultado anterior.
#!/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: $!";
}
Ahí tienes! Nada de eso! :)
Solo usted puede juzgar si su habilidad con expresiones regulares depende de cualquier tarea de análisis particular. El nivel de habilidad de cada persona es diferente, y cada nueva tarea es diferente. Para los trabajos en los que tiene un conjunto de entrada bien definido, las expresiones regulares son obviamente la opción correcta, porque es trivial juntarlas cuando tiene un subconjunto restringido de HTML con el que lidiar. Incluso los principiantes de expresiones regulares deben manejar esos trabajos con expresiones regulares. Cualquier otra cosa es exagerada.
Sin embargo , una vez que el HTML comienza a ser menos claro, una vez que comienza a ramificarse de maneras que no puede predecir pero que son perfectamente legales, una vez que tiene que hacer coincidir más tipos diferentes de cosas o con dependencias más complejas, eventualmente llegará a un punto donde tiene que trabajar más para lograr una solución que use expresiones regulares de lo que tendría que usar una clase de análisis. Donde cae ese punto de equilibrio depende nuevamente de su propio nivel de comodidad con expresiones regulares.
¿Entonces qué debo hacer?
No voy a decirte lo que debes hacer o lo que no puedes hacer. Creo que eso está mal. Solo quiero presentarte posibilidades, abre los ojos un poco. Puedes elegir lo que quieres hacer y cómo quieres hacerlo. No hay absolutos, y nadie más conoce tu propia situación tan bien como tú. Si algo parece que es demasiado trabajo, bueno, tal vez lo sea. La programación debería ser divertida , ya sabes. Si no es así, puede estar haciéndolo mal.
Uno puede mirar mi html_input_rx
programa de muchas maneras válidas. Uno de ellos es que de hecho puede analizar HTML con expresiones regulares. Pero otra es que es mucho, mucho, mucho más difícil de lo que casi todos piensan. Esto puede llevar fácilmente a la conclusión de que mi programa es un testimonio de lo que no debe hacer, porque realmente es demasiado difícil.
No estaré en desacuerdo con eso. Ciertamente, si todo lo que hago en mi programa no tiene sentido para usted después de algún estudio, entonces no debería intentar usar expresiones regulares para este tipo de tarea. Para HTML específico, las expresiones regulares son geniales, pero para HTML genérico, equivalen a locura. Uso clases de análisis todo el tiempo, especialmente si es HTML que no he generado yo mismo.
Regexes óptimos para problemas de análisis de HTML pequeños , pesimales para problemas grandes
Incluso si mi programa se toma como ilustrativo de por qué usted debe no utiliza expresiones regulares para analizar general de HTML - lo cual está bien, porque un poco decir para que sea de esa ☺ - que todavía debe ser una revelación para que más gente a romper el terriblemente común y desagradable, desagradable hábito de escribir patrones ilegibles, no estructurados e imposibles de mantener.
Los patrones no tienen que ser feos, y no tienen que ser difíciles. Si creas patrones feos, es un reflejo en ti, no en ellos.
Lenguaje fenomenalmente exquisito de expresiones regulares
Me han pedido que señale que mi solución proferida a su problema ha sido escrita en Perl. ¿Estás sorprendido? ¿No te diste cuenta? ¿Es esta revelación una bomba?
Es cierto que no todas las otras herramientas y lenguajes de programación son tan convenientes, expresivos y poderosos cuando se trata de expresiones regulares como Perl. Hay un gran espectro, algunos son más adecuados que otros. En general, es más fácil trabajar con los idiomas que han expresado expresiones regulares como parte del lenguaje central en lugar de como una biblioteca. No he hecho nada con expresiones regulares que no pudieras hacer, por ejemplo, PCRE, aunque estructurarías el programa de manera diferente si usaras C.
Eventualmente, otros idiomas se pondrán al día con Perl en términos de expresiones regulares. Digo esto porque cuando comenzó Perl, nadie más tenía nada como las expresiones regulares de Perl. Di lo que quieras, pero aquí es donde Perl claramente ganó: todos copiaron las expresiones regulares de Perl, aunque en diferentes etapas de su desarrollo. Perl fue pionero en casi (no del todo, pero casi) todo lo que usted ha llegado a confiar en los patrones modernos de hoy, sin importar qué herramienta o lenguaje use. Entonces, eventualmente los demás se pondrán al día.
Pero solo se pondrán al día con Perl en el pasado, tal como es ahora. Todo avanza. En expresiones regulares, si nada más, donde Perl conduce, otros lo siguen. ¿Dónde estará Perl una vez que todos los demás finalmente se pongan al día donde está Perl ahora? No tengo idea, pero sé que nosotros también nos habremos mudado. Probablemente estaremos más cerca del estilo de patrones de elaboración de Perl₆ .
Si te gusta ese tipo de cosas pero te gustaría usarlo en Perl₅, quizás te interese el maravilloso módulo Regexp :: Grammars de Damian Conway . Es completamente increíble, y hace que lo que he hecho aquí en mi programa parezca tan primitivo como el mío hace que los patrones que las personas agrupan sin espacios en blanco o identificadores alfabéticos. ¡Echale un vistazo!
HTML simple Chunker
Aquí está la fuente completa del analizador desde el que mostré la pieza central al comienzo de esta publicación.
Estoy no sugiriendo que usted debe utilizar esta clase de análisis a través de una rigurosa prueba. Pero estoy cansado de que la gente finja que nadie puede analizar HTML con expresiones regulares solo porque no pueden. Claramente puede, y este programa es prueba de esa afirmación.
Está claro que no es fácil, pero que es posible!
Y tratar de hacerlo es una pérdida de tiempo terrible, porque existen buenas clases de análisis que debe utilizar para esta tarea. La respuesta correcta para las personas que intentan analizar HTML arbitrario no es que sea imposible. Esa es una respuesta fácil y falsa. La respuesta correcta y honesta es que no deberían intentarlo porque es demasiado molesto descubrirlo desde cero; no deben romperse la espalda tratando de reinventar una rueda que funcione perfectamente bien.
Por otro lado, el HTML que se encuentra dentro de un subconjunto predecible es muy fácil de analizar con expresiones regulares. No es de extrañar que la gente intente usarlos, porque para problemas pequeños, problemas con los juguetes, tal vez, nada podría ser más fácil. Es por eso que es tan importante distinguir las dos tareas, específicas versus genéricas, ya que estas no requieren necesariamente el mismo enfoque.
Espero en el futuro ver un tratamiento más justo y honesto de las preguntas sobre HTML y expresiones regulares.
Aquí está mi lexer HTML. No intenta hacer un análisis de validación; solo identifica los elementos léxicos. Puede pensarlo más como un fragmentador de HTML que como un analizador de HTML. No es muy indulgente con HTML roto, aunque hace algunas concesiones muy pequeñas en esa dirección.
Incluso si nunca analiza HTML completo usted mismo (¿y por qué debería hacerlo? ¡Es un problema resuelto!), Este programa tiene muchos bits de expresiones regulares geniales de los que creo que mucha gente puede aprender mucho. ¡Disfrutar!
#!/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 }