当正则表达式模式在字符串中没有匹配时,该怎么办?

201

我正在尝试使用这个模式匹配类型为hidden<input>字段:

/<input type="hidden" name="([^"]*?)" value="([^"]*?)" />/

这是一些示例表单数据:

<input type="hidden" name="SaveRequired" value="False" /><input type="hidden" name="__VIEWSTATE1" value="1H4sIAAtzrkX7QfL5VEGj6nGi+nP" /><input type="hidden" name="__VIEWSTATE2" value="0351118MK" /><input type="hidden" name="__VIEWSTATE3" value="ZVVV91yjY" /><input type="hidden" name="__VIEWSTATE0" value="3" /><input type="hidden" name="__VIEWSTATE" value="" /><input type="hidden" name="__VIEWSTATE" value="" />

但我不确定typenamevalue属性是否总是以相同的顺序出现。如果type属性出现在最后,匹配将失败,因为在我的模式中它在开头。

问题:
如何更改我的模式,使其与<input>标记中属性的位置无关?

P.S.:顺便说一下,我正在使用基于Adobe Air的RegEx Desktop Tool测试正则表达式。


4
如果您能够掌控生成的HTML,那么正则表达式是一种非常出色的解决方案,因为这是一个规则 vs 非规则的辩论。但在我的情况下,我不知道HTML将来会如何变化,所以最好使用解析器而不是正则表达式。我在自己项目中使用了正则表达式,但只用于我可以控制的部分。 - Salman
2
Stack Overflow经典问题是一个回答的问题,其开头为“你不能使用正则表达式解析[X]HTML。”。 - Peter Mortensen
1
@PeterMortensen 很不幸,那个答案对于寻求知识的人并没有帮助。它只会让已经理解问题的人感到有趣。 - Andy Lester
8个回答

726

哦,是的,你可以使用正则表达式解析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"

解析输入标签,避免恶意输入

这是生成上述输出的程序源代码。

#!/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) 
            (?= (?&quote)? 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>           (?&quoted_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) 
                (?:
                    (?&quoted_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(为什么要这样做?这是一个已解决的问题!),这个程序有很多很酷的正则表达式部分,我相信很多人可以从中学到很多东西。享受吧!
#!/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)
                            (?&quote) ?
            (?<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)
            (?= (?&quote)? 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>           (?&quoted_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> (?&quoted_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 }

24
你的评论中有两个亮点:“我经常使用解析类,特别是对于我没有生成的 HTML。”和“模式不必丑陋,也不必难以理解。如果你创建了丑陋的模式,那反映的是你自己,而不是它们本身。”我完全同意你的说法,所以我正在重新评估这个问题。非常感谢你提供如此详细的答案。 - Salman
175
对于那些不知道的人,我想提一下Tom是“编程Perl”(也称为骆驼书)的联合作者之一,也是顶尖的Perl权威之一。如果你怀疑这是真正的Tom Christiansen,请返回并阅读帖子。 - Bill Ruppert
23
总之:正则表达式的命名不准确。我认为这很遗憾,但它不会改变。兼容的“正则表达式”引擎不允许拒绝非正则语言。因此,仅使用有限状态机无法正确实现它们。计算类别周围的强大概念不适用。使用正则表达式不能保证O(n)执行时间。正则表达式的优点是简洁的语法和暗示的字符识别领域。对我来说,这是一场缓慢而灾难性的列车事故,不可能看着不管,但后果却可怕。 - Steve Steiner
29
@tchrist,这并没有回答原帖中的问题。而且,“解析”是否是这里的正确术语?我认为正则表达式正在进行令牌化/词法分析,但最终解析是使用Perl代码完成的,而不是由正则表达式本身完成的。 - Qtax
71
@tchrist,非常令人印象深刻。显然,您是一位技能高超、才华横溢的Perl程序员,并且对现代正则表达式非常了解。但我想指出的是,您所编写的并不是真正的正则表达式(无论是现代的还是普通的),而是使用正则表达式的Perl程序。您的帖子是否真正支持正则表达式可以正确解析HTML?还是更像证明了Perl可以正确解析HTML?无论哪种方式,都很棒! - Mike Clark
显示剩余9条评论

131
  1. 你可以像tchrist一样写一本小说
  2. 你可以使用DOM库,加载HTML并使用xpath,只需使用 //input[@type="hidden"]。或者如果不想使用xpath,只需获取所有输入并使用getAttribute过滤哪些是隐藏的。

我更喜欢第二种方法。

<?php

$d = new DOMDocument();
$d->loadHTML(
    '
    <p>fsdjl</p>
    <form><div>fdsjl</div></form>
    <input type="hidden" name="blah" value="hide yo kids">
    <input type="text" name="blah" value="hide yo kids">
    <input type="hidden" name="blah" value="hide yo wife">
');
$x = new DOMXpath($d);
$inputs = $x->evaluate('//input[@type="hidden"]');

foreach ( $inputs as $input ) {
    echo $input->getAttribute('value'), '<br>';
}

结果:

hide yo kids<br>hide yo wife<br>

78
那其实就是我的观点。我想展示这有多难。 - tchrist
19
非常好的东西。我真的希望人们能展示使用解析类有多么容易,所以谢谢!我只是想要一个可行的例子,来展示用正则表达式从头开始做需要经历多大的麻烦。我真心希望大多数人会得出结论,使用预制的解析器处理通用 HTML 而不是自己动手。然而,对于自己编写的简单 HTML,正则表达式仍然非常好用,因为它可以消除 99.98% 的复杂性。 - tchrist
5
阅读这两种非常有趣的方法后,进行比较它们之间的速度/内存使用/CPU(例如基于正则表达式与解析类)将会很不错。 - the_yellow_logo
1
@Avt'W 是的,如果正则表达式确实更快,你不应该去写一个“小说”,但事实上了解这一点真的很有趣。 :) 但我猜测解析器所需的资源也会更少。 - Dennis98
这其实就是XPath最初被发明的原因! - Thorbjørn Ravn Andersen

113

与所有其他答案相反,对于您要做的事情,正则表达式是完全有效的解决方案。这是因为您并不需要匹配平衡的标签——使用正则表达式是不可能匹配成功的!但是您只需要匹配一个标签内的内容,这是完全规律的。

然而,有个问题。你不能只用一个正则表达式来完成它......你需要进行一次匹配以捕获一个<input>标签,然后对其进行进一步处理。请注意,如果任何属性值中有>字符,那么这将无法正常工作,所以它并不完美,但对于合理的输入应该足够了。

这里是一些Perl(伪)代码,可以说明我的意思:

my $html = readLargeInputFile();

my @input_tags = $html =~ m/
    (
        <input                      # Starts with "<input"
        (?=[^>]*?type="hidden")     # Use lookahead to make sure that type="hidden"
        [^>]+                       # Grab the rest of the tag...
        \/>                         # ...except for the />, which is grabbed here
    )/xgm;

# Now each member of @input_tags is something like <input type="hidden" name="SaveRequired" value="False" />

foreach my $input_tag (@input_tags)
{
  my $hash_ref = {};
  # Now extract each of the fields one at a time.

  ($hash_ref->{"name"}) = $input_tag =~ /name="([^"]*)"/;
  ($hash_ref->{"value"}) = $input_tag =~ /value="([^"]*)"/;

  # Put $hash_ref in a list or something, or otherwise process it
}
基本原则是,不要试图在一个正则表达式中做太多事情。正如您所注意到的,正则表达式强制执行一定的顺序。因此,您需要做的是首先匹配您要提取内容的上下文,然后对所需数据进行子匹配。
编辑:但是,我同意通常情况下,使用HTML解析器可能更容易更好,并且您真的应该考虑重新设计代码或重新审视目标。:-) 但是我必须发布这个答案作为对于解析任何HTML子集都是不可能的这种短视反应的反驳:当您考虑整个规范时,HTML和XML都是不规则的,但是标签的规范相当规则,肯定在PCRE的能力范围内。

16
并不与这里的所有回答都相矛盾。 :) - tchrist
7
当我回答的时候,@tchrist的回答还没有出现。;-) - Platinum Azure
9
嗯,出于某种原因,我的打字比你的慢。我想我的键盘可能需要加油脂。 :) - tchrist
6
这是无效的HTML代码 - 应该是value="<Are you really sure about this?>"。如果他正在爬取的网站没有正确转义这样的内容,那么他需要一个更复杂的解决方案 - 但是如果他们做得对(如果他可以控制它,他应该确保它是对的),那么他就没问题了。 - Ross Snyder
14
必要的链接,关于此主题最好的SO答案之一(可能是有史以来最好的SO答案):https://dev59.com/X3I-5IYBdhLWcg3wq6do#1732454 - Daniel Ribeiro
显示剩余12条评论

22

在Tom Christiansen的词法分析器解决方案的启发下,这里提供了Robert Cameron在1998年似乎被遗忘的文章链接:REX: XML Shallow Parsing with Regular Expressions.

http://www.cs.sfu.ca/~cameron/REX.html

摘要

XML的语法足够简单,可以使用一个正则表达式将XML文档解析为标记和文本项列表。对XML文档进行浅层解析可以构建各种轻量级的XML处理工具。然而,复杂的正则表达式往往难以构建,更难以阅读。本文采用一种面向正则表达式的文学编程形式,记录了一组XML浅层解析表达式,可用作简单、正确、高效、健壮和语言无关的XML浅层解析的基础。此外,还给出了不到50行Perl、JavaScript和Lex/Flex实现完整浅层解析器的示例。

如果您喜欢阅读有关正则表达式的内容,Cameron的论文非常有趣。他的写作简洁、详细,十分详尽。他不仅展示了如何构建REX正则表达式,还介绍了一种从更小的部分构建任何复杂正则表达式的方法。

我已经使用REX正则表达式十年左右,解决了最初发布者提出的问题(如何匹配此特定标记但不匹配其他非常相似的标记?)。我发现他开发的正则表达式完全可靠。

在处理文档的词法细节时,REX尤其有用--例如,在将一种类型的文本文档(如纯文本、XML、SGML、HTML)转换为另一种类型时,该文档可能无效、格式错误,甚至大部分不能解析。它允许您针对文档中任何位置的标记集合而不影响文档的其余部分。


9
虽然我喜欢其他答案的内容,但它们没有直接或准确地回答问题。即使Platinum的答案过于复杂,效率也不高。因此,我被迫提供这个答案。
当使用正确时,我非常支持正则表达式。但由于某些负面印象(和性能),我总是建议使用格式良好的XML或HTML来使用XML解析器。而且,更好的性能将会是字符串解析,尽管在可读性与效率之间有一定的平衡。不过,这不是问题的关键。问题是如何匹配隐藏类型的输入标签。答案是:
<input[^>]*type="hidden"[^>]*>

根据您的喜好,您唯一需要包括的正则表达式选项是忽略大小写选项。


6
<input type='hidden' name='Oh, <really>?' value='尝试使用真正的HTML解析器。'> - Ilmari Karonen
4
你的示例是自闭的,应该以/> 结尾。此外,虽然在名称字段中出现 > 的可能性几乎为零,但确实有可能在操作句柄中出现 >。例如:在OnClick属性上调用内联JavaScript函数。话虽如此,我有一个XML解析器,也有一个针对那些给定文档太混乱而无法由XML解析器处理的情况的正则表达式。此外,这不是问题所在。您永远不会遇到这些情况隐藏的输入,并且我的答案是最好的。 是的,<真的>! - Suamere
4
/> 是 XML 的写法,在除了 XHTML(实际上并不被广泛使用,已被 HTML5 取代)之外的任何 HTML 版本中都不需要。你说得没错,有很多混乱的不太合法的 HTML,但是一个好的 HTML(不是 XML)解析器应该能够处理大部分情况;如果它们不能处理,那么浏览器也很可能无法处理。 - Ilmari Karonen
1
如果你只需要解析或搜索一次来返回一组隐藏输入字段,那么这个正则表达式会非常完美。当Regex内置时,使用.NET XML Document类或引用第三方XML/HTML解析器仅调用一个方法会过度杀伤。你说得对,一个网站如此混乱以至于一个好的HTML解析器无法处理它,可能根本不是开发人员所要看的东西。但我的公司每个月都会交接数百万页被串联并且多种方式上被破坏的页面,有时(并非总是),Regex是最好的选择。 - Suamere
1
唯一的问题是我们不确定整个公司为什么这个开发人员想要这个答案。但这就是他要求的。 - Suamere

3
您可以尝试这个:
<[A-Za-z ="/_0-9+]*>

如果想要更精确的结果,可以尝试以下方法:

<[ ]*input[ ]+type="hidden"[ ]*name=[A-Za-z ="_0-9+]*[ ]*[/]*>

您可以在http://regexpal.com/上测试您的正则表达式模式。

以下模式适用于此情况:

<input type="hidden" name="SaveRequired" value="False" /><input type="hidden" name="__VIEWSTATE1" value="1H4sIAAtzrkX7QfL5VEGj6nGi+nP" /><input type="hidden" name="__VIEWSTATE2" value="0351118MK" /><input type="hidden" name="__VIEWSTATE3" value="ZVVV91yjY" />

如果您需要随机排列typenamevalue,则可以使用以下代码:

<[ ]*input[ ]*[A-Za-z ="_0-9+/]*>

或者

<[ ]*input[ ]*[A-Za-z ="_0-9+/]*[ ]*[/]>

在这个问题上:

<input  name="SaveRequired" type="hidden" value="False" /><input type="hidden" name="__VIEWSTATE1" value="1H4sIAAtzrkX7QfL5VEGj6nGi+nP" /><input type="hidden" name="__VIEWSTATE2" value="0351118MK" /><input  name="__VIEWSTATE3" type="hidden" value="ZVVV91yjY" />

顺便说一下,我认为你需要的是这样的内容:
<[ ]*input(([ ]*type="hidden"[ ]*name=[A-Za-z0-9_+"]*[ ]*value=[A-Za-z0-9_+"]*[ ]*)+)[ ]*/>|<[ ]*input(([ ]*type="hidden"[ ]*value=[A-Za-z0-9_+"]*[ ]*name=[A-Za-z0-9_+"]*[ ]*)+)[ ]*/>|<[ ]*input(([ ]*name=[A-Za-z0-9_+"]*[ ]*type="hidden"[ ]*value=[A-Za-z0-9_+"]*[ ]*)+)[ ]*/>|<[ ]*input(([ ]*value=[A-Za-z0-9_+"]*[ ]*type="hidden"[ ]*name=[A-Za-z0-9_+"]*[ ]*)+)[ ]*/>|<[ ]*input(([ ]*name=[A-Za-z0-9_+"]*[ ]*value=[A-Za-z0-9_+"]*[ ]*type="hidden"[ ]*)+)[ ]*/>|<[ ]*input(([ ]*value=[A-Za-z0-9_+"]*[ ]*name=[A-Za-z0-9_+"]*[ ]*type="hidden"[ ]*)+)[ ]*/>

虽然不太好,但它可以在任何情况下正常工作。

在以下网站进行测试:http://regexpal.com/


1
我想使用**DOMDocument**来提取HTML代码。
$dom = new DOMDocument();
$dom ->loadHTML($input);
$x = new DOMXpath($dom );
$results = $x->evaluate('//input[@type="hidden"]');

foreach ( $results as $item) {
    print_r( $item->getAttribute('value') );
}

顺便提一下,你可以在regex101.com上测试它。它会实时显示结果。 关于正则表达式的一些规则:http://www.eclipse.org/tptp/home/downloads/installguide/gla_42/ref/rregexp.html Reader


0
假设您的HTML内容存储在字符串html中,为了获取每个包含隐藏类型的输入,您可以使用正则表达式。
var regex = /(<input.*?type\s?=\s?["']hidden["'].*?>)/g;
html.match(regex);

上述正则表达式查找<input后跟任意数量的字符,直到遇到type="hidden"或type='hidden'后跟任意数量的字符,直到遇到>

/g告诉正则表达式查找与给定模式匹配的每个子字符串。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接