我正在尝试在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中执行的更整洁的正则表达式吗?
答案 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
。
您可以使用查找哈希,其中键是标题引用,并将其与第一个哈希的生成相结合。虽然,在这种情况下,我会开始担心每个链接可以有多个关键字,例如foo
和bar
都指向#foobar
,因此#foobar
应排除关键字foo
和bar
。
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)
在我看来,你的程序将有三种状态:
因为这大致是一种常规语言,所以可以由正则表达式解析。但是,为什么我们要这样做,考虑到我们需要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模式将无效,您将不得不自己动手。
正则表达式很棒,但它们并不总是一个适当的工具。