【问题标题】:Perl regex without variable length lookbehind?Perl 正则表达式没有可变长度的lookbehind?
【发布时间】:2013-09-09 12:10:05
【问题描述】:

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

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

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

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

上面的例子暗示了源 Markdown 文档中出现“keyword”的任何地方,都应该用 markdown 片段“(keyword)[#heading-to-jump-to]”代替。

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

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

  • 不在未开始的行上#
  • 不是最直接在要跳转到的标题下方

这些简单的英文意思是:不要匹配任何标题中的关键字,也不要替换它们将链接到的标题下的关键字。

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

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

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

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

我已经阅读了许多关于如何解决可变长度回溯的其他帖子,但我似乎无法针对我的特定情况做出正确的选择。任何常驻 regex 向导都可以帮助提供将在 Perl 中执行的更整洁的 regex 吗?

【问题讨论】:

    标签: regex perl lookbehind variable-length


    【解决方案1】:

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

    我会建议一些相当简单的东西。使用哈希存储替换,使用单词边界防止部分匹配,使用/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 语句创建替换关键字的哈希,该语句为每个keyword::replacement 字符串返回一个包含两个元素的列表。
    • #开头的行,直接跳到print
    • 对于散列中的每个关键字,在每一行上执行一个全局/g,不区分大小写的/i 替换。使用单词边界\b 防止部分匹配,并使用\Q ... \E 引用元字符。替换为该关键字的哈希值。

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

    更新:

    如果我理解正确,您在带有自己标题的段落中跳过关键字的意思是这样的:

    #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 的有限列表以及何时使用它。你必须做出这些决定,因为我不知道你的数据。

    【讨论】:

    • 感谢您的详细回答 TLP,我会尝试您的建议。在回答如何识别标题时,任何以 # 开头的行都是 markdown 中的标题(不是注释)。
    • 仍在消化这个,但似乎它不满足第二个约束,即:如果我们刚刚跳过的标题是“heading-to-jump-”,则不应匹配 $kw到" $rep 的一部分。你应该是可行的......
    • @Tode 您的意思是说您需要检查以# 开头的行,看看它是否包含每个关键字替换中的部分字符串,例如#heading-to-jump-to,然后不在里面进行替换?那么,在这种情况下,您需要一个反向哈希,并提取 #heading-... 关键字。
    • 但是没有一些示例数据,这只是对如何最好地实现它的猜测。
    【解决方案2】:

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

    1. 在标题中。
    2. 在标题之后的段落中。
    3. 在其他段落中。

    因为这大致是一种正则语言,它可以被正则表达式解析。但是,考虑到我们需要对文本进行 400 次传递,我们为什么要这样做呢?

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

    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 模式将不起作用,您必须自己滚动。

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

    【讨论】:

    • 感谢 amon,您列出的三个状态在技术上可能是正确的,但状态 2 对我来说并不重要。重要的是:如果该关键字出现在该关键字应链接到的标题正下方的段落中,则不应匹配该关键字。链接到的标题不一定包含关键字本身(尽管它经常可能)。标题不必总是用空行与以下段落分开(它们大多不是)。
    • @Tode 我更新了我的帖子以反映禁止出现链接,而不是关键字出现。我没有扩展我的答案以将标题与段落分开,因为这不是您问题的核心问题(它可以通过仅打印标题部分来解决,而redoing 与其余部分一起循环,这被认为是一段)。
    • 谢谢@amon。我想我可以使用您的解决方案的改编版,逐行进行(因为标题下可以有多个段落),并且每次遇到新的标题行时才清除“禁止”链接。
    猜你喜欢
    • 2014-11-04
    • 2012-07-23
    • 1970-01-01
    • 1970-01-01
    • 2020-10-17
    • 2015-10-12
    • 2012-02-08
    • 2013-06-22
    相关资源
    最近更新 更多