1

我正在尝试在 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 不支持的可变长度回溯。所以我什至无法在完整文档上测试这个正则表达式,看看它是否真的有效。

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

4

2 回答 2

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。替换为该关键字的哈希值。

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

更新:

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

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

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

您可以使用键作为标题引用的查找散列,并将其与第一个散列的生成相结合。虽然,在这种情况下,我会开始担心每个链接可以有多个关键字,例如 bothfoobarpoint to #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以及何时使用它。你必须做出这些决定,因为我不知道你的数据。

于 2013-09-09T12:44:35.707 回答
1

如我所见,您的程序将具有三种状态:

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

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

于 2013-09-09T12:41:15.203 回答