没有可变长度的Perl正则表达式?

时间:2013-09-09 12:10:05

标签: regex perl lookbehind variable-length

我正在尝试在50,000字的降价文档中超链接400个左右的关键字。

这是Perl“构建链”中的几个步骤之一,因此在Perl中实现超链接也是理想的。

我有一个单独的文件包含所有关键字,并将每个关键字映射到应该替换的markdown片段,如下所示:

keyword::(keyword)[#heading-to-jump-to]

上面的例子暗示,在源标记文档中出现“关键字”的地方,它应该用降价片段“(关键字)[#heading-to-jump-to]”替换。

忽略作为其他关键字,复数/单数形式和模糊关键字的子串出现的关键字,这是相当简单的。但自然,还有两个额外的限制。

我只需要匹配以下关键字的实例:

  • 不在线上#
  • 不是最直接跳到标题下

这些的简单英语含义是:不匹配任何标题中的关键字,也不要替换链接到标题下的关键字。

My Perl脚本读取$ keyword :: $链接对,然后逐对配对,将它们替换为正则表达式,然后使用该正则表达式搜索/替换该文档。

我编写了一个正则表达式,使用Regex Buddy的JGSoft正则表达式实现进行匹配(对于我目前为止手动测试的情况)。它看起来像这样:

Frog::(Frog)[#the-frog)
-->    
([Ff]rog'?s?'?)(?=[\.!\?,;: ])(?<!#+ [\w ]*[Ff]rogs?)(?<!#+ the-frog)(?<!#+ the-frog[^#]*)

问题(或者,可能是 问题),它使用Perl不支持的可变长度回顾。所以我甚至无法在完整文档上测试这个正则表达式,看它真的是否有效。

我已经阅读了一些关于如何解决可变长度回顾的其他帖子,但我似乎无法为我的特定案例做到正确。任何常驻正则表达式向导都可以帮助使用在Perl中执行的更整洁的正则表达式吗?

2 个答案:

答案 0 :(得分:2)

这是一个可怕的正则表达式。我不想成为一个坚持维护它的可怜的傻瓜。另外,您是如何从替换模板生成的?

我会建议一些相当简单的东西。使用散列来存储替换,使用单词边界来防止部分匹配,使用/i修饰符来匹配不区分大小写,并使用常规循环逻辑来避免替换注释行。

use strict;
use warnings;

my @kw = "keyword::(keyword)[#heading-to-jump-to]";
my %rep = map { /([^:]+)::(.+)/ } @kw;
while (<DATA>) {
    next if /^#/;
    for my $kw (keys %rep) {
        s/\b\Q$kw\E\b/$rep{$kw}/ig;
    }
} continue {
    print;
}

__DATA__
This is a text with keywords. Only the keyword 'keyword' should be replaced.
# Dont replace keyword when in a comment

<强>输出:

This is a text with keywords. Only the (keyword)[#heading-to-jump-to] '(keyword)
[#heading-to-jump-to]' should be replaced.
# Dont replace keyword when in a comment

<强>解释

  • 使用map语句创建替换关键字的哈希值,该语句为每个关键字::替换字符串返回两个元素列表。
  • 对于以#开头的行,请直接跳至print
  • 对于散列中的每个关键字,请在每一行上执行全局/g,不区分大小写的/i替换。使用单词边界\b来阻止部分匹配,并使用\Q ... \E引用元字符。用该关键字的哈希值替换。

与所有语言处理一样,这将有一些需要处理的警告和边缘情况。例如,字边界将替换foo中的foo-bar。至于如何控制在哪个标题下不替换什么,你首先要告诉我如何识别标题。

<强>更新

如果我理解正确,你在段落中用自己的标题跳过关键字是什么意思,就像这样:

#heading-to-jump-to
Here is 'keyword' not replaced

查找字符串#heading-to-jump-to并从替换列表中删除keyword

您可以使用查找哈希,其中键是标题引用,并将其与第一个哈希的生成相结合。虽然,在这种情况下,我会开始担心每个链接可以有多个关键字,例如foobar都指向#foobar,因此#foobar应排除关键字foobar

my %rep;
my %heading;

for my $str (@kw) {
    chomp $str;
    my ($kw, $rep) = split /::/, $str, 2;  # split into 2 fields
    $rep{$kw} = $rep;
    my ($heading) = $rep =~ /\[([^]]+)\]/;
    push @{ $heading{$heading} }, $kw;
}

然后,不要简单地用next跳过一行,而是执行类似

的操作
my @kws = keys %rep;   # default list
while (<DATA>) {
    if (/^(#.+)/) {    # inside heading
        my %exclude = map { $_ => 1 } @{ $heading{$1} };
        @kws = grep { ! $exclude{$_} } @kws;
    } else {
        # not in a heading
        # ...
    }
}

请注意,这只是原理的演示,而不是作为工作代码。正如您所看到的,这里棘手的部分是知道何时重置@kws的有限列表以及何时使用它。你必须做出这些决定,因为我不知道你的数据。

答案 1 :(得分:1)

在我看来,你的程序将有三种状态:

  1. 标题。
  2. 在标题后面的一段中。
  3. 在其他段落中。
  4. 因为这大致是一种常规语言,所以可以由正则表达式解析。但是,为什么我们要这样做,考虑到我们需要400遍文本?

    将文件拆分成段落数组可能更容易。当我们达到标题时,我们会生成可以指向那里的所有链接。然后在下一段中,我们替换除禁用之外的所有关键字。 E.g:

    my %substitutions = ...;
    my $kw_regex = ...;
    my %forbidden; # holds state
    
    local $/ = ""; # paragraph mode
    while (<>) {
      if (/^#/) {
        # it's a headline
        @forbidden{ slugify($_) } = ();  # extract forbidden link(s)
      } else {
        # a paragraph
        s{($kw_regex)}{
          my $keyword = $1;
          my $link = $substitutions{lc $keyword};
          exists $forbidden{$link} ? $keyword : "($keyword)[$link]";
        }eg;
        %forbidden = (); # forbidden links only in 1st paragraph after headline
      }
      print;
    }
    

    如果无法保证标题与段落分隔为空行,那么paragrapg模式将无效,您将不得不自己动手。

    正则表达式很棒,但它们并不总是一个适当的工具。