Ah, sim, você pode usar expressões regulares para analisar HTML!
Para a tarefa que você está tentando, as expressões regulares estão perfeitamente corretas!
Ele é verdade que a maioria das pessoas subestimam a dificuldade de análise de HTML com expressões regulares e, portanto, fazê-lo mal.
Mas essa não é uma falha fundamental relacionada à teoria computacional. Essa bobagem é muito repetida por aqui , mas você não acredita nelas.
Portanto, embora certamente possa ser feito (essa publicação serve como prova de existência desse fato incontestável), isso não significa que deva ser.
Você deve decidir por si próprio se está preparado para a tarefa de escrever o que equivale a um analisador HTML específico para fins especiais a partir de expressões regulares. A maioria das pessoas não é.
Mas eu sou ☻
Soluções gerais de análise de HTML baseadas em Regex
Primeiro, mostrarei como é fácil analisar HTML arbitrário com expressões regulares. O programa completo está no final desta postagem, mas o coração do analisador é:
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);
}
}
}
Veja como é fácil ler isso?
Como está escrito, ele identifica cada pedaço de HTML e informa onde ele o encontrou. Você pode modificá-lo facilmente para fazer o que quiser com qualquer tipo de peça ou para tipos mais específicos que estes.
Não tenho casos de teste com falha (à esquerda :): executei esse código com êxito em mais de 100.000 arquivos HTML - todos os que consegui com rapidez e facilidade. Além disso, eu também o executei em arquivos construídos especificamente para quebrar analisadores ingênuos.
Este não é um analisador ingênuo.
Ah, tenho certeza de que não é perfeito, mas ainda não consegui quebrá-lo. Eu acho que, mesmo que algo acontecesse, a correção seria fácil de se encaixar por causa da estrutura clara do programa. Mesmo programas pesados em regex devem ter estrutura.
Agora que isso está fora do caminho, deixe-me abordar a questão do OP.
Demonstração de solução de tarefas do OP usando expressões regulares
O pequeno html_input_rx
programa que incluo abaixo produz a seguinte saída, para que você possa ver que a análise de HTML com regexes funciona bem para o que você deseja fazer:
% 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"
Analisar tags de entrada, consulte Nenhuma entrada incorreta
Aqui está a fonte do programa que produziu a saída acima.
#!/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: $!";
}
Ai está! Nada disso! :)
Somente você pode julgar se sua habilidade com expressões regulares depende de uma tarefa de análise específica. O nível de habilidade de todos é diferente e cada nova tarefa é diferente. Para trabalhos em que você tem um conjunto de entradas bem definido, as expressões regulares são obviamente a escolha certa, porque é trivial reunir algumas quando você tem um subconjunto restrito de HTML para lidar. Mesmo iniciantes em regex devem lidar com esses trabalhos com regexes. Qualquer outra coisa é um exagero.
No entanto , uma vez que o HTML comece a ficar menos complicado, uma vez que começa a se ramificar de maneiras que você não pode prever, mas que são perfeitamente legais, uma vez que você tenha que combinar tipos diferentes de coisas ou dependências mais complexas, você chegará a um ponto em que você precisa trabalhar mais para efetuar uma solução que use expressões regulares do que para uma classe de análise. O ponto em que esse ponto de equilíbrio cai depende novamente do seu próprio nível de conforto com as expressões regulares.
Então, o que eu deveria fazer?
Não vou lhe dizer o que você deve fazer ou o que não pode fazer. Eu acho que está errado. Eu só quero lhe apresentar possibilidades, abra seus olhos um pouco. Você escolhe o que deseja fazer e como deseja fazê-lo. Não há absolutos - e ninguém mais conhece sua própria situação, assim como você. Se algo parece dar muito trabalho, bem, talvez seja. A programação deve ser divertida , você sabe. Se não estiver, você pode estar fazendo errado.
Pode-se olhar para o meu html_input_rx
programa de várias maneiras válidas. Uma delas é que você realmente pode analisar HTML com expressões regulares. Mas outra é que é muito, muito, muito mais difícil do que quase todo mundo pensa que é. Isso pode facilmente levar à conclusão de que meu programa é uma prova do que você não deve fazer, porque é realmente muito difícil.
Não vou discordar disso. Certamente, se tudo o que faço no meu programa não faz sentido para você depois de algum estudo, você não deve tentar usar expressões regulares para esse tipo de tarefa. Para HTML específico, as expressões regulares são ótimas, mas para HTML genérico, é o mesmo que loucura. Eu uso classes de análise o tempo todo, especialmente se for HTML que eu não tenha gerado.
Regexa ideal para pequenos problemas de análise de HTML, pessimal para grandes
Mesmo que meu programa seja tomado como ilustrativo do motivo pelo qual você não deve usar expressões regulares para analisar HTML geral - o que é bom, porque eu meio que pretendia que fosse isso -, ainda assim deveria ser algo que abre os olhos para que mais pessoas quebrem o padrão comum. e o hábito desagradável de escrever padrões ilegíveis, não estruturados e inatingíveis.
Os padrões não precisam ser feios e não precisam ser difíceis. Se você criar padrões feios, é um reflexo sobre você, não sobre eles.
Linguagem Regex fenomenalmente requintada
Me pediram para salientar que minha solução proferida para o seu problema foi escrita em Perl. Você está surpreso? Você não percebeu? Esta revelação é uma bomba?
É verdade que nem todas as outras ferramentas e linguagens de programação são tão convenientes, expressivas e poderosas quando se trata de expressões regulares quanto o Perl. Há um grande espectro por aí, com alguns sendo mais adequados que outros. Em geral, os idiomas que expressaram expressões regulares como parte do idioma principal e não como uma biblioteca são mais fáceis de trabalhar. Não fiz nada com expressões regulares que você não poderia fazer, por exemplo, no PCRE, embora você estruture o programa de maneira diferente se estiver usando C.
Eventualmente, outros idiomas serão informados sobre o local em que o Perl está agora em termos de expressões regulares. Digo isso porque, quando o Perl começou, ninguém mais tinha nada parecido com as expressões regulares do Perl. Diga o que quiser, mas foi aqui que Perl claramente venceu: todos copiaram as expressões regulares de Perl, embora em estágios variados de seu desenvolvimento. O Perl foi pioneiro em quase (não quase tudo, mas quase) tudo em que você confia nos padrões modernos de hoje, independentemente da ferramenta ou linguagem usada. Então, eventualmente, os outros vão alcançá-lo.
Mas eles só alcançam onde Perl estava em algum momento no passado, exatamente como está agora. Tudo avança. Em regexes, se nada mais, onde Perl leva, outros seguem. Onde estará o Perl depois que todo mundo finalmente alcançar onde está agora? Não faço ideia, mas sei que nós também teremos mudado. Provavelmente estaremos mais próximos do estilo de criação de padrões de Perl₆ .
Se você gosta desse tipo de coisa, mas gostaria de usá-lo em Perl₅, pode estar interessado no maravilhoso módulo Regexp :: Grammars de Damian Conway . É totalmente incrível e faz com que o que eu fiz aqui no meu programa pareça tão primitivo quanto o meu, que faz com que os padrões que as pessoas criam juntos sem espaço em branco ou identificadores alfabéticos. Confira!
Chunker HTML Simples
Aqui está a fonte completa do analisador de onde mostrei a peça central no início desta postagem.
Estou não sugerindo que você deve usar este sobre uma classe analisar rigorosamente testados. Mas estou cansado de pessoas que fingem que ninguém pode analisar HTML com expressões regulares apenas porque não podem. Você pode, claramente, e este programa é a prova dessa afirmação.
Claro, não é fácil, mas isso é possível!
E tentar fazer isso é uma terrível perda de tempo, porque existem boas classes de análise que você deve usar para esta tarefa. A resposta certa para as pessoas que tentam analisar HTML arbitrário não é que seja impossível. Essa é uma resposta fácil e falsa. A resposta correta e honesta é que eles não devem tentar, porque é um incômodo demais descobrir do zero; eles não devem quebrar as costas se esforçando para reativar uma roda que funcione perfeitamente bem.
Por outro lado, o HTML que se enquadra em um subconjunto previsível é extremamente fácil de analisar com expressões regulares. Não é de admirar que as pessoas tentem usá-las, porque, para pequenos problemas, talvez problemas com brinquedos, nada poderia ser mais fácil. É por isso que é tão importante distinguir as duas tarefas - específica e genérica -, pois elas não exigem necessariamente a mesma abordagem.
Espero no futuro aqui ver um tratamento mais justo e honesto das perguntas sobre HTML e expressões regulares.
Aqui está o meu lexer HTML. Ele não tenta fazer uma análise de validação; apenas identifica os elementos lexicais. Você pode pensar nisso mais como um chunker HTML do que como um analisador HTML. Ele não perdoa muito o HTML quebrado, embora faça algumas permissões muito pequenas nessa direção.
Mesmo se você nunca analisar o HTML completo (e por que deveria? É um problema resolvido!), Este programa possui muitos bits legais de regex que acredito que muitas pessoas podem aprender muito. Aproveitar!
#!/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 }