О так, ви можете використовувати Regexes для розбору HTML!
Для завдання, яке ви намагаєтесь, регулярно виходять регулярні виразки !
Це є правдою , що більшість людей недооцінюють складність розбір HTML з регулярними виразами і , отже , зробити так погано.
Але це не якийсь фундаментальний недолік, пов'язаний з теорією обчислень. Ця дурість тут дуже папугує , але чи не вірите ви їм.
Отже, хоча це, безумовно, можна зробити (ця публікація слугує доказом існування цього незаперечного факту), це не означає, що воно повинно бути.
Ви повинні вирішити, чи вирішуєте ви написати те, що означає спеціальний, спеціальний HTML-аналізатор із регулярних виразів. Більшість людей - ні.
Але я є. ☻
Загальні рішення для розбиття HTML на основі Regex
Спершу я покажу, як легко розібрати довільний 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-файлах - кожен з них я міг швидко і легко отримати свої руки. Крім того, я також запускав його на файли, спеціально створені для злому наївних аналізаторів.
Це не наївний аналізатор.
О, я впевнений, що він не ідеальний, але я ще не встиг його зламати. Я вважаю, що навіть якщо щось зробить, виправлення було б легко вписати через чітку структуру програми. Навіть програми, важкі для регулярних викидів, повинні мати загрозу.
Тепер, коли це не в змозі, дозвольте мені вирішити питання ОП.
Демонстрація завдання ОП за допомогою реджексів
Маленька 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 <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: $!";
}
Ось так! Нічого до цього! :)
Тільки ви можете судити, чи відповідає ваша майстерність з регулярними виразами якесь конкретне завдання розбору. У кожного рівень майстерності різний, і кожне нове завдання різне. Для робочих місць, де у вас чітко визначений набір вводу, регулярно виражаються правильні вибори, оскільки тривіально об'єднувати деякі, коли у вас є обмежений підмножина HTML, з якою ви маєте справу. Навіть новачки з регулярними виразками повинні впоратися з цими роботами за допомогою регулярних виразів. Все інше - надмірне.
Однак , як тільки HTML почне ставати менш прибитим донизу, як тільки він почне розширюватися способами, які ви не можете передбачити, але які є цілком законними, як тільки вам доведеться співставити більше різних речей або зі складнішими залежностями, ви зрештою дістанетесь до точки, коли вам доведеться більше працювати, щоб виконати рішення, яке використовує регулярні вирази, ніж вам доведеться використовувати клас розбору. Де ця точка беззбитковості знову залежить від вашого власного рівня комфорту з регулярними виразами.
Отже, що мені робити?
Я не збираюся розповідати вам, що ви повинні робити або чого не можете зробити. Я думаю, що це неправильно. Я просто хочу представити вам можливості, трохи відкрийте очі. Ви можете вибрати, що ви хочете робити і як ви хочете це зробити. Абсолютів немає - і ніхто не знає вашої власної ситуації так само, як ви самі. Якщо щось здається, що це занадто багато роботи, ну, можливо, так і є. Програмування має бути цікавим . Якщо це не так, ви можете зробити це неправильно.
Мою html_input_rx
програму можна переглянути будь-якою кількістю дійсних способів. Одне з таких полягає в тому, що ви дійсно можете розбирати HTML за допомогою регулярних виразів. Але інше - це набагато, набагато, набагато важче, ніж майже хто-небудь думає, що це таке. Це може легко призвести до висновку, що моя програма є свідченням того, чого ви не повинні робити, оскільки це дійсно занадто важко.
Я не погоджуюся з цим. Звичайно, якщо все, що я роблю в своїй програмі, не має сенсу для вас після якогось дослідження, то вам не слід намагатися використовувати регулярні вирази для подібних завдань. Для конкретного HTML, режекси є чудовими, але для загального HTML вони рівнозначні безумству. Я постійно використовую класи для розбору, особливо якщо це HTML, який я сам не створив.
Режекси оптимальні для невеликих проблем розбору HTML, песимізовані для великих
Навіть якщо моя програма сприймається як ілюстративний, чому ви не повинні використовувати регулярні вирази для розбору загального HTML - це нормально, тому що я начебто мав на увазі, що це that - це все одно має бути відкривачем очей, щоб більше людей порушувало жахливо загальне явище і бридкий, бридкий звичку писати нечитані, неструктуровані та незрозумілі зразки.
Візерунки не повинні бути потворними, і вони не повинні бути жорсткими. Якщо ви створюєте некрасиві візерунки, це відображення на вас, а не на них.
Феноменально вишукана мова Regex
Мене попросили зазначити, що моє глибоке рішення вашої проблеми було написано в Perl. Ви здивовані? Ви не помітили? Це одкровення бомба?
Це правда, що не всі інші інструменти та мови програмування є такими ж зручними, виразними та потужними, що стосується регулярних виразів, як Perl. Там великий спектр, де деякі є більш підходящими, ніж інші. Взагалі, з мовами, які виражають регекси як частину основної мови, а не як бібліотеку, легше працювати. Я нічого не робив з реджексами, які ти не міг зробити, скажімо, в PCRE, хоча ти б по-іншому структурував програму, якби ти використовував C.
Врешті-решт, інші мови будуть наздогнані там, де зараз Perl з точки зору регулярних виразів. Я говорю це тому, що тоді, коли Перл починав, ніхто інший не мав нічого подібного до виразів Перла. Скажіть що завгодно, але тут явно перемогла Перл: усі копіювали регекси Перла, хоча на різних етапах свого розвитку. Perl був першопрохідцем майже (не зовсім, але майже) все, на що ви покладаєтесь в сучасних моделях, незалежно від того, яким інструментом чи мовою ви користуєтесь. Таким чином , в кінці кінців , інші будуть наздоганяти.
Але вони доганятимуться лише там, де колись був Перл, як і зараз. Все просувається. У реджексах, якщо нічого іншого, куди веде Перл, слідують інші. Де колись буде Перл, коли всі нарешті наздоганяють туди, де зараз знаходиться Перл? Я поняття не маю, але я знаю, що ми теж переїхали. Напевно, ми будемо ближче до стилю Perl₆ майстерності майстерності .
Якщо вам подобається така річ, але ви хочете використовувати її в Perl₅, вам може бути цікавий чудовий модуль Regexp :: Grammars від Damian Conway . Це абсолютно дивовижно, і те, що я робив тут у своїй програмі, здається настільки ж примітивним, як і моя, створює шаблони, які люди стискають разом без пробілів чи алфавітних ідентифікаторів. Перевір!
Простий HTML Chunker
Ось повне джерело до синтаксичного аналізу, який я показав у центрі цього повідомлення.
Я не пропоную вам використовувати це в умовах жорсткого тестування класу розбору. Але я втомився від того, щоб люди робили вигляд, що ніхто не може розібрати HTML з регулярними виразами лише тому, що вони не можуть. Ви однозначно можете, і ця програма є доказом цього твердження.
Звичайно, це не легко, але це це можливо!
І намагатися це зробити - це страшна трата часу, адже існують хороші класи для розбору, які ви повинні використовувати для цього завдання. Правильна відповідь людям, які намагаються розібрати довільний HTML, - це не те, що це неможливо. Це чудова і недоброзичлива відповідь. Правильна і чесна відповідь полягає в тому, що вони не повинні намагатися цього робити, тому що це занадто сильно турбуватися, щоб зрозуміти з нуля; вони не повинні ламати спину, прагнучи відтворити колесо, яке працює чудово.
З іншого боку, HTML, який потрапляє до передбачуваного підмножини, дуже легко проаналізувати з регулярними виразами. Не дивно, що люди намагаються їх використовувати, тому що для невеликих проблем, проблем із іграшками, можливо, нічого не може бути простішим. Ось чому так важливо розмежувати дві задачі - специфічну проти загальної - оскільки вони не обов'язково вимагають однакового підходу.
Я сподіваюсь у майбутньому тут побачити більш справедливе та чесне ставлення до питань щодо HTML та регулярних виразів.
Ось мій HTML лексер. Він не намагається зробити валідаційний синтаксичний аналіз; він просто ідентифікує лексичні елементи. Ви можете подумати про це швидше як HTML-фрагмент, ніж проаналізатор HTML. Це не дуже прощає зламаний HTML, хоча він робить дуже маленькі припущення в цьому напрямку.
Навіть якщо ви ніколи самі не розбираєте повний HTML (а навіщо це вам? Це вирішена проблема!), У цій програмі є безліч крутих шматочків регулярних виразів, з яких я вважаю, що багато людей можуть багато чому навчитися. Насолоджуйтесь!
#!/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 }