О да, ты можешь использовать регулярные выражения для анализа 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.
Демонстрация решения задачи 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"
Разобрать теги ввода, не видеть ввод зла
Вот исходный код программы, которая дала результат выше.
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";
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: $!";
}
Вот так! Ничего подобного! :)
Только вы можете судить, подходят ли ваши навыки работы с регулярными выражениями для какой-либо конкретной задачи синтаксического анализа. Уровень навыков у всех разный, и каждое новое задание разное. Для заданий, где у вас есть четко определенный набор входных данных, регулярные выражения, очевидно, являются правильным выбором, потому что собрать некоторые вместе, когда у вас есть ограниченное подмножество HTML, с которым нужно иметь дело, тривиально. Даже новички в регулярных выражениях должны выполнять эти задания с помощью регулярных выражений. Все остальное - излишество.
Однако , как только HTML начинает становиться менее пригвожденным, как только он начинает разветвляться способами, которые вы не можете предсказать, но которые совершенно законны, как только вам придется сопоставить больше разных видов вещей или с более сложными зависимостями, вы в конечном итоге достигнете точки, когда вам придется потрудиться, чтобы реализовать решение, использующее регулярные выражения, чем при использовании класса синтаксического анализа. То, где упадет эта точка безубыточности, снова зависит от вашего собственного уровня комфорта с регулярными выражениями.
Так что я должен делать?
Я не собираюсь говорить вам, что вы должны делать или чего не можете . Я думаю, что это неправильно. Я просто хочу представить вам возможности, приоткройте глаза немного. Вы можете выбирать, что вы хотите делать и как вы хотите это делать. Абсолютов не существует - и никто другой не знает вашу ситуацию так хорошо, как вы сами. Если что-то кажется, что это слишком много работы, что ж, может быть, это так. Знаешь, программирование должно доставлять удовольствие . Если это не так, возможно, вы делаете это неправильно.
На мою html_input_rx
программу можно смотреть множеством подходящих способов. Одна из них заключается в том, что вы действительно можете анализировать HTML с помощью регулярных выражений. Но другое заключается в том, что это намного, намного, намного сложнее, чем кто-либо думает. Это может легко привести к выводу, что моя программа является свидетельством того, чего вам не следует делать, потому что это действительно слишком сложно.
Я не буду с этим не согласен. Конечно, если все, что я делаю в своей программе, не имеет для вас смысла после некоторого изучения, вам не следует пытаться использовать регулярные выражения для такого рода задач. Для определенного HTML регулярные выражения хороши, но для обычного HTML они равносильны безумию. Я все время использую классы синтаксического анализа, особенно если это HTML, который я не создавал сам.
Регулярные выражения оптимальны для небольших задач разбора HTML, пессимальные - для больших
Даже если мою программу рассматривать как иллюстрацию того, почему вы не должны использовать регулярные выражения для синтаксического анализа общего HTML - и это нормально, потому что я вроде как хотел, чтобы это было так - она все равно должна открывать глаза, чтобы больше людей нарушали ужасно распространенные и неприятная, неприятная привычка писать нечитаемые, неструктурированные и неподдерживаемые шаблоны.
Выкройки не обязательно должны быть уродливыми и сложными. Если вы создаете уродливые узоры, это отражается на вас, а не на них.
Феноменально изысканный язык регулярных выражений
Меня попросили указать, что предложенное мной решение вашей проблемы написано на Perl. Вы удивлены? Вы не заметили? Это открытие произвело эффект разорвавшейся бомбы?
Верно, что не все другие инструменты и языки программирования столь же удобны, выразительны и мощны, когда дело касается регулярных выражений, как Perl. Существует большой спектр, некоторые из которых подходят больше, чем другие. В общем, с языками, которые выражают регулярные выражения как часть основного языка, а не как библиотеку, легче работать. Я ничего не сделал с регулярными выражениями, что вы не могли бы сделать, скажем, в PCRE, хотя вы бы структурировали программу иначе, если бы использовали C.
В конце концов, другие языки догонят Perl с точки зрения регулярных выражений. Я говорю это потому, что когда начинался Perl, ни у кого не было ничего похожего на регулярные выражения Perl. Говорите что угодно, но именно здесь Perl явно победил: все копировали регулярные выражения Perl, хотя и на разных этапах их разработки. Perl был пионером почти (не совсем всего, но почти) всего, на что вы привыкли полагаться в современных шаблонах сегодня, независимо от того, какой инструмент или язык вы используете. Таким образом , в конце концов , другие будут догонять.
Но они догонят только то, на чем был Perl когда-то в прошлом, как и сейчас. Все идет вперед. В регулярных выражениях, если ничто иное, куда ведет Perl, следуют другие. Где будет Perl, когда все, наконец, поймут, где Perl находится сейчас? Понятия не имею, но знаю, что мы тоже переедем. Возможно, мы будем ближе к стилю создания шаблонов Perl .
Если вам нравятся такие вещи, но вы хотели бы использовать их в Perl₅, вас может заинтересовать замечательный модуль Regexp :: Grammars Дэмиана Конвея . Это совершенно потрясающе, и то, что я сделал здесь, в своей программе, кажется таким же примитивным, как и мое, что создает шаблоны, которые люди собирают вместе, без пробелов или буквенных идентификаторов. Проверить это!
Простой HTML Chunker
Вот полный исходный код парсера, который я показал в самом начале этой публикации.
Я не предлагаю вам использовать это вместо тщательно протестированного класса синтаксического анализа. Но я устал от людей, которые притворяются, будто никто не может анализировать HTML с помощью регулярных выражений только потому, что они не могут. Вы явно можете, и эта программа является доказательством этого утверждения.
Конечно, это не легко, но это это возможно!
И попытки сделать это - ужасная трата времени, потому что существуют хорошие классы синтаксического анализа, которые вы должны использовать для этой задачи. Правильный ответ людям, пытающимся разобрать произвольный HTML, заключается не в том, что это невозможно. Это легкий и лукавый ответ. Правильный и честный ответ состоит в том, что они не должны пытаться это сделать, потому что слишком сложно разобраться с нуля; они не должны ломать себе спину, пытаясь заново изобрести колесо, которое отлично работает.
С другой стороны, HTML, который попадает в предсказуемое подмножество , очень легко анализировать с помощью регулярных выражений. Неудивительно, что люди пытаются их использовать, потому что для небольших задач, возможно, игрушечных, нет ничего проще. Вот почему так важно различать две задачи - конкретную и общую - поскольку они не обязательно требуют одного и того же подхода.
Я надеюсь, что в будущем здесь будет более справедливое и честное решение вопросов об HTML и регулярных выражениях.
Вот мой лексер HTML. Он не пытается выполнить проверочный синтаксический анализ; он просто определяет лексические элементы. Вы могли бы думать об этом больше как о чанкере HTML, чем о парсере HTML. Он не очень прощает сломанный HTML, хотя делает некоторые очень небольшие поправки в этом направлении.
Даже если вы никогда не разбираете полный HTML самостоятельно (а зачем вам это нужно? Это решенная проблема!), В этой программе есть много интересных битов регулярных выражений, на которых, я думаю, многие люди могут многому научиться. Наслаждайтесь!
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();
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);
}
}
}
say ".";
}
sub slurpy {
our ($RX_SUBS, $Meta_Tag_Rx);
my $_ = do { local $/; <ARGV> };
return unless length;
use Encode qw< decode >;
my $bom = "";
given ($_) {
$bom = "UTF-32LE" when / ^ \xFf \xFe \0 \0 /x;
$bom = "UTF-32BE" when / ^ \0 \0 \xFe \xFf /x;
$bom = "UTF-16LE" when / ^ \xFf \xFe /x;
$bom = "UTF-16BE" when / ^ \xFe \xFf /x;
$bom = "UTF-8" when / ^ \xEF \xBB \xBF /x;
}
if ($bom) {
say "[BOM $bom]";
s/^...// if $bom eq "UTF-8";
$bom =~ s/-[LB]E//;
return decode($bom, $_);
}
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, $_);
}
UNITCHECK { load_rxsubs() }
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;
}
END { close STDOUT }