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



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

这是Perl

"构建链"中的几个步骤之一,因此在Perl中实现炒作链接也是理想的。

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

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

上面的例子意味着,无论"关键字"出现在源 markdown 文档中的任何地方,都应该用 markdown 片段"(关键字)[#heading-to-jump-to]"代替。

忽略作为其他关键字的

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

我只需要匹配关键字的实例,这些实例是:

  • 不在行上不以 #
  • 开头
  • 不是最直接的下面 标题跳到

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

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

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

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

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

我已经阅读了一堆关于如何解决可变长度回溯的其他文章,但我似乎无法针对我的特定情况进行正确的处理。任何常驻正则表达式向导可以帮助处理将在 Perl 中执行的更整洁的正则表达式吗?

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

我会建议一些更简单的东西。使用哈希来存储替换,使用字边界来防止部分匹配,使用修饰符/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/bQ$kwEb/$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-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列表以及何时使用它。您将不得不做出这些决定,因为我不知道您的数据。

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

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

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

最新更新