У меня есть случайный текст, хранящийся в $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 "TVA", # 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. Fairy Tail and Tokyo Ghoul.";
Что я могу сделать, чтобы заставить его работать, когда символы Юникода существуют в тексте?
Вот идеон для тестирования.
Я ищу полное решение этого. Прежде чем отправлять ответ, ознакомьтесь с темой комментариев, которую я написал с WiktorStribiżew, для получения более релевантной информации по этой проблеме.
Как и следовало ожидать, любая обработка естественного языка не является тривиальной задачей. Причиной этого является то, что они являются эволюционными системами. Нет ни одного человека, который бы сел и подумал о том, какие хорошие идеи, а какие – нет. Каждое правило имеет 20-40% исключений. С учетом сказанного сложность одного регулярного выражения, которое может делать ваши ставки, будет за пределами графиков. Тем не менее, следующее решение зависит главным образом от регулярных выражений.
Что касается того, откуда взялись эти регулярные выражения? – Я перевел эту библиотеку Ruby , которая создается на основе этой статьи . Если вы действительно хотите их понять, нет альтернативы, кроме как прочитать статью.
Что касается точности – я рекомендую вам протестировать ее с помощью разных текстов. После некоторых экспериментов я был очень приятно удивлен.
С точки зрения производительности – регулярные выражения должны быть высокоэффективными, поскольку все они имеют либо привязку \A
либо \Z
, почти нет квантификаторов повторения, а в местах есть – не может быть никакого возврата. Тем не менее регулярные выражения являются регулярными выражениями. Вам нужно будет провести бенчмаркинг, если вы планируете использовать это жесткие петли на огромных кусках текста.
Обязательный отказ от ответственности : извините мои ржавые навыки PHP. Следующий код, возможно, не самый идиоматический php, он все равно должен быть достаточно ясным, чтобы понять суть.
function sentence_split($text) { $before_regexes = array('/(?:(?:[\'\"„][\.!?…][\'\"”]\s)|(?:[^\.]\s[AZ]\.\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[AZ]\.\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));
Это то, на что похоже, когда вы печатаете UTF-8 символ U + 00A0 Non-Breaking Space на страницу / консоль, интерпретируемую как Latin-1. Поэтому я думаю, что у вас есть неразрывное пространство между предложениями, а не нормальное пространство.
\s
может также совпадать с нераспадающимся пространством, но вам нужно будет использовать модификатор /u
, чтобы сообщить preg, что вы отправляете ему строку с кодировкой UTF-8. В противном случае он, как и ваша команда печати, будет угадывать латинский-1 и рассматривать его как два символа Â
.
Если пробелы ненадежны, вы можете использовать совпадение на a .
за которым следует любое количество пробелов, за которым следует заглавная буква .
Вы можете сопоставить любую заглавную букву 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);
Примечание. Этот ответ может быть недостаточно точным для вашей ситуации. Я не могу это судить. Он решает проблему, как описано выше, и ее легко понять.
Я считаю, что невозможно получить пуленепробиваемый разделитель предложений, учитывая, что пользовательский контент не всегда грамматически и синтаксически корректен. Более того, достижение 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'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 ?>
Образец кода:
<?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 USA 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. // )
ВАЖНОЕ ЗАМЕЧАНИЕ . Большинство тестируемых моделей токенов NLP не обрабатывают приклеенные предложения хорошо. Однако, если вы добавите пробел после цепочки препинания, повысится качество разделения предложений. Просто добавьте это перед отправкой текста в функцию расщепления предложения:
$txt = preg_replace('~\p{P}+~', "$0 ", $txt);
Henrik Petterson Пожалуйста, прочтите его полностью, потому что мне нужно повторить несколько вещей, которые уже были упомянуты выше.
Как и выше, многие люди упомянули, что если вы добавите модификатор \ u, то он будет работать на символе Unicode, ИСТИНА, и он отлично работает в приведенном ниже примере
<?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 "AD", | B\.C\. # or "BC", | Sr\. # or "Sr.", | T\.V\.A\. # or "TVA", # 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. Â Fairy Tail and Tokyo Ghoul.'; $sentences = splitSentences($sentences); print_r($sentences);
-<?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 "AD", | B\.C\. # or "BC", | Sr\. # or "Sr.", | T\.V\.A\. # or "TVA", # 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. Â Fairy Tail and Tokyo Ghoul.'; $sentences = splitSentences($sentences); print_r($sentences);
Ваши примеры, которые вы указали в комментариях, не работали, потому что у них нет пробелов между двумя предложениями . И ваш код, конкретно указывая на это, должно быть пробел между предложениями.
\s+ # Split on whitespace between sentences.
Нижеприведенный пример, который у вас есть в приведенных выше комментариях, не работает только потому, что перед ним нет места.
Существует довольно сложный алгоритм Unicode Text Segmentation, который имеет дело с различными границами текста, включая границы предложений.
http://unicode.org/reports/tr29/
Наиболее известная реализация этих алгоритмов – ICU.
Я нашел этот класс: http://php.net/manual/en/class.intlbreakiterator.php, но, похоже, он не в рутине.
Поэтому, если вы хотите решить эту ОЧЕНЬ сложную проблему в лучшем случае, я бы предложил: