Wait the light to fall

正则表达式和猜测(名称提取)

焉知非鱼

img

在我的上一篇文章中,我解释了我为生活所做的事情,并举例说明了我在工作中使用 Raku 的特殊内容。 这是另一个例子。

首先是一些背景信息:挪威是一个小国家,大多数人都讲同一种语言(当然,方言有所不同,但语言是相同的)。但听起来很疯狂……这种单一的口语有两种书面语言。是的,你读的是正确的:你说挪威语,但你写的是 Bokmål 或 Nynorsk - 松散地翻译成 “Book language” 和 “New Norwegian”。我不会深入了解其中的历史原因 - 如果你有兴趣我会推荐关于它的维基百科文章。我只想说这两个版本的挪威语非常相似,但差异足以让差异显而易见。

一些新闻媒体使用 Nynorsk,而大多数人使用 Bokmål。这样做的结果是,Bokmål 读者的内容远远多于 Nynorsk 读者。这就是为什么我的工作场所试图获得将 Bokmål 翻译成 Nynorsk 的软件的原因。

翻译工作得很好,但偶尔会有不幸。其中相当一部分是由译者误解的名字引起的。我们有字典的字典,所以它们应该很容易识别。但是,如果名称是较少使用的名称,则不容易识别。当字典失败时,我们可以尝试使用规则/正则表达式来识别它们。但即便如此,我们也无法总是决定一个单词是常规单词还是名称。

让我们来看看 Fuglesang 这个名字(即宇航员)。 Fuglesang 是一个名字,但它也是一个名词,意思是鸟的歌或唧唧。如上所述,如果使用正则表达式或使用名称字典在句子中间发生 Fuglesang 作为名称,则没有问题。正则表达式查找以大写字母开头的单词,并向译者发出该单词可能是名称的信号。但如果这个词出现在一个句子的开头 - 所有单词都被大写 - 那么 Fuglesang 就会变得模棱两可,即使这个名字出现在名字词典中。消除这种歧义很重要,因为如果没有,名称可以翻译为好像是任何其他单词。在我的情况下,由于不公正,Fuglesang 可能被翻译成 “fuglesong”,如果它是任何其他类型的单词而不是名称,这是正确的。

那么 Fuglesang 是什么?一首名字还是鸟鸣?

显然,这是翻译软件需要帮助的情况。我想了一会儿,并决定也许我们可以开发一个相当天真的算法来帮助我们。我编写了一个简单的脚本:它解析一个文本并找到句子中出现的每个大写单词。然后它提取任何句子的第一个单词并检查该单词是否出现在句子中间单词列表中。如果确实如此,那么我们假设 - 在该单个文本的特定上下文中 - 该单词可以被视为名称。

我必须照顾的唯一特殊情况,与所有格有关。在英语中,所有格相对容易处理,因为它们很容易被发现和移除。假设你有两句 “Peter owns a car” 和 “This is Peter’s car”。一个简单的 Str.subst("'s", "") 将删除后一句中的所有格,现在 Peter 可以在第一句中与 Peter 相比较。

但是在挪威,所有格只是在没有撇号的情况下拖尾s-es,因此答案并不那么简单(“Peter eier en bil” vs “Peters bil”)。但总而言之,使用 Raku 解决这个问题也相当简单。

因此生成的脚本可能很简单,但确实很有用。在任何情况下,我都会在这里分享脚本。它展示了我在本系列文章前面讨论过的很多概念。我将进一步深入研究细节,但首先是代码。

#!/usr/bin/env raku
my $text = prep-text(slurp);

my regex sentence-begin {
    ^^ | \ <space> + | \n <space> *
}

my regex probably-name {
    <upper> <alnum> +
}

my regex sentence-delimiter {
    <[ \n \. \: \? \! \ \- ]> ' ' *
}

my regex strip-characters {
    <[ \» \« ]>
}

my @in-sentence = ( $text ~~   m:g/
      <<
      ( <!after  <sentence-begin> > )
      ( <probably-name> )
      >>
      /
).map( *.Str ).unique.sort;

my @begin-sentence = ( $text ~~ m:g/
       <<
       ( <after <sentence-begin> > )
       ( <probably-name> )
       >> 
       /
).map( *.Str ).unique.sort;

my @probably-names = @in-sentence.grep( -> $a { 
     @begin-sentence.grep( -> $b { is-equal($a, $b) } ) 
} );

say @probably-names.unique.join("\n");

sub is-equal($a is copy, $b is copy) {
    $a ~~ s/s$//; # Possessives
    $b ~~ s/s$//; 
    return $a eq $b;
}

sub prep-text($t is copy) {
    $t ~~ s:g/ <strip-characters> //;
    return $t.split( / <sentence-delimiter> /).join(".\n");
}

代码从 slurp 开始。 Slurp 适用于任何文件句柄或隐式在 stdin 上。这个接收通过管道传递给脚本的文本,并将其传递给 prep-text 子例程。例程移除几个字符。此外,例程删除一组自定义标点字符,并将其替换为 “.“(点+空格)。 Raku 内置了字符类 <punct>,但包含我想保留的字符。因此,我创建了自己的 <sentence-delimiter> 命名正则,它定义了一组特定的字符。

prep-text 的第二件事是重新格式化文本,以便每个句子自己放在一行上。这样做可以使解析的其余部分更容易。稍后会详细介绍。

在准备文本之后,我还声明了更多命名正则表达式。我本可以将所有这些内容写入我进一步使用的正则表达式中,但是将批量分解为命名正则表达式可以提高可读性。

@in-sentence 数组是一个大写单词列表,它不是句子的第一个单词。我的假设是这个列表中的大多数单词都是名称(挪威语不使用大写,通常只用于专有名词,因此大多数情况下假设可能是正确的)。正则表达式返回Match 对象列表。由于我以后必须比较字符串,我使用 map 方法将 Match 对象转换为字符串。最后我在列表上运行 unique.sort。唯一确保我们只获得每个大写单词的一个表示。至于排序,我本可以没有它。但我喜欢我的输出是按字母顺序排列的。这对我来说足够了。

@begin-sentence 数组也是大写单词的列表,但现在大写单词出现在句子的第一个单词中。句子开头的所有单词,无论它们是否是名称(它们可能不是),都是大写的。所以在这里我们无法确定任何事情。

这就是第三个数组发挥作用的地方。在这里,我使用 grep 和 匿名函数来保存 @in-sentence 数组中的所有名称,这些数组也出现在 @begin-sentence 数组中。我剩下的是一系列在句子内部和句子开头都被大写的单词。这些词很有可能是名字。

几乎所有这一切都不是因为最内层的匿名函数调用名为 is-equal 的子例程。我本可以做一个简单的 $a eq $b 来找到相等的东西。但是,由于名字可以拥有所有格(“Peter’s car” 在挪威语中写为 “Peters bil”),我必须在检查相等之前采取额外的步骤。

因此,当单词A和单词B被发送到 is-equal 时,我使用一个简单的正则表达式来删除单词中的最后一个s(如果有的话)。然后我比较两个并返回 True 或 False。

我们最终得到的是在句子开头出现的单词列表,可能是名称。干得好。

所以…这是一个你永远不会偶然发现的场景,那你为什么要关心这段代码呢?

嗯,我认为这是一个展示不同的 Raku 正则表达式如何与好老的 perlre 相比较。它们并没有因为差异而有所不同,但实际上是可读的。此外,他们还有一些功能,而 perlre 则没有。命名正则表达式是其中的核心。它们使主要的正则表达式简短易读。

另外我喜欢用于创建 @might-names 数组的双 grep。在其中使用匿名函数有助于避免嵌套 for 循环。并不是说我有任何反对他们的东西,只是这种方式更简短,更简洁。我也有一点希望使用 Raku 内置插件也更快,尽管在这种情况下速度并不是真正的问题。

至于匿名函数,我很高兴我找到了它们的用途。我在第一篇关于 Raku 的文章中提到过它们。当时我不确定它们是否有用或只是给你的朋友留下深刻印象。民谣歌手伍迪格思里曾经说过“任何使用超过两个和弦的人都只是炫耀”。我认为匿名功能可能是 Raku 相当于 Woody 的 3+ 和弦。我很高兴地报告他们不是。它们本身就很有用。

结论:此脚本以有用的方式利用了早期文章中的以下概念:

  • 新的 Raku 正则表达式语法
  • 命名正则表达式
  • map 和 grep
  • 匿名函数的使用

我不仅使用 Raku 开始变得有用,它也变得更有趣。希望你有相同的经历。