哦,是的,你可以使用正则表达式解析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的问题。
使用Regexes演示解决OP的任务
我在下面包含的小程序html_input_rx
会生成以下输出,以便您可以看到使用regex解析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₅中使用它,那么你可能会对
Damian Conway的精彩Regexp::Grammars模块感兴趣。它非常棒,使我在我的程序中所做的工作看起来像是一些没有空格或字母标识符的模式。快去看看吧!
简单的HTML分块器
这是我在文章开头展示的中心代码的完整源代码。
我并不建议您使用此代码替代经过严格测试的解析类。但我已经厌倦了人们假装无法使用正则表达式解析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 }