Perl Mojo :: DOM来查找和替换html块

时间:2014-06-05 15:46:06

标签: html perl dom mojo

由于这里的每个人都建议使用Perl模块Mojo::DOM来执行此任务,因此我想知道如何使用它。

我在模板中有这个html代码:

some html content here top base
<!--block:first-->
    some html content here 1 top
    <!--block:second-->
        some html content here 2 top
        <!--block:third-->
            some html content here 3a
            some html content here 3b
        <!--endblock-->
        some html content here 2 bottom
    <!--endblock-->
    some html content here 1 bottom
<!--endblock-->
some html content here bottom base

我想做什么(请不要建议再次使用模板模块),我想先找到内部块:

        <!--block:third-->
            some html content here 3a
            some html content here 3b
        <!--endblock-->

然后用一些html代码替换它,然后找到第二个块:

<!--block:second-->
    some html content here 2 top
    <!--block:third-->
        some html content here 3a
        some html content here 3b
    <!--endblock-->
    some html content here 2 bottom
<!--endblock-->

然后用一些html代码替换它,然后找到第三个块:

<!--block:first-->
    some html content here 1 top
    <!--block:second-->
        some html content here 2 top
        <!--block:third-->
            some html content here 3a
            some html content here 3b
        <!--endblock-->
        some html content here 2 bottom
    <!--endblock-->
    some html content here 1 bottom
<!--endblock-->

1 个答案:

答案 0 :(得分:1)

没有建议使用Mojo::DOM执行此任务,因为它可能有点矫枉过正,但是......你可以。

真正的答案是我已经说过in other questions的答案,那就是使用现有的框架,例如Template::Toolkit。它功能强大,经过良好测试,速度快,因为它允许缓存模板。

但是,您希望推出自己的模板解决方案。任何此类解决方案都应包括解析,验证和执行阶段。我们将专注于前两个步骤,因为您在最后一个步骤中没有真正的信息。

Mojo::DOM中没有任何真正的魔法。它的好处和强大之处在于它可以完全轻松地解析HTML,捕获所有这些潜在的边缘情况。它只能帮助解析模板的解析阶段,因为它是你决定验证的自己的规则。事实上,它基本上就像我提供给你的my earlier solutionsplit的替换一样。这就是为什么它可能过于沉重的解决方案。

因为进行修改并不难,所以我在下面编写了一个完整的解决方案。但是,为了使事情更有趣,并试图证明我的一个更重要的观点,现在是时候在3个可用的解决方案之间分享一些Benchmark测试:

  1. Mojo::DOM进行解析,如下所示。
  2. split用于解析我在Match nested html comment blocks regex
  3. 中提出的解析 slnPerl replace nested blocks regex 中提出的
  4. recursive regex

    以下包含所有三种解决方案:

    use strict;
    use warnings;
    
    use Benchmark qw(:all);
    
    use Mojo::DOM;
    use Data::Dump qw(dump dd);
    
    my $content = do {local $/; <DATA>};
    
    #dd parse_using_mojo($content);
    #dd parse_using_split($content);
    #dd parse_using_regex($content);
    
    timethese(100_000, {
        'regex' => sub { parse_using_regex($content) },
        'mojo' => sub { parse_using_mojo($content) },
        'split' => sub { parse_using_split($content) },
    });
    
    sub parse_using_mojo {
        my $content = shift;
    
        my $dom = Mojo::DOM->new($content);
    
        # Resulting Data Structure
        my @data = ();
    
        # Keep track of levels of content
        # - This is a throwaway data structure to facilitate the building of nested content
        my @levels = ( \@data );
    
        for my $html ($dom->all_contents->each) {
            if ($html->node eq 'comment') {
                # Start of Block - Go up to new level
                if ($html =~ m{^<!--\s*block:(.*)-->$}s) {
                    #print +('  ' x @levels) ."<$1>\n";  # For debugging
                    my $hash = {
                        block   => $1,
                        content => [],
                    };
                    push @{$levels[-1]}, $hash;
                    push @levels, $hash->{content};
                    next;
    
                # End of Block - Go down level
                } elsif ($html =~ m{^<!--\s*endblock\s*-->$}) {
                    die "Error: Unmatched endblock found before " . dump($html) if @levels == 1;
                    pop @levels;
                    #print +('  ' x @levels) . "</$levels[-1][-1]{block}>\n";  # For debugging
                    next;
                }
            }
    
            push @{$levels[-1]}, '' if !@{$levels[-1]} || ref $levels[-1][-1];
            $levels[-1][-1] .= $html;
        }
        die "Error: Unmatched start block: $levels[-2][-1]{block}" if @levels > 1;
    
        return \@data;
    }
    
    
    sub parse_using_split {
        my $content = shift;
    
        # Tokenize Content
        my @tokens = split m{<!--\s*(?:block:(.*?)|(endblock))\s*-->}s, $content;
    
        # Resulting Data Structure
        my @data = (
            shift @tokens, # First element of split is always HTML
        );
    
        # Keep track of levels of content
        # - This is a throwaway data structure to facilitate the building of nested content
        my @levels = ( \@data );
    
        while (@tokens) {
            # Tokens come in groups of 3.  Two capture groups in split delimiter, followed by html.
            my ($block, $endblock, $html) = splice @tokens, 0, 3;
    
            # Start of Block - Go up to new level
            if (defined $block) {
                #print +('  ' x @levels) ."<$block>\n"; # For Debugging
                my $hash = {
                    block    => $block,
                    content  => [],
                };
                push @{$levels[-1]}, $hash;
                push @levels, $hash->{content};
    
            # End of Block - Go down level
            } elsif (defined $endblock) {
                die "Error: Unmatched endblock found before " . dump($html) if @levels == 1;
                pop @levels;
                #print +('  ' x @levels) . "</$levels[-1][-1]{block}>\n"; # For Debugging
            }
    
            # Append HTML content
            push @{$levels[-1]}, $html;
        }
        die "Error: Unmatched start block: $levels[-2][-1]{block}" if @levels > 1;
    
        return \@data;
    }
    
    
    sub parse_using_regex {
        my $content = shift;
        my $href = {};
        ParseCore( $href, $content );
    
        return $href;
    }
    
    
    sub ParseCore
    {
        my ($aref, $core) = @_;
    
            # Set the error mode on/off here ..
        my $BailOnError = 1;
        my $IsError = 0;
    
        my ($k, $v);
        while ( $core =~ /(?is)(?:((?&content))|(?><!--block:(.*?)-->)((?&core)|)<!--endblock-->|(<!--(?:block:.*?|endblock)-->))(?(DEFINE)(?<core>(?>(?&content)|(?><!--block:.*?-->)(?:(?&core)|)<!--endblock-->)+)(?<content>(?>(?!<!--(?:block:.*?|endblock)-->).)+))/g )
        {
           if (defined $1)
           {
             # CONTENT
               $aref->{content} .= $1;
           }
           elsif (defined $2)
           {
             # CORE
               $k = $2; $v = $3;
               $aref->{$k} = {};
     #         $aref->{$k}->{content} = $v;
     #         $aref->{$k}->{match} = $&;
    
               my $curraref = $aref->{$k};
               my $ret = ParseCore($aref->{$k}, $v);
               if ( $BailOnError && $IsError ) {
                   last;
               }
               if (defined $ret) {
                   $curraref->{'#next'} = $ret;
               }
           }
           else
           {
             # ERRORS
               print "Unbalanced '$4' at position = ", $-[0];
               $IsError = 1;
    
               # Decide to continue here ..
               # If BailOnError is set, just unwind recursion. 
               # -------------------------------------------------
               if ( $BailOnError ) {
                  last;
               }
           }
        }
        return $k;
    }
    
    
    __DATA__
    some html content here top base
    <!--block:first-->
        <table border="1" style="color:red;">
        <tr class="lines">
            <td align="left" valign="<--valign-->">
        <b>bold</b><a href="http://www.mewsoft.com">mewsoft</a>
        <!--hello--> <--again--><!--world-->
        some html content here 1 top
        <!--block:second-->
            some html content here 2 top
            <!--block:third-->
                some html content here 3 top
                <!--block:fourth-->
                    some html content here 4 top
                    <!--block:fifth-->
                        some html content here 5a
                        some html content here 5b
                    <!--endblock-->
                <!--endblock-->
                some html content here 3a
                some html content here 3b
            <!--endblock-->
            some html content here 2 bottom
        <!--endblock-->
        some html content here 1 bottom
    <!--endblock-->
    some html content here1-5 bottom base
    
    some html content here 6-8 top base
    <!--block:six-->
        some html content here 6 top
        <!--block:seven-->
            some html content here 7 top
            <!--block:eight-->
                some html content here 8a
                some html content here 8b
            <!--endblock-->
            some html content here 7 bottom
        <!--endblock-->
        some html content here 6 bottom
    <!--endblock-->
    some html content here 6-8 bottom base
    

    具有3个嵌套块的简单模板的结果:

    Benchmark: timing 100000 iterations of mojo, regex, split...
          mojo: 50 wallclock secs (50.36 usr +  0.00 sys = 50.36 CPU) @ 1985.78/s (n=100000)
         regex: 14 wallclock secs (13.42 usr +  0.00 sys = 13.42 CPU) @ 7453.79/s (n=100000)
         split:  2 wallclock secs ( 2.70 usr +  0.00 sys =  2.70 CPU) @ 37050.76/s (n=100000)
    

    将正则表达式归一化为100%,相当于mojo为375%,并且分为20%。

    对于上面代码中包含的更复杂的模板:

    Benchmark: timing 100000 iterations of mojo, regex, split...
          mojo: 237 wallclock secs (236.61 usr +  0.02 sys = 236.62 CPU) @ 422.61/s (n=100000)
         regex: 46 wallclock secs (47.25 usr +  0.00 sys = 47.25 CPU) @ 2116.31/s (n=100000)
         split:  7 wallclock secs ( 6.65 usr +  0.00 sys =  6.65 CPU) @ 15046.64/s (n=100000)
    

    将正则表达式归一化为100%,相当于魔力为501%,分为14%。 (快7倍)

    速度是否重要?

    如上所述,我们可以毫无疑问地看到,我的split解决方案将比迄今为止的任何其他解决方案更快。这不应该是一个惊喜。它是一个非常简单的工具,因此速度很快。

    事实上,速度并不重要。

    为什么不呢?好吧,因为每次要执行模板时都可以缓存和重新加载从解析和验证模板构建的数据结构,直到模板更改为止。

    最终决定

    因为速度与缓存无关,所以你应该关注的是代码的可读性,代码的脆弱程度,扩展和调试的容易程度等等。

    尽管我很欣赏制作精良的正则表达式,但它们往往很脆弱。将所有解析和验证逻辑放在一行代码中只会遇到麻烦。

    留下拆分解决方案或mojo。

    如果你像我描述的那样进行缓存,你实际上可以毫无顾虑地选择其中任何一个。我为每个代码提供的代码基本上是相同的,只是略有变化,因此它是个人偏好。即使分割比初始解析更快20-35倍,也比使用实际HTML解析器更容易维护代码要少。

    祝你选择最后的方法。我仍然会用手划过你,有一天会和TT一起去,但你会选择自己的毒药:)