使用正则表达式将字符串拆分为句子
nlp
php
4
0

我在$sentences存储了随机文本。我想使用正则表达式将文本拆分为句子,请参见:

function splitSentences($text) {
    $re = '/                # Split sentences on whitespace between them.
        (?<=                # Begin positive lookbehind.
          [.!?]             # Either an end of sentence punct,
        | [.!?][\'"]        # or end of sentence punct and quote.
        )                   # End positive lookbehind.
        (?<!                # Begin negative lookbehind.
          Mr\.              # Skip either "Mr."
        | Mrs\.             # or "Mrs.",
        | T\.V\.A\.         # or "T.V.A.",
                            # or... (you get the idea).
        )                   # End negative lookbehind.
        \s+                 # Split on whitespace between sentences.
        /ix';

    $sentences = preg_split($re, $text, -1, PREG_SPLIT_NO_EMPTY);
    return $sentences;
}

$sentences = splitSentences($sentences);

print_r($sentences);

工作正常。

但是,如果存在unicode字符,它不会分成句子:

$sentences = 'Entertainment media properties. Fairy Tail and Tokyo Ghoul.';

或这种情况:

$sentences = "Entertainment media properties.&Acirc;&nbsp; Fairy Tail and Tokyo Ghoul.";

当文本中存在Unicode字符时,我该怎么做才能使其正常工作?

这是一个测试的ideone

赏金信息

我正在寻找一个完整的解决方案。在发布答案之前,请阅读我与WiktorStribiżew在一起的评论主题,以获取有关此问题的更多相关信息。

参考资料:
Stack Overflow
收藏
评论
共 5 个回答
高赞 时间 活跃

如果空格不可靠,则可以在上使用match .后跟任意数量的空格, 后跟一个大写字母

您可以使用Unicode字符属性 \p{Lu}匹配任何大写UTF-8字母。

您只需要排除倾向于使用自己的名字(人名,公司名称等)的缩写,因为它们以大写字母开头。

function splitSentences($text) {
    $re = '/                # Split sentences ending with a dot
        .+?                 # Match everything before, until we find
        (
          $ |               # the end of the string, or
          \.                # a dot
          (?<!              #  Begin negative lookbehind.
            Mr\.            #   Skip either "Mr."
          | Mrs\.           #   or "Mrs.",
                            #   or... (you get the idea).
          )                 #   End negative lookbehind.
          "?                #   Optionally match a quote
          \s*               #   Any number of whitespaces
          (?=               #  Begin positive lookahead
            \p{Lu} |        #   an upper case letter, or
            "               #   a quote
          )
        )
        /iux';

    if (!preg_match_all($re, $text, $matches, PREG_PATTERN_ORDER)) { 
        return [];
    }

    $sentences = array_map('trim', $matches[0]);

    return $sentences;
}

$text = "Mr. Entertainment media properties. Fairy Tail 3.5 and Tokyo Ghoul.";
$sentences = splitSentences($text);

print_r($sentences);

注意:此答案可能不足以适合您的情况。我无法判断。它确实解决了上述问题,并且很容易理解。

收藏
评论

不出所料,任何自然语言处理都不是一件容易的事。其原因是它们是进化系统。没有一个人坐下来思考哪些是好主意,哪些不是。每个规则都有20-40%的例外。话虽这么说,但可以进行出价的单个正则表达式的复杂性已超出了图表。尽管如此,以下解决方案仍主要依靠正则表达式。


  • 这个想法是逐步遍历全文
  • 在任何给定时间, 文本的当前块将包含在两个不同的部分中。一个是在句子边界之前的子字符串的候选对象,另一个是在之后的子字符串的候选对象。
  • 前10个正则表达式对检测看起来像句子边界但实际上不是的位置。在这种情况下, 不注册新句子的情况下前进和之后
  • 如果这些对均不匹配,则将尝试与最后3对进行匹配,可能会检测到边界。

至于这些正则表达式来自哪里? -我翻译了基于本文生成的Ruby库 。如果您真的想了解它们,除了阅读本文之外别无选择。

就准确性而言,我建议您使用其他文字进行测试。经过一些试验,我感到非常惊喜。

在性能方面-正则表达式应该具有很高的性能,因为它们都具有\A\Z锚,几乎没有重复量词,并且在有的地方-不能有任何回溯。尽管如此,正则表达式还是正则表达式。如果您打算对大量文本使用紧密循环,则必须进行一些基准测试。


强制性免责声明 :原谅我生锈的PHP技能。以下代码可能不是有史以来最惯用的php,但仍然应该足够清楚,以使您明白这一点。


function sentence_split($text) {
    $before_regexes = array('/(?:(?:[\'\"„][\.!?…][\'\"”]\s)|(?:[^\.]\s[A-Z]\.\s)|(?:\b(?:St|Gen|Hon|Prof|Dr|Mr|Ms|Mrs|[JS]r|Col|Maj|Brig|Sgt|Capt|Cmnd|Sen|Rev|Rep|Revd)\.\s)|(?:\b(?:St|Gen|Hon|Prof|Dr|Mr|Ms|Mrs|[JS]r|Col|Maj|Brig|Sgt|Capt|Cmnd|Sen|Rev|Rep|Revd)\.\s[A-Z]\.\s)|(?:\bApr\.\s)|(?:\bAug\.\s)|(?:\bBros\.\s)|(?:\bCo\.\s)|(?:\bCorp\.\s)|(?:\bDec\.\s)|(?:\bDist\.\s)|(?:\bFeb\.\s)|(?:\bInc\.\s)|(?:\bJan\.\s)|(?:\bJul\.\s)|(?:\bJun\.\s)|(?:\bMar\.\s)|(?:\bNov\.\s)|(?:\bOct\.\s)|(?:\bPh\.?D\.\s)|(?:\bSept?\.\s)|(?:\b\p{Lu}\.\p{Lu}\.\s)|(?:\b\p{Lu}\.\s\p{Lu}\.\s)|(?:\bcf\.\s)|(?:\be\.g\.\s)|(?:\besp\.\s)|(?:\bet\b\s\bal\.\s)|(?:\bvs\.\s)|(?:\p{Ps}[!?]+\p{Pe} ))\Z/su',
        '/(?:(?:[\.\s]\p{L}{1,2}\.\s))\Z/su',
        '/(?:(?:[\[\(]*\.\.\.[\]\)]* ))\Z/su',
        '/(?:(?:\b(?:pp|[Vv]iz|i\.?\s*e|[Vvol]|[Rr]col|maj|Lt|[Ff]ig|[Ff]igs|[Vv]iz|[Vv]ols|[Aa]pprox|[Ii]ncl|Pres|[Dd]ept|min|max|[Gg]ovt|lb|ft|c\.?\s*f|vs)\.\s))\Z/su',
        '/(?:(?:\b[Ee]tc\.\s))\Z/su',
        '/(?:(?:[\.!?…]+\p{Pe} )|(?:[\[\(]*…[\]\)]* ))\Z/su',
        '/(?:(?:\b\p{L}\.))\Z/su',
        '/(?:(?:\b\p{L}\.\s))\Z/su',
        '/(?:(?:\b[Ff]igs?\.\s)|(?:\b[nN]o\.\s))\Z/su',
        '/(?:(?:[\"”\']\s*))\Z/su',
        '/(?:(?:[\.!?…][\x{00BB}\x{2019}\x{201D}\x{203A}\"\'\p{Pe}\x{0002}]*\s)|(?:\r?\n))\Z/su',
        '/(?:(?:[\.!?…][\'\"\x{00BB}\x{2019}\x{201D}\x{203A}\p{Pe}\x{0002}]*))\Z/su',
        '/(?:(?:\s\p{L}[\.!?…]\s))\Z/su');
    $after_regexes = array('/\A(?:)/su',
        '/\A(?:[\p{N}\p{Ll}])/su',
        '/\A(?:[^\p{Lu}])/su',
        '/\A(?:[^\p{Lu}]|I)/su',
        '/\A(?:[^p{Lu}])/su',
        '/\A(?:\p{Ll})/su',
        '/\A(?:\p{L}\.)/su',
        '/\A(?:\p{L}\.\s)/su',
        '/\A(?:\p{N})/su',
        '/\A(?:\s*\p{Ll})/su',
        '/\A(?:)/su',
        '/\A(?:\p{Lu}[^\p{Lu}])/su',
        '/\A(?:\p{Lu}\p{Ll})/su');
    $is_sentence_boundary = array(false, false, false, false, false, false, false, false, false, false, true, true, true);
    $count = 13;

    $sentences = array();
    $sentence = '';
    $before = '';
    $after = substr($text, 0, 10);
    $text = substr($text, 10);

    while($text != '') {
        for($i = 0; $i < $count; $i++) {
            if(preg_match($before_regexes[$i], $before) && preg_match($after_regexes[$i], $after)) {
                if($is_sentence_boundary[$i]) {
                    array_push($sentences, $sentence);
                    $sentence = '';
                }
                break;
            }
        }

        $first_from_text = $text[0];
        $text = substr($text, 1);
        $first_from_after = $after[0];
        $after = substr($after, 1);
        $before .= $first_from_after;
        $sentence .= $first_from_after;
        $after .= $first_from_text;
    }

    if($sentence != '' && $after != '') {
        array_push($sentences, $sentence.$after);
    }

    return $sentences;
}

$text = "Mr. Entertainment media properties. Fairy Tail 3.5 and Tokyo Ghoul.";
print_r(sentence_split($text));
收藏
评论

亨里克·佩特森(Henrik Petterson),请完整阅读,因为我需要重复上面已经说过的几件事。

正如许多人上面所提到的,如果你添加一个带有\ U修饰它完全可以在Unicode字符为 ,这是可以正常使用的例子下面提到

http://ideone.com/750lMn

<?php


    function splitSentences($text) {
        $re = '/# Split sentences on whitespace between them.
            (?<=                # Begin positive lookbehind.
              [.!?]             # Either an end of sentence punct,
            | [.!?][\'"]        # or end of sentence punct and quote.
            )                   # End positive lookbehind.
            (?<!                # Begin negative lookbehind.
              Mr\.              # Skip either "Mr."
            | Mrs\.             # or "Mrs.",
            | Ms\.              # or "Ms.",
            | Jr\.              # or "Jr.",
            | Dr\.              # or "Dr.",
            | Prof\.            # or "Prof.",
            | Vol\.             # or "Vol.",
            | A\.D\.            # or "A.D.",
            | B\.C\.            # or "B.C.",
            | Sr\.              # or "Sr.",
            | T\.V\.A\.         # or "T.V.A.",
                                # or... (you get the idea).
            )                   # End negative lookbehind.
            \s+                 # Split on whitespace between sentences.
            /uix';

        $sentences = preg_split($re, $text, -1, PREG_SPLIT_NO_EMPTY);
        return $sentences;
    }

$sentences = 'Entertainment media properties. Ã Fairy Tail and Tokyo Ghoul. Entertainment media properties. &Acirc;&nbsp; Fairy Tail and Tokyo Ghoul.';

$sentences = splitSentences($sentences);

print_r($sentences);

您在注释中给出的示例无法正常工作,因为它们在两个句子之间没有任何空格字符 。您的代码特别指定了句子之间必须有空白。

\s+                 # Split on whitespace between sentences.

您上面的注释中的以下示例仅由于Â之前没有空格而无法正常工作。

http://ideone.com/m164fp

收藏
评论

我认为,考虑到用户生成的内容在语法和语法上并不总是正确的,因此不可能获得防弹句子拆分器。此外,由于刮取/内容获取工具的技术缺陷(可能无法获取包含空格或标点符号的干净内容),在技术上不完美,要达到100%正确的结果是不可能的。最后,业务现在更偏向于制定适当的策略,并且如果您设法将文本分成95%的次数,则在大多数情况下,它被认为是成功的。

现在,任何句子拆分任务都是NLP任务,仅一个,两个或三个正则表达式是不够的。建议不要使用自己的正则表达式链,而应该使用一些现有的NLP库。

  1. vanderlee的php句子取决于合理地语法正确的标点符号

以下是用于拆分句子的规则的粗略列表。

  • 每个换行符分隔句子。
  • 文本的末尾表示句子的结尾(如果没有,则通过适当的标点符号结束)。
  • 句子必须至少两个字长,除非有换行符或文本结尾。
  • 空行不是句子。
  • 每个问号或感叹号或它们的组合都被视为句子的结尾。
  • 单个句点被视为句子的结尾,除非...
    • 它前面有一个字,或...
    • 它后面是一个字。
  • 多个句点的序列不视为句子的结尾。

用法示例:

<?php
    require_once 'classes/autoloader.php'; // Include the autoloader.
    $text   = "Hello there, Mr. Smith. What're you doing today... Smith,"
            . " my friend?\n\nI hope it's good. This last sentence will"
            . " cost you $2.50! Just kidding :)"; // This is the test text we're going to use
    $Sentence   = new Sentence;   // Create a new instance
    $sentences  = $Sentence->split($text); // Split into array of sentences
    $count      = $Sentence->count($text); // Count the number of sentences
?>
  1. NlpTools是您可以用于此任务的另一个库。这是实现基于幼稚规则的句子标记器的示例代码:

样例代码:

<?php
include ('vendor/autoload.php');

use \NlpTools\Tokenizers\ClassifierBasedTokenizer;
use \NlpTools\Tokenizers\WhitespaceTokenizer;
use \NlpTools\Classifiers\ClassifierInterface;
use \NlpTools\Documents\DocumentInterface;

class EndOfSentence implements ClassifierInterface
{
    public function classify(array $classes, DocumentInterface $d) {
        list($token,$before,$after) = $d->getDocumentData();

        $dotcnt = count(explode('.',$token))-1;
        $lastdot = substr($token,-1)=='.';

        if (!$lastdot) // assume that all sentences end in full stops
            return 'O';

        if ($dotcnt>1) // to catch some naive abbreviations U.S.A.
            return 'O';

        return 'EOW';
    }
}
$tok = new ClassifierBasedTokenizer(
    new EndOfSentence(),
    new WhitespaceTokenizer()
);
$text = "We are what we repeatedly do.
        Excellence, then, is not an act, but a habit.";

print_r($tok->tokenize($text));

// Array
// (
//    [0] => We are what we repeatedly do.
//    [1] => Excellence, then, is not an act, but a habit.
// )
  1. 您可以获得使用Java StanfordNLP的PHP / JAVA桥 (这是将文本拆分为句子的Java示例 )。

重要说明 :我测试的大多数NLP标记化模型都不能很好地处理粘合的句子。但是,如果在标点符号链后添加空格,则句子拆分质量会提高。在将文本发送到句子拆分功能之前,只需添加以下内容:

$txt = preg_replace('~\p{P}+~', "$0 ", $txt);
收藏
评论

当您将UTF-8字符U + 00A0 Non-Breaking Space打印到被解释为Latin-1的页面/控制台时,它的外观是Â 。所以我认为您在句子之间有一个不间断的空格,而不是正常的空格。

\s也可以匹配一个不间断的空格,但是您将需要使用/u修饰符来告诉preg您要向其发送一个UTF-8编码的字符串。否则,就像您的打印命令一样,它将猜测Latin-1并将其视为两个字符Â

收藏
评论
新手导航
  • 社区规范
  • 提出问题
  • 进行投票
  • 个人资料
  • 优化问题
  • 回答问题

关于我们

常见问题

内容许可

联系我们

@2020 AskGo
京ICP备20001863号