Регулярное выражение для разбиения текста на предложения
Вот выражение, которое можно иногда использовать для разбиения текста на предложения
(?<![A-ZА-Я])[\.?!]+\s*(?=[A-ZА-Я][^\.]|$)
Задача разбиения текста на предложения является одной из задач искусственного интеллекта, потому что для этого нужно понимать смысл текста. Ведь предложение это именно смысловая единица. Разделителями предложений в тексте служат: точка, восклицательный знак, вопросительный знак и их комбинации (троеточие, пара восклицательный и вопросительный знаки). Но точка ставится и в конце слова, сокращенного методом усечения, и может находиться в составе прямой речи. Алгоритм должен распознавать подобные случаи.
Рассмотрим пример. Вот отрывок текста:
Лекцию прочитал профессор А. П. Езерский, ученики которого часто оказывались весьма успешными, т.е. неоднократно добивались впечатляющих научных результатов.
Вот еще один отрывок текста:
Человек выпрыгнул через окно, утверждал гражданин К. Ясно что это противоречило сказанному ранее.
В первом случае имеем одно предложение, а во втором — два. Но как алгоритм распознает (отличит), что "А. П. Езерский" — инициалы и фамилия (строка входящая в одно предложение), а "К. Ясно" — конец одного предложения и начало другого. Не зная смысла утверждения невозможно понять два предложения здесь или одно. Алгоритм может учитывать что строка "т.е." скорее всего не может быть концом осмысленного предложения, а вот как быть со строкой "т.д." — она может быть и концом предложения, а может и находиться в середине. Ну хорошо: если после "т.д." следуют пробелы и слово начинающееся с заглавной буквы (без предшествующей запятой) — то это почти наверняка конец предложения. Есть еще интересная строка "т.п."... В случае вхождения этой строки в текст так просто уже не разобраться — полно лексематических конструкций в которых "т.п." может быть концом или серединой предложения. И без какого-либо осмысливания текста, нельзя установить какой именно случай имеет место. Есть запутанные отрывки текста, которые даже человек не способен однозначно разделить на предложения.
Но все-таки для многих задач, которые связаны с разбитием текста на предложения, будет достаточно простого алгоритма.
И одного регулярного выражения для строки-разделителя предложений
(?<![A-ZА-Я])[\.?!]+\s*(?=[A-ZА-Я][^\.])
Разберем его. Непосредственно разделитель здесь это [\.?!]+\s* — последовательность из точек, восклицательных знаков и вопросительных знаков за которой могут следовать пробелы (или вообще символы-разделители).
За разделителем должно быть следующее предложение, начинающееся с заглавной буквы, непосредственно за которой нет точки. Иначе что это за следующее предложение такое, из одной буквы, выкрик разве что?
Добавляем к выражению позитивную опережающую проверку (так называется проверка на наличие какого-то выражения непосредственно ЗА искомым), получаем
[\.?!]+\s*(?=[A-ZА-Я][^\.]) — это еще не все.
Непосредственно перед разделителем не должно быть заглавной буквы (это конечно не всегда верно, пример). Добавляем к выражению негативную ретроспективную проверку (проверку на то, что непосредственно перед искомым выражением не должно следовать какое-то выражение). При создании алгоритма будем руководствоваться правилом, что лучше не разбить какие-то предложения и получить в результате их как единое целое, чем ошибочно разбить одно предложение на два.
Получаем (?<![A-ZА-Я])[\.?!]+\s*(?=[A-ZА-Я][^\.]) — это выражение можно использовать очень во многих случаях для разбиения текста на предложения.
Мы будем получать два массива: массив предложений и массив разделителей.
Еще немного изменим шаблон указав что после разделителя может идти не только следующее предложение но и конец текста.
Это для того, чтобы последний разделитель тоже попал в массив разделителей.
Тогда правда мы получим одно лишнее предложение — пустое.
Итак, вот что получилось: (?<![A-ZА-Я])[\.?!]+\s*(?=[A-ZА-Я][^\.]|$)
Пример кода на PHP:
$text = "Сегодня я был в университете с целью найти научного руководителя. Доктор физ.-мат. наук Г. А. Гальперин — один из лучших специалистов на кафедре. Но хотелось бы найти какого-то преподавателя, чтобы не работать полгода над дипломной работой. Отсканировать книгу, например или купить пачку ксероксной бумаги... В общем Вы меня понимаете, наверное, да?";
/* шаблон разделителя */
$pattern = "/(?<![A-ZА-Я])[\.?!]+\s*(?=[A-ZА-Я][^\.]|$)/";
/* разделяем текст на предложения и выводим все на экран */
/* указываем флаг PREG_SPLIT_NO_EMPTY для того, чтобы функция не возвращала последнюю (пустую) строку*/
$sentences = preg_split($pattern, $text, -1, PREG_SPLIT_NO_EMPTY);
echo "<pre>";
print_r($sentences);
echo "</pre>";
/* получаем собственно разделители */
preg_match_all($pattern, $text, $delimiters);
echo "<pre>";
print_r($delimiters[0]);
echo "</pre>";
Выводится на экран:
(
[0] => Сегодня я был в университете с целью найти научного руководителя
[1] => Доктор физ.-мат. наук Г. А. Гальперин — один из лучших специалистов на кафедре
[2] => Но хотелось бы найти какого-то преподавателя, что-бы не работать полгода над дипломной работой
[3] => Отсканировать книгу, например или купить пачку ксероксной бумаги
[4] => В общем Вы меня понимаете, наверное, да
)
Array
(
[0] => .
[1] => .
[2] => .
[3] => ...
[4] => ?
)
Шаблон может быть изменен в зависимости от поставленной задачи.
Например, если Вы хотите указать что точка в конце строки "т.е." не может быть концом предложения измените шаблон на следующий:
(?<!т\. е)(?<!т\.е)(?<![A-ZА-Я])[\.?!]+\s*(?=[A-ZА-Я][^\.]|$)
Выражения для ретроспективной и опережающей проверки должны быть фиксированной длинны,
нельзя писать (?<!т\.\s*е), поэтому проверяем дважды:
непосредственно перед разделителем не должно быть ни строки "т. е", ни строки "т.е".
Дополнение…
Еще для разделения предложений может использоваться сивол "…" — троеточие (Alt 0133) и символ "‼" — два восклицательных знака (Alt 19). Дополним регулярное выражение символами "…","‼" и посмотрим код на Perl.
Переведем текст из первого примера на английский язык. Добавим в регулярное выражение проверку того, что перед разделителем нет сокращений Dr. и Sci. (без последней точки):
(?<!Dr)(?<!Sci)(?<![A-Z])[\.?!…‼]+\s*(?=[A-Z][^\.]|$)
Пример кода на Perl:
$text = "Today I was at university in order to find a scientific leader. Dr. Sci. Science GA Halperin - one of the best specialists in the department. But I would like to find some kind of teacher that, would not work half of the thesis. Scan the book, for example, or buy a pack of Xerox paper... In general, you understand me, probably, yes?";
@sentences = split(/(?<!Dr)(?<!Sci)(?<![A-Z])[\.?!…‼]+\s*(?=[A-Z][^\.]|$)/, $text);
foreach $sentence (@sentences){
print "$sentence\n";
}
@delimiters = $text =~ /(?<!Dr)(?<!Sci)(?<![A-Z])([\.?!…‼]+)\s*(?=[A-Z][^\.]|$)/g;
foreach $delimiter (@delimiters){
print "$delimiter\n";
}
Вывод коммандной строки:
Today I was at university in order to find a scientific leader
Dr. Sci. Science GA Halperin - one of the best specialists in the department
But I would like to find some kind of teacher that, would not work half of the t
hesis
Scan the book, for example, or buy a pack of Xerox paper
In general, you understand me, probably, yes
.
.
.
...
?
C:\>
Заключение
Полученные в статье регулярные выражения не являются универсальными. Они должны быть изменены и дополнены в зависимости от характера текста для которого они будут применяться.
При создании регулярного выражения руководствуйтесь правилом: "Лучше не разбить какие-то предложения и получить в результате их как единое целое, чем ошибочно разбить одно предложение на два."
