哦,是的,你可以使用正则表达式解析 HTML!
对于您正在尝试的任务,正则表达式非常好!
确实大多数人都低估了使用正则表达式解析 HTML 的难度,因此做得很差。
但这并不是与计算理论相关的基本缺陷。 That silliness is parroted a lot around here,但你不相信他们。
因此,虽然它肯定是可以做到的(这篇文章是这个无可争议的事实的存在证明),但这并不意味着它应该是。
您必须自己决定是否能够胜任使用正则表达式编写相当于专用、专用 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"
解析输入标签,看不到恶意输入
这是产生上述输出的程序的源代码。
#!/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。
正则表达式最适合 small HTML 解析问题,对大问题最不利
即使我的程序被用来说明为什么你应该不使用正则表达式来解析一般的 HTML——这没关系,因为我的意思是这样 ☺——它仍然应该是一个让更多人大开眼界,让更多的人改掉编写不可读、非结构化和不可维护的模式的非常普遍、令人讨厌、令人讨厌的习惯。
模式不必难看,也不必难。如果你创造了丑陋的图案,那是你的反映,而不是它们。
非凡精致的正则表达式语言
有人要求我指出,我为您的问题提供的解决方案是用 Perl 编写的。你惊喜吗?你没注意到吗?这个启示是重磅炸弹吗?
确实,在正则表达式方面,并非所有其他工具和编程语言都像 Perl 一样方便、富有表现力和强大。那里有很大的范围,有些比其他更合适。一般来说,将正则表达式表达为核心语言的一部分而不是作为库的语言更容易使用。我没有对你在 PCRE 等中无法使用的正则表达式做任何事情,尽管如果你使用 C,你会以不同的方式构建程序。
最终,其他语言将在正则表达式方面赶上 Perl 现在的水平。我这样说是因为在 Perl 刚开始的时候,没有其他人有像 Perl 的正则表达式这样的东西。随便说什么,但这就是 Perl 明显胜出的地方:每个人都在复制 Perl 的正则表达式,尽管它们处于不同的开发阶段。 Perl 开创了当今现代模式中几乎(不是全部,但几乎)您所依赖的一切,无论您使用什么工具或语言。所以最终其他人会赶上来。
但它们只会赶上 Perl 过去某个时候的水平,就像现在一样。一切都在进步。在正则表达式中,如果没有别的,Perl 领先,其他人紧随其后。一旦其他人最终赶上 Perl 现在的位置,Perl 会在哪里?我不知道,但我知道我们也会搬家。可能我们会更接近Perl₆’s style of crafting patterns。
如果你喜欢这种东西但又想在 Perl₅ 中使用它,你可能会对 Damian Conway’s wonderful Regexp::Grammars 模块感兴趣。这太棒了,让我在我的程序中所做的看起来和我的一样原始,让人们在没有空格或字母标识符的情况下挤在一起的模式。看看吧!
简单的 HTML 分块器
这是我在本文开头展示的核心部分的解析器的完整源代码。
我不建议您应该在经过严格测试的解析类上使用它。但是我厌倦了人们假装没有人可以用正则表达式解析 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 }