【问题标题】:Split string into sentences using regex使用正则表达式将字符串拆分为句子
【发布时间】:2016-04-25 05:18:57
【问题描述】:

我在$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 的评论主题,以获取有关此问题的更多相关信息。

【问题讨论】:

  • 一旦符合条件,我将奖励这个问题 50 分。
  • 你需要使用/u修饰符。
  • @WiktorStribiżew 是的,如果我删除 unicode 字符,它可以正常工作,请参见示例:ideone.com/ZQhPSV
  • 我刚刚使用\s*\s+ 设为可选。我看到亨利很快就会阅读别人的 cmets :)
  • @WiktorStribiżew 知道了。十分感谢你分享这些信息。如果可以将其放入代码中,我将保留此问题并悬赏 50 分(如果符合条件)以获得“防弹”解决方案。

标签: php regex unicode nlp


【解决方案1】:

正如预期的那样,任何类型的自然语言处理都不是一项简单的任务。原因是它们是进化系统。没有一个人坐下来思考哪些是好主意,哪些不是。每条规则都有 20-40% 的例外。话虽如此,可以完成您的竞标的单个正则表达式的复杂性将超出预期。不过,以下解决方案主要依赖于正则表达式。


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

至于这些正则表达式从何而来? - 我翻译了this Ruby library,它是基于this paper 生成的。如果你真的想了解它们,除了阅读论文之外别无他法。

就准确性而言 - 我鼓励您使用不同的文本对其进行测试。经过一些实验,我感到非常惊喜。

在性能方面 - 正则表达式应该是高性能的,因为它们都有一个 \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));

【讨论】:

  • 这是一个出色的答案,非常感谢您发布它。它适用于我在问题中提出的场景,但是将其添加到我的脚本时,它仍然无法正常工作。我进一步调查了它,似乎我没有正确检查页面的源代码。看这个例子:ideone.com/epdpxO它不适用于“娱乐媒体属性。 妖精的尾巴和东京食尸鬼。”——你有任何使用正则表达式的技巧来调整函数到检测到这种类型的内容?
  • 事先使用html_entity_decode
  • @HenrikPetterson,虽然复制正则表达式以用于字符的 html 编码版本可能是可行的,但最好按照 Lucas 的建议解码字符串。
  • BEFORE_RE = /(?:#{RULES.map{|s,e,v| "(#{s})"}.join("|")})\Z/m 有一个 /m 修饰符,它重新定义了 Ruby 中的 . 匹配行为。在 PHP 中,它相当于/s。但是,我在这里看不到任何使用“特殊”点的模式,因此可以从正则表达式中删除 m 修饰符。此外,为简洁起见,\A\Z 可以替换为 ^$(在 PHP 中,^$ 仅在使用 /m 修饰符时匹配行的开始/结束 -如果您将\A\Z 替换为^$,您将必须 删除m 修饰符)。
  • 优秀的答案和非常彻底,但对于大块文本也很慢。
【解决方案2】:

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

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

【讨论】:

  • 你介意给我提供一个 /u 修饰符如何工作的示例代码,因为我似乎无法按照你的建议让它工作。这是一个ideone.com/ZQhPSV 供参考。另外,请参阅上面我与 WiktorStribiżew 的对话。
  • /ix 替换为/uix
  • 我试过了,但它没有拆分句子。请看:ideone.com/m164fp
  • ideone 的输入已经是 UTF-8 编码的,所以通过输入 Â 你已经对你的输入字符串进行了双 UTF-8 编码。用真实的输入字符串试试。
【解决方案3】:

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

您可以使用 Unicode character property \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);

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

【讨论】:

    【解决方案4】:

    Henrik Petterson 请完整阅读它,因为我需要重复上面已经说过的几件事。

    正如上面许多人提到的,如果你添加一个 \u 修饰符,它将适用于 Unicode 字符 TRUE 并且在下面提到的示例中它是 Working Perfectly

    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);
    

    您在 cmets 中给出的示例不起作用,因为它们两个句子之间没有任何空白字符。您的代码特别指定必须在句子之间有一个空格。

    \s+                 # Split on whitespace between sentences.
    

    您在上面的 cmets 中的以下示例无法正常工作,只是因为 Â 之前没有空格。

    http://ideone.com/m164fp

    【讨论】:

      【解决方案5】:

      考虑到用户生成的内容在语法和句法上并不总是正确的,我认为不可能获得防弹的句子拆分器。此外,由于抓取/内容获取工具的技术缺陷,可能无法获得包含空格或标点符号垃圾的干净内容,因此不可能达到 100% 正确的结果。最后,企业现在更倾向于足够好的策略,如果你设法将文本分成 95% 的时间,在大多数情况下就被认为是成功的。

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

      1. vanderlee's php-sentence取决于语法正确的标点符号

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

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

      使用示例:

      <?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. 您可以获得PHP/JAVA bridge 用于使用Java StanfordNLP(这里是Java example 用于将文本拆分为句子)。

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

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

      【讨论】:

      • 感谢您提供相关脚本的概要。我有个问题。最后的 preg_replace() 正则表达式示例,它是否在 每个 标点符号之后添加一个空格,或者它到底是做什么的?在各种情况下不应添加空格。例如“3.50”
      • 每一个或多个标点后加一个空格,有利于数句。如果要获取句子,则需要一些更复杂的后期处理。
      • 我选择@ndn 答案,但我非常感谢您抽出宝贵时间发布此答案,这在我们执行单元测试等时将非常有用。
      【解决方案6】:

      有相当复杂的 Unicode 文本分割算法来处理包括句子边界在内的各种文本边界。

      http://unicode.org/reports/tr29/

      这种算法最著名的实现是 ICU。

      我找到了这个类:http://php.net/manual/en/class.intlbreakiterator.php 但是它似乎在 git 中而不是在主流中。

      因此,如果您想最好地解决这个非常复杂的问题,我建议:

      • 从某个地方获取这个类
      • 编写一个小的 PHP 插件来包装您需要的 ICU 功能 - 只要您构建特定的功能,它实际上非常简单。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2017-02-23
        相关资源
        最近更新 更多