У меня случайный текст хранится в $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. Fairy Tail and Tokyo Ghoul.";
Что я могу сделать, чтобы это работало, когда в тексте есть символы Юникода?
Вот ideone для тестирования.
Я ищу полное решение для этого. Прежде чем публиковать ответ, пожалуйста, прочтите ветку комментариев, которую я имел с WiktorStribiżew для получения более актуальной информации по этому вопросу.
Как и следовало ожидать, любая обработка естественного языка не является тривиальной задачей. Причина в том, что они являются эволюционными системами. Нет ни одного человека, который бы сидел и думал о том, какие хорошие идеи, а какие — нет. Каждое правило имеет 20-40% исключений. С учетом вышесказанного, сложность одного регулярного выражения, которое может делать ваши ставки, будет вне графика. Тем не менее, следующее решение опирается в основном на регулярные выражения.
Что касается откуда взялись эти регулярные выражения? — я перевел эта библиотека 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));
Â
это то, на что это похоже, когда вы печатаете UTF-8 символ U + 00A0 Non-Breaking Space на страницу / консоль, интерпретируемую как Latin-1. Поэтому я думаю, что у вас есть неразрывный пробел между предложениями, а не нормальный пробел.
\s
может соответствовать неразрывному пробелу, но вам нужно будет использовать /u
модификатор, сообщающий preg, что вы отправляете ему строку в кодировке UTF-8. В противном случае он, как и ваша команда печати, будет угадывать Latin-1 и видеть его как два символа. Â
,
Если пробелы ненадежны, то вы можете использовать совпадение на .
с последующим любым количеством пробелов, с большой буквы.
Вы можете сопоставить любую заглавную букву 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.
Ниже приведен приблизительный список правил, используемых для разделения предложений.
- Каждый перевод строки разделяет предложения.
- В конце текста указывается конец, если предложение, если не указано иное, завершается правильной пунктуацией.
- Предложения должны состоять как минимум из двух слов, за исключением перевода строки или конца текста.
- Пустая строка не является предложением.
- Каждый вопрос или восклицательный знак или их комбинация считается концом предложения.
- Отдельный период считается концом предложения, если только …
- Ему предшествует одно слово, или …
- За ним следует одно слово.
- Последовательность нескольких периодов не считается концом предложения.
Пример использования:
<?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 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.
// )
ВАЖНАЯ ЗАМЕТКА: Большинство протестированных мною моделей НЛП плохо обрабатывают склеенные предложения. Однако, если вы добавите пробел после знака препинания, качество разбиения предложений повысится. Просто добавьте это перед отправкой текста в функцию разделения предложения:
$txt = preg_replace('~\p{P}+~', "$0 ", $txt);
Хенрик Петтерсон Пожалуйста, прочитайте его полностью, потому что мне нужно повторить несколько вещей, которые уже были сказаны выше.
Как уже упоминалось выше, многие люди упоминали, что если вы добавите модификатор \ u, он будет работать с символом Unicode: ПРАВДА и это Работает отлично в приведенном ниже примере
<?phpfunction 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. Â Fairy Tail and Tokyo Ghoul.';
$sentences = splitSentences($sentences);
print_r($sentences);
Ваши примеры, которые вы привели в комментариях, были не работает, потому что Oни не должно быть пробелов между двумя предложениями. И твой код с указанием именно того, что там должен быть пробелом между предложениями.
\s+ # Split on whitespace between sentences.
Приведенный ниже пример, который вы видите в комментариях выше, не работает только потому, что до него нет места.
Существует довольно сложный алгоритм Unicode Text Segmentation, который работает с различными текстовыми границами, включая границы предложений.
http://unicode.org/reports/tr29/
Самая известная реализация этих алгоритмов — ICU.
Я нашел этот класс: http://php.net/manual/en/class.intlbreakiterator.php однако, похоже, что это в git, а не в мейнстриме.
Так что если вы хотите решить эту ОЧЕНЬ сложную проблему в лучшем случае, почему я бы предложил: