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

Hen*_*son 23 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);
Run Code Online (Sandbox Code Playgroud)

它工作正常.

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

$sentences = 'Entertainment media properties. Fairy Tail and Tokyo Ghoul.';
Run Code Online (Sandbox Code Playgroud)

或者这种情况:

$sentences = "Entertainment media properties.&Acirc;&nbsp; Fairy Tail and Tokyo Ghoul.";
Run Code Online (Sandbox Code Playgroud)

当文本中存在unicode字符时,我该怎么做才能使它工作?

这是一个测试的理念.

赏金信息

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

ndn*_*kov 12

正如所料,任何一种自然语言处理都不是一项微不足道的任务.原因是它们是进化系统.没有一个人坐下来思考哪些是好主意,哪些不是.每条规则都有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));
Run Code Online (Sandbox Code Playgroud)

  • 事先使用[`html_entity_decode`](http://php.net/manual/fr/function.html-entity-decode.php). (8认同)
  • 这是一个出色的答案,非常感谢您发布它.它适用于我在我的问题中提出的场景,但是当它添加到我的脚本时,它仍然无法正常工作.我进一步调查了它,似乎我没有正确检查页面的源代码.请参阅此示例:http://ideone.com/epdpxO它不适用于**"娱乐媒体资产.&Acirc;&nbsp; Fairy Tail和Tokyo Ghoul."** - 您是否有任何正则表达式调整技巧检测此类内容的功能? (2认同)
  • 优秀的答案并且非常彻底,但对于大块文本也很慢。 (2认同)

bob*_*nce 6

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

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

  • ideone的输入已经是UTF-8编码的,因此通过输入`Â`,您可以对输入字符串进行双重UTF-8编码.尝试对付真正的输入字符串. (3认同)