韩林涛老师:译者编程知识30天×300字写作计划 | Day 16

在《如何基于SRX将英文文本切分成句(一)》中我们介绍了什么是SRX以及基于SRX进行文章断句的原理。在这篇帖子中我们将重点探讨如何用程序来实现英文文本的切分。

准备工作

本文将基于PHP来开发文本切分断句工具,所以需要提前启动编程环境XAMPP、PHPStudy或MAMP Pro,在过去的文章已有详细介绍。

编程环境启动后,在根目录下创建一个空白文件夹,命名为textsplit,并在其中新建index.php文件,用于撰写代码,如下图:

一、PHP的正则表达式相关函数在断句中的基本应用

在PHP中有诸多内置的正则表达式相关函数,在文本切分方面有重要作用。

我们选择两句话作为测试用例:

Welcome Dr. Liu and Dr. Lu to BLCU. It's our pleasure.

在这段话中共有两句话,对断句造成主要影响的就是“Dr.”。

1)preg_match()

我们首先看preg_match()函数的作用:

<?php
$text = "Welcome Dr. Liu and Dr. Lu to BLCU. It's our pleasure.";

echo preg_match("/Dr\./",$text,$matches);

echo "<br>";

var_dump($matches);
?>

代码运行结果为:

1
array(1) {
  [0]=>
  string(3) "Dr."
}

在这个测试中我们发现,虽然文本中出现了两次“Dr.”,但$matches变量中只出现了一次“Dr.”,这与preg_match()函数的特性有关。

在上例中,preg_match()函数中共有三个参数,分别是:

"/Dr./":Dr.是一个正则表达式,用于匹配“Dr.”,在preg_match()函数中我们需要用两个“/”将正则表达式包裹。

$text:这个参数是我们要匹配的变量。

$matches:这个参数是一个数组,用于存储正则表达式匹配到的第一个结果,所以使用var_dump()函数去读取其内容时会发现该数组中只有一个元素,元素中的内容是:“Dr.”。

整个函数运行完成后会返回0或1,如果返回1就表明所用的正则表达式匹配到了结果。

由此可见,我们可以用preg_match()函数来判断某个正则表达式能否在文本中匹配到内容,而不关心匹配到了多少内容:

<?php
$text = "Welcome Dr. Liu and Dr. Lu to BLCU. It's our pleasure.";

if(preg_match("/Dr\./",$text,$matches))
{
    echo "Match found";
}else{
    echo "Match not found";
}
?>

以上代码运行的结果就是:Match found。

2) preg_match_all()

我们再来看preg_match_all()函数:

<?php
$text = "Welcome Dr. Liu and Dr. Lu to BLCU. It's our pleasure.";

echo preg_match_all("/Dr\./",$text,$matches);

echo "<br>";

var_dump($matches);
?>

代码运行结果为:

2
array(1) {
  [0]=>
  array(2) {
    [0]=>
    string(3) "Dr."
    [1]=>
    string(3) "Dr."
  }
}

preg_match_all()接收的参数与preg_match()一样,但输出的结果并不一样。整个函数输出的是:正则表达式总共匹配到了多少结果,而所有的结果则以数组的形式存储在$matches变量中。

相比两个函数,对断句更有帮助的是实际上是preg_match(),因为我们在使用正则表达式确定断点时,实际上是先去判断有没有断点,而不是有多少断点。

那么判断到有断点时应该怎么办呢?

3)preg_replace()

preg_replace()函数的作用是借助正则表达式进行文本的替换,如下:

<?php
$text = "Welcome Dr. Liu and Dr. Lu to BLCU. It's our pleasure.";

if(preg_match("/Dr\./",$text,$matches))
{
    echo preg_replace("/(Dr\.)/","$1\n",$text);
}
?>

以上代码运行结果为:

Welcome Dr.
 Liu and Dr.
 Lu to BLCU. It's our pleasure.

看到这个结果后,你可能会想究竟发生了什么才会使得两个“Dr.”后面都出现了换行。显然这不是我们想看到的结果,因为我们希望在“BLCU.”后面换行,而不是“Dr.”后面。

确实如此,但我们通过这个例子可以知道preg_replace()函数起到了替换的作用,在第6行代码中,preg_replace()函数接受了三个参数,分别是:

"/(Dr.)/":这是一个正则表达式,我们用圆括号()将正则表达式“Dr.”包裹住,目的是将其视为一个整体,而这个整体可以用“$1”来表示。

"$1\n":这是用于替换的表达式,$1表示的就是前面的正则表达式匹配到的结果,\n是一个换行符,连在一起的效果就是:在Dr.后面加一个换行符。

$text:正则表达式要匹配的目标文本。

整个preg_replace()函数运行后输出的是一个字符串,这个字符串就是完成了替换操作的字符串。

即便我们知道了原理,但其实也并没有解决断句的问题,所以我们还需要再次调整代码:

<?php
$text = "Welcome Dr. Liu and Dr. Lu to BLCU. It's our pleasure.";

$result = preg_replace("/\./",".\n",$text);

$result = preg_replace("/(Dr)(\.)(\s)/","$1$2",$result);

$final = explode("\n",$result);

foreach($final as $f)
{
    echo $f."<br>";
}
?>

以上代码输出的结果是:

Welcome Dr. Liu and Dr. Lu to BLCU.
It's our pleasure.

如下图所示:

似乎我们的代码更复杂了,但至少结果是符合预期的。

第4行代码是:

$result = preg_replace("/\./",".\n",$text);

这里的"/./"匹配的是所有的句号,注意,是所有的句号。".\n"表示将所有的句号替换成一个“.”加上一个换行符。

这一步完成后,即便是“Dr.”里的句号也会替换成一个句号加上一个换行符。

所以第6行代码的作用就是:纠偏

$result = preg_replace("/(Dr)(\.)(\s)/","$1$2",$result);

"/(Dr)(.)(\s)/"匹配的是:Dr、句号、一个空白字符,我们用圆括号分别将它们三者包裹起来,可以用$1、$2、$3分别表示。

因为在第4行中我们在Dr.后面新增了一个换行符,所以这里的"(\s)"实际上匹配到的就是这个换行符,也就是$3。

我们在替换时只用了$1$2,这就表明我们保留了“Dr.”,但没有保留后面的换行符,这就间接将之前第4行添加的换行符给去掉了。

有读者看到这里可能会疑惑:\s不是表示的空格吗,怎么这里又能表示换行符了? 
这是因为在正则表达式中,\s 是匹配所有空白符,包括换行,而不仅仅是空格。
参考:https://www.runoob.com/regexp/regexp-syntax.html

第6行代码运行完成后,实际上断句也没有完成,因为这个时候我们只是在“BLCU.”后面和“pleasure.”后面还保留了两个换行符“\n”。

因此我们使用了一个explode()函数,这个函数可以基于某个特定的符号将文本切分成数组,所以第8行代码就起到了这样的作用。

如果用var_dump()函数将$final变量呈现出来,内容如下:

array(3) {
  [0]=>
  string(35) "Welcome Dr. Liu and Dr. Lu to BLCU."
  [1]=>
  string(19) " It's our pleasure."
  [2]=>
  string(0) ""
}

这个时候用一个foreach循环就能把两个句子分别读取出来,进而完成了断句操作。

以上就是PHP中我们常用到的用于应用正则表达式的函数。

二、从“.srx”文件中读取正则表达式断句规则

如果一篇英文文本中没有那么多干扰断句的标点符号,那就不存在SRX标准了,但事实上英文文本中有非常多的断句干扰,所以本地化专家们整理了一整套的断句规则,覆盖多种语言。

目前在网上能够找到许多断句规则,大家平时可能以为像Trados、memoQ这种主流的计算机辅助翻译工具中能够直接下载到这些规则,但仔细去看才发现他们要么是将规则内嵌到了工具中,要么是使用了自己的特定格式的断句规则文件,所以我们在网上找到了开源计算机辅助翻译工具OmegaT的断句规则,链接如下:

https://github.com/omegat-org/omegat/blob/master/src/org/omegat/core/segmentation/defaultRules.srx

其中从3800行开始是针对英文的断句规则:

如果我们希望基于“.srx”文件来断句,那么就需要将这些断句规则读取出来。考虑到“.srx”文件是基于XML的,所以我们使用了simplexml_load_file()函数,如下:

<?php
$xml = simplexml_load_file("defaultRules.srx");

$english_rules = $xml->body->languagerules->languagerule[3]->rule;

echo "<table border='1'><th>BeforeBreak</th><th>AfterBreak</th>";

foreach($english_rules as $rule)
{
    echo "<tr><td>".$rule->beforebreak."</td><td>".$rule->afterbreak."</td></tr>";
}

echo "</table>";
?>

运行效果如下:

如果之前仔细阅读过《译者编程入门指南》中“.tmx”格式文件读取的方法,这里就不难理解我们是如何将全部正则表达式读取出来的了,所以在本帖中就不再详细介绍。

我们现在既然已经将所有的用于英文断句的正则表达式找到,那么拿到一个文本后就可以考虑用这些表达式来协助我们断句。

三、使用“.srx”中的正则表达式将英文文本切分成句

为了演示方便,我们依然使用之前的测试例句:

<?php

$text = "Welcome Dr. Liu and Dr. Lu to BLCU. It's our pleasure.";

$result = preg_replace("/\./",".\n",$text);

$xml = simplexml_load_file("defaultRules.srx");

$english_rules = $xml->body->languagerules->languagerule[3]->rule;

foreach($english_rules as $rule)
{
    $before = $rule->beforebreak;

    $after = $rule->afterbreak;

    $result = preg_replace("/(".$before.")(\s)(".$after.")/","$1$3",$result); 
}

$final = explode("\n",$result);

foreach($final as $f)
{
    echo $f."<br>";
}
?>

以上代码的运行结果如下:

可见,我们将两部分代码组合后,依然可以对这段文本进行断句。

这段代码中我们可能会比较疑惑的是第13到17行。这里我们选取的策略是:

将“.srx”文件中断点前规则和断点后规则组合到一起让preg_replace()函数去处理,如果找到了误增的换行符,就把这个换行符去掉,这样就不会在断点处切分了。大家要特别注意我们在“.srx”文件中提取到的断句规则的“break”属性值都是“no”,这意味着我们在上面的例子中选择的正则表达式断句规则都是告诉程序不要切分句子的。

问题

通过以上代码的分析,我们知道了如何基于SRX将英文文本切分成句,但本文只是介绍的基本原理,在实际使用中还是有很多问题,比如这个句子:

The U.K. Prime Minister, Mr. Blair, was seen out with his family today.

断句结果为:

聪明的你可以想想这个问题是如何产生的,以及如何解决这个问题。

本文原文《如何基于SRX将英文文本切分成句(二)》发表于韩老师的公众号《简言》。

作者:韩林涛,北京语言大学高级翻译学院教师,《译者编程入门指南》作者