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

时间:2016-01-19 16:20:36

标签: php regex unicode nlp

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

6 个答案:

答案 0 :(得分:12)

正如所料,任何一种自然语言处理都不是一件轻而易举的事。原因是它们是进化系统。没有一个人坐下来思考哪些是好主意,哪些不是。每条规则都有20-40%的例外。有了这个说,可以进行出价的单个正则表达式的复杂性将不在图表之列。不过,以下解决方案主要依赖于正则表达式。

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

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

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

就性能而言 - 正则表达式应该具有高性能,因为它们都有\A\Z锚点,几乎没有重复量词,并且在那些地方 - 可以& #39;是任何回溯。仍然,正则表达式是正则表达式。如果您计划在大块文本中使用紧密循环,则必须进行一些基准测试。

强制免责声明:请原谅我生锈的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));

答案 1 :(得分:6)

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

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

答案 2 :(得分:3)

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

您可以使用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);

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

答案 3 :(得分:2)

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

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

  1. vanderlee's php-sentence取决于合理格式正确的标点符号
  2.   

    以下是用于分割句子的粗略列表。

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

    用法示例:

    <?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是您可能用于此任务的另一个库。以下是实现基于规则标记符的天真规则的示例代码:
    2. 示例代码:

      <?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 bridge(这里是将Java example分割为句子的文本。)
      2. 重要提示:我测试的大多数NLP标记化模型都不能很好地处理粘合句子。但是,如果在标点符号链后添加空格,则句子分割质量会提高。只需在将文本发送到句子分割函数之前添加它:

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

答案 4 :(得分:2)

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

如上所述,很多人都提到过,如果你添加一个\ u修饰符,它将对 TRUE 的Unicode字符起作用,并且在下面提到的例子中完美地工作 < / p>

  

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

答案 5 :(得分:1)

非常复杂的Unicode文本分割算法可处理各种文本边界,包括句子边界。

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

此算法最着名的实现是ICU。

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

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

  • 从某个地方获取此课程
  • 编写一个包含所需ICU功能的小型PHP插件 - 只要您构建特定功能,它实际上非常简单。