Ответ 2
О да, вы можете использовать регулярные выражения для разбора HTML!
Для задачи, которую вы пытаетесь, регулярные выражения прекрасно подходят!
Это правда, что большинство людей недооценивают сложность анализа HTML с помощью регулярных выражений и, следовательно, делают это плохо.
Но это не какой-то фундаментальный недостаток, связанный с вычислительной теорией. Эта глупость много попугаев здесь, но вы не верите им.
Таким образом, хотя это, безусловно, можно сделать (эта публикация служит доказательством существования этого неопровержимого факта), это не означает, что это должно быть.
Вы должны решить для себя, подходит ли вам задача написания того, что составляет выделенный специализированный HTML-анализатор из регулярных выражений. Большинство людей нет.
Но я. ☻
Общие решения для парсинга HTML на основе регулярных выражений
Сначала я покажу, как легко разобрать произвольный HTML с регулярными выражениями. Полные программы в конце этой публикации, но сердце синтаксического анализатора:
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);
}
}
}
Видишь, как легко это читать?
Как написано, он идентифицирует каждый фрагмент HTML и сообщает, где он нашел этот фрагмент. Вы можете легко изменить его, чтобы сделать что-либо еще с любым типом элемента или для более конкретных типов, чем эти.
У меня нет неудачных тестов (слева :): я успешно запустил этот код на более чем 100 000 HTML файлов - каждый из которых я мог быстро и легко достать. Помимо этого, я также запускаю его на файлах, специально созданных для того, чтобы сломать наивные парсеры.
Это не наивный парсер.
О, я уверен, что это не идеально, но мне еще не удалось сломать это. Я полагаю, что даже если бы что-то и было, исправить это было бы легко из-за четкой структуры программ. Даже регулярные программы должны иметь структуру.
Теперь это не так, позвольте мне ответить на вопрос ОП.
Демонстрация решения задачи OP с помощью регулярных выражений
Маленькая программа html_input_rx
, которую я включил ниже, производит следующий вывод, так что вы можете видеть, что анализ HTML с регулярными выражениями прекрасно работает для того, что вы хотите сделать:
% 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"
Разбор тегов ввода, не вижу злого ввода
Вот источник для программы, которая произвела вывод выше.
#!/usr/bin/env perl
#
# html_input_rx - pull out all <input> tags from (X)HTML src
# via simple regex processing
#
# Tom Christiansen <[email protected]>
# 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 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: $!";
}
Вот, пожалуйста! Ничего подобного! :)
Только вы можете судить, соответствует ли ваш навык регулярным выражениям какой-либо конкретной задаче анализа. Каждый уровень мастерства отличается, и каждое новое задание отличается. Для заданий, где у вас есть четко определенный входной набор, регулярные выражения, очевидно, являются правильным выбором, потому что их легко собрать, когда у вас есть ограниченное подмножество HTML для работы. Даже начинающие регулярные выражения должны справляться с этими заданиями с помощью регулярных выражений. Все остальное излишне.
Однако, как только HTML начинает становиться все менее понятным, как только он начинает разветвляться способами, которые вы не можете предсказать, но которые совершенно законны, как только вам придется сопоставлять более разные вещи или с более сложными зависимостями, вы в конечном итоге достигнете точка, в которой вам придется работать усерднее, чтобы создать решение, использующее регулярные выражения, чем при использовании класса синтаксического анализа. То, где эта точка безубыточности падает, снова зависит от вашего собственного уровня комфорта с регулярными выражениями.
Так что мне делать?
Я не собираюсь рассказывать вам, что вы должны сделать или что вы не можете сделать. Я думаю, что это неправильно. Я просто хочу представить вам возможности, немного откройте глаза. Вы можете выбрать, что вы хотите сделать и как вы хотите это сделать. Абсолютов нет - и никто другой не знает вашей ситуации так же хорошо, как вы сами. Если что-то кажется слишком большой работой, ну, может быть, это так. Знаете, программирование должно быть увлекательным. Если это не так, возможно, вы делаете это неправильно.
Можно посмотреть на мою программу html_input_rx
любым количеством подходящих способов. Одним из них является то, что вы действительно можете анализировать HTML с помощью регулярных выражений. Но другое заключается в том, что это намного, намного, намного сложнее, чем кто-либо когда-либо думал. Это может легко привести к выводу, что моя программа является свидетельством того, что вы не должны делать, потому что это действительно слишком сложно.
Я не буду с этим не согласен. Конечно, если все, что я делаю в своей программе, не имеет смысла для вас после некоторого изучения, то вам не следует пытаться использовать регулярные выражения для такого рода задач. Для конкретного HTML регулярные выражения хороши, но для общего HTML они равносильны безумию. Я все время использую классы парсинга, особенно если его HTML я не генерировал сам.
Оптимальные регулярные выражения для небольших задач разбора HTML, а для больших - пессимальные
Даже если моя программа воспринимается как иллюстрация того, почему вы не должны использовать регулярные выражения для анализа общего HTML-кода - это нормально, потому что я вроде как хотел, чтобы это было ☺ - это все равно должно быть откровением, так все больше людей ломают ужасно распространенную и неприятную привычку писать нечитаемые, неструктурированные и не поддерживаемые шаблоны.
Шаблоны не должны быть безобразными, и они не должны быть жесткими. Если вы создаете уродливые узоры, это отражение вас, а не их.
Феноменально изысканный язык регулярных выражений
Меня попросили указать, что мое решение вашей проблемы было написано на Perl. Вы удивлены? Вы не заметили? Является ли это откровение бомбой?
Это правда, что не все другие инструменты и языки программирования столь же удобны, выразительны и мощны, когда речь идет о регулярных выражениях, как Perl. Существует большой спектр, причем некоторые из них являются более подходящими, чем другие. В целом, с языками, в которых регулярные выражения выражены как часть основного языка, а не как библиотека, легче работать. Я ничего не сделал с регулярными выражениями, которые вы не могли бы сделать, скажем, в PCRE, хотя вы бы по-другому структурировали программу, если бы использовали C.
В конце концов, другие языки будут догонять то, где сейчас находится Perl с точки зрения регулярных выражений. Я говорю это потому, что когда Perl начинал, никто не имел ничего подобного регулярным выражениям Perls. Скажите что угодно, но именно здесь Perl явно выиграл: все копировали Perls регулярные выражения, хотя и на разных этапах своего развития. Perl впервые применил почти (не совсем все, но почти) все, на что вы сегодня полагаетесь в современных моделях, независимо от того, какой инструмент или язык вы используете. Так что в конце концов остальные догонят.
Но они только догонят, где Perl был когда-то в прошлом, так же, как сейчас. Все продвигается. В регулярных выражениях, если ничего другого, куда ведет Perl, следуют другие. Где будет Perl, когда все наконец поймут, где сейчас находится Perl? Понятия не имею, но знаю, что мы тоже переедем. Вероятно, лучше быть ближе к стилю создания Perl.
Если вам нравятся такие вещи, но вы хотели бы использовать их в Perl₅, вас может заинтересовать Damian Conways замечательный Regexp :: Grammars. Это совершенно потрясающе и делает то, что я сделал здесь в моей программе, таким же примитивным, как и мое, делает шаблоны, которые люди собирают вместе, без пробелов и буквенных идентификаторов. Проверьте это!
Простой HTML Chunker
Вот полный исходный код парсера, с которого я показал центральную часть в начале этой публикации.
Я не предлагаю, чтобы вы использовали это в строго проверенном классе разбора. Но я устал от людей, притворяющихся, что никто не может анализировать HTML с регулярными выражениями только потому, что они не могут. Вы можете ясно, и эта программа является доказательством этого утверждения.
Конечно, это нелегко, но это возможно!
И попытка сделать это - ужасная трата времени, потому что существуют хорошие классы разбора, которые вы должны использовать для этой задачи. Правильный ответ для людей, пытающихся разобрать произвольный HTML, не, что это невозможно. Это простой и неискренний ответ. Правильный и честный ответ заключается в том, что они не должны пытаться сделать это, потому что это слишком сложно для выяснения с нуля; они не должны ломать спину, стремясь заново изобрести колесо, которое отлично работает.
С другой стороны, HTML, который находится в пределах предсказуемого подмножества, очень легко анализировать с помощью регулярных выражений. Неудивительно, что люди пытаются использовать их, потому что для небольших проблем, игрушечных проблем, возможно, нет ничего проще. Вот почему так важно различать две задачи - специфическую и общую - поскольку они не обязательно требуют одинакового подхода.
Я надеюсь, что в будущем мы увидим более справедливое и честное рассмотрение вопросов о HTML и регулярных выражениях.
Вот мой HTML-лексер. Он не пытается выполнить проверочный анализ; он просто определяет лексические элементы. Вы можете думать об этом больше как о блоке HTML, чем о парсере HTML. Разбитый HTML не очень простителен, хотя в этом направлении он делает некоторые небольшие поправки.
Даже если вы никогда не анализируете полный HTML самостоятельно (и почему вы должны решить эту проблему!), В этой программе есть много интересных элементов регулярных выражений, из которых, я полагаю, многие люди могут многому научиться. Наслаждайтесь!
#!/usr/bin/env perl
#
# chunk_HTML - a regex-based HTML chunker
#
# Tom Christiansen <[email protected]
# 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 }