Подтвердить что ты не робот

Разделить строку на предложения с использованием регулярного выражения

У меня есть случайный текст, хранящийся в $sentences. Используя regex, я хочу разбить текст на предложения, см.

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, для получения более релевантной информации по этой проблеме.

4b9b3361

Ответ 1

Как и следовало ожидать, любая обработка естественного языка не является тривиальной задачей. Причиной этого является то, что они являются эволюционными системами. Нет ни одного человека, который бы сел и подумал о том, какие хорошие идеи, а какие - нет. Каждое правило имеет 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));

Ответ 2

  выглядит так, как будто вы печатаете символ U + 00A0 UTF-8 Non-Breaking Space на страницу/консоль, интерпретируемую как Latin-1. Поэтому я думаю, что у вас есть неразрывное пространство между предложениями, а не нормальное пространство.

\s может также совпадать с нераспадающимся пространством, но вам нужно будет использовать модификатор /u, чтобы сообщить preg, что вы отправляете ему строку с кодировкой UTF-8. В противном случае он, как и ваша команда печати, угадает латинский-1 и увидит его как два символа  .

Ответ 3

Если пробелы ненадежны, вы можете использовать совпадение на ., за которым следует любое количество пробелов, , за которым следует заглавная буква.

Вы можете сопоставить любую заглавную букву UTF-8, используя Свойство символов Unicode \p{Lu}.

Вам нужно только исключить аббревиатуры, которые, как правило, следуют собственным именам (имена людей, названия компаний и т.д.), поскольку они начинаются с заглавной буквы.

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

Я считаю, что невозможно получить пуленепробиваемый разделитель предложений, учитывая, что пользовательский контент не всегда грамматически и синтаксически корректен. Более того, достижение 100% правильных результатов просто невозможно из-за технического несовершенство инструментов для очистки/содержания, которые могут не получить чистого содержимого, которое либо будет содержать пробелы, либо препинание. И, наконец, бизнес теперь более предвзято относится к достаточно хорошей стратегии, и если вам удастся разделить текст на 95% случаев, то в большинстве случаев считается успешным.

Теперь любая задача расщепления предложения - это задача NLP, и недостаточно одного или двух или трех регулярных выражений. Вместо того, чтобы думать о вашей собственной цепочке регулярных выражений, я бы посоветовал использовать для этого некоторые существующие библиотеки НЛП.

Ниже приведен примерный список правил, используемых для разделения предложений.

  • Каждый linebreak отделяет предложения.
  • Конец текста указывает конец, если предложение, если иное не закончилось с помощью правильной пунктуации.
  • Приговоры должны быть длиннее, по крайней мере, на два слова, кроме строк или конца текста.
  • Пустая строка не является предложением.
  • Каждый вопросительный знак или его восклицательный знак или их комбинация считается окончанием предложения.
  • Один период считается окончанием предложения, если...
    • Ему предшествует одно слово или...
    • За ним следует одно слово.
  • Последовательность нескольких периодов не считается окончанием предложения.

Пример использования:

<?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 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 пример разбиения текста на предложения).

ВАЖНОЕ ЗАМЕЧАНИЕ. Большинство тестируемых моделей токенов NLP не обрабатывают приклеенные предложения. Однако, если вы добавите пробел после цепочки препинания, повысится качество разделения предложений. Просто добавьте это перед отправкой текста в функцию расщепления предложения:

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

Ответ 5

Хенрик Петтерсон Пожалуйста, прочтите его полностью, потому что мне нужно повторить несколько вещей, которые уже были упомянуты выше.

Как упоминалось выше, многие люди упомянули, что если вы добавите модификатор \u, то он будет работать с символом Unicode TRUE, и он будет работать отлично в приведенном ниже примере

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

Ответ 6

Существует довольно сложный алгоритм юникодной текстовой сегментации, который имеет дело с различными границами текста, включая границы предложений.

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

Наиболее известная реализация этих алгоритмов - ICU.

Я нашел этот класс: http://php.net/manual/en/class.intlbreakiterator.php, однако он, кажется, находится в git не в mainstream.

Итак, если вы хотите решить эту ОЧЕНЬ сложную проблему в лучшем случае, почему я предлагаю:

  • Получить этот класс где-нибудь
  • Напишите небольшой плагин PHP, который обертывает функциональные возможности ICU, который вам нужен - на самом деле это довольно просто, пока вы создаете определенные функции.