Если вы начнете это делать, то довольно быстро столкнетесь с тем, что точка — это не всегда разделитель предложений (“т.к.”, “т.д.”, “т.п.”, “пр.”, “S.T.A.L.K.E.R.”). Причем эти токены не всегда будут исключениями при разбивке текста на предложения. Например, “т.п.” может быть в середине предложения, а может и в конце.
Вопросительный и восклицательный знак тоже не всегда разделяют текст на предложения. Например, “Yahoo!”. Предложения могут разделять и другие знаки, например, двоеточие (когда следует список из отдельных утверждений).
Поэтому я долго не думая поискал готовый инструмент и остановился на Томита-парсере от Яндекса. О нем и расскажу.
Вообще, Томита-парсер — это мощный инструмент для извлечения фактов из текста. Сегментатор (разбивка текста на предложения) в нем — лишь часть проекта. Томита-парсер можно скачать сразу в виде бинарника и запускать из командной строки. Мне эта система понравилась тем, что она работает на основе правил, не прихотлива к ресурсам и дает возможность настраивать процесс сегментации. А также по моим наблюдениям в большинстве случаев отлично справляется с задачей.
Еще мне понравилось, что при возникновении вопросов можно задать их на github и иногда даже получить ответ.
Запуск
Запускается Томита-парсер таким образом
$ echo "Парсеp, Разбей эти... буквы, знаки и т.п. на предложения. И покажи пож. как со словом S.T.A.L.K.E.R. получится." | ./tomita-linux64 config.proto
То есть чтение происходит из stdin, вывод — в stdout.
Результат получаем примерно такой:
[10:01:17 17:06:37] - Start. (Processing files.)
Парсер , Разбей эти . . . буквы , знаки и т.п . на предложения .
И покажи пож . как со словом S. T. A. L. K. E. R. получится .
[10:01:17 17:06:37] - End. (Processing files.)
Одна строка — одно предложение. На этом примере видно, что разбивка прошла корректно.
Особенности
На что обращаем внимание.
- В результат добавляются пробелы перед знаками пунктуации.
- Лишние пробелы удаляются.
- Происходит автоматическая коррекция некоторых опечаток (например, в исходном тексте последняя буква в слове “Парсеp” — это английская “пи”, а в обработанном тексте — это уже русская “эр”).
Эти особенности могут быть как плюсами так и минусами в зависимости от того, что вы дальше будете делать с полученным текстом. Я, например, дальше по полученному тексту строю синтаксические деревья с помощью SyntaxNet, а там как раз знаки препинания должны быть отделены пробелами, так что для меня это плюс.
Настройки
Я столкнулся с тем, что при анализе предложений, содержащих адреса, система разбивает их некорректно. Пример:
$ echo "Я живу на ул. Ленина и меня зарубает время от времени." | ./tomita-linux64 config.proto
[10:01:17 18:00:38] - Start. (Processing files.)
Я живу на ул .
Ленина и меня зарубает время от времени .
[10:01:17 18:00:38] - End. (Processing files.)
Как видим, разбивка прошла некорректно. К счастью, такие вещи можно настраивать. Для этого в gzt файле прописываем
TAbbreviation "ул." {
key = { "abbreviation_г." type = CUSTOM }
text = "ул."
type = NewerEOS
}
То есть просим считать, что после “ул.” предложение всегда продолжается. Пробуем:
$ echo "Я живу на ул. Ленина и меня зарубает время от времени." | ./tomita-linux64 config.proto
[10:01:17 18:20:59] - Start. (Processing files.)
Я живу на ул. Ленина и меня зарубает время от времени .
[10:01:17 18:20:59] - End. (Processing files.)
Теперь все хорошо. Пример настроек я выложил на github.
Какие минусы
О некоторых особенностях я упомянул выше. Пару слов о минусах инструмента на данный момент.
Первое — это документация. Она есть, но в ней описано не все. Попробовал сейчас поискать настройку, которую описал выше — не нашел.
Второе — это отсутствие легкой возможности работы с парсером в режиме демона. Обработка одного текста за 0.3-0.4 секунды с учетом загрузки всей системы в память для меня не критична, так как вся обработка идет в фоновых процессах и среди них есть гораздо более жирные задачи. Для кого-то это может стать узким местом.
Пример вызова из PHP
Как и говорил выше, подаем входные данные в stdin, читаем из stdout. Пример ниже сделан на основе github.com/makhov/php-tomita:
<?php
class TomitaParser
{
/**
* @var string Path to Yandex`s Tomita-parser binary
*/
protected $execPath;
/**
* @var string Path to Yandex`s Tomita-parser configuration file
*/
protected $configPath;
/**
* @param string $execPath Path to Yandex`s Tomita-parser binary
* @param string $configPath Path to Yandex`s Tomita-parser configuration file
*/
public function __construct($execPath, $configPath)
{
$this->execPath = $execPath;
$this->configPath = $configPath;
}
public function run($text)
{
$descriptors = array(
0 => array('pipe', 'r'), // stdin
1 => array('pipe', 'w'), // stdout
2 => array('pipe', 'w') // stderr
);
$cmd = sprintf('%s %s', $this->execPath, $this->configPath);
$process = proc_open($cmd, $descriptors, $pipes, dirname($this->configPath));
if (is_resource($process))
{
fwrite($pipes[0], $text);
fclose($pipes[0]);
$output = stream_get_contents($pipes[1]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
return $this->processTextResult($output);
}
throw new \Exception('proc_open fails');
}
/**
* Обработка текстового результата
* @param string $text
* @return string[]
*/
public function processTextResult($text)
{
return array_filter(explode("\n", $text));
}
}
$parser = new TomitaParser('/home/mnv/tmp/tomita/tomita-linux64', '/home/mnv/tmp/tomita/config.proto');
var_dump($parser->run('Предложение раз. Предложение два.'));
Проверяем:
$ php example.php
/home/mnv/tmp/tomita/example.php:66:
array(2) {
[0] =>
string(32) "Предложение раз . "
[1] =>
string(32) "Предложение два . "
}
В завершение
Мне в процессе работы над текстом регулярно попадаются проекты, в которых авторы делают сегментатор самостоятельно. Возможно потому, что с первого взгляда задача кажется чуть проще, чем на самом деле. Надеюсь статья будет полезна тем, кто собирается сделать очередной сегментатор в рамках своего проекта и сэкономит время, выбрав готовый вариант.
Буду рад узнать из комментариев, каким инструментом для разбивки текста на предложения пользуетесь вы?
Комментарии (16)
fijj
11.01.2017 11:38+2А если предложение все же заканчивается на 'ул.' например: '… Пушкинская ул.' я так понимаю отработает не корректно и в конфиг не поможет?
mnv
11.01.2017 11:40К сожалению в этом случае и конфиг не помогает, по крайней мере я не нашел возможности задать более точные настройки. Пришлось смириться, так как подобные случаи — редкость.
Carduelis
11.01.2017 12:06-3А зачем для подобной задачи целый парсер, который даже не имеет встроенной библиотеки исключений?
Разбивка по ". " [точка-пробел] дала бы такой же результат. Остальное — в исключение.mnv
11.01.2017 12:29Там есть исключения и правила, которые определяют — когда исключения работают, а когда нет. Несколько примеров как работают исключения я описал.
ServPonomarev
11.01.2017 13:40Вот ссылка. Сам пользуюсь решением от Солярикс и доволен.
Хочу просто упомянуть не совсем тривиальные элементы токенизации:
«у.е.»
«по моему»
«Васисуалий Пупкин»
«В.И. Ленин»
Это всё единичные токены. Без словаря вы такое не сделаете.mnv
11.01.2017 13:51Попробовал разбить — все корректно:
$ echo "Уважаемый В.И. Ленин, сколько в у.е. это стоит? По моему 5 у.е., не больше - сказал Васисуалий Пупкин и ушел." | ./tomita-linux64 config.proto [11:01:17 13:49:36] - Start. (Processing files.) Уважаемый В. И. Ленин , сколько в у.е . это стоит ? По моему 5 у.е . , не больше - сказал Васисуалий Пупкин и ушел . [11:01:17 13:49:36] - End. (Processing files.)
kloppspb
11.01.2017 14:20Вы часом не путаете «по моему [мнению это так | рукаву ползёт букашка]» и «по-моему»?
buriy
11.01.2017 15:17+1Речь идёт про парсинг реальных текстов, не все из которых написаны профессиональными лингвистами.
Там ещё и не такие ошибки встречаются, и такие тексты тоже нужно правильно парсить.kloppspb
11.01.2017 15:55А как вы собираетесь отличать правильный вариант от фантазии двоечника? IMHO, в спорных случаях парсер должен работать исключительно по правилам русского языка, иначе будет полный бардак. А здесь и спорного случая-то нет, во всяком случае без глубокого смыслового анализа текста.
buriy
11.01.2017 16:27+1Напомню: этот пост совершенно про другую задачу — разбиение текста на предложения.
> в спорных случаях парсер должен работать исключительно по правилам русского языка, иначе будет полный бардак.
Ваши слова означают, что вы не знаете, как решать задачу исправления ошибок для произвольных текстов, и умеете делать парсинг только для грамматичных текстов.
Задача исправления ошибок комплексная, обычно решается на разных уровнях, потому что ошибки бывают совершенно разные.
Конкретно, исправление для «по моему» чаще всего делается на уровне вероятностного морфологического парсера, который должен учитывать возможность отсутствия дефиса в словах. Также возможна корректировка принятого морфологическом парсером решения на уровне синтактико-семантического анализа.
irastypain
11.01.2017 15:45+1Было упомянуто исправление ошибок. А как быть, если была пропущена именно «точка»? Или какой-нибудь другой знак препинания, завершающий предложение.
mnv
11.01.2017 15:52С этим сложнее. Я так понял что Томита-парсер опирается именно на знаки препинания и правила там организованы вокруг знаков препинания и токенов.
Тут от задачи зависит. Если нужно парсить новостные сайты например, то там текст более-менее аккуратно написан и грубые ошибки встречаются редко. Если нужно парсить текст из соцсетей, то нужно искать более продвинутый инструмент.
irastypain
11.01.2017 16:00По идее «правильный» парсер должен дополнительно опираться на синтаксические конструкции и семантику (тут сложнее). А пока по ощущениям: предложение есть то, что начинается с заглавной буквы и заканчивается на ". или! или? или ...", а в gzt файл как раз добавляются места спотыканий (то же «ул.»).
В целом да, если текст более менее грамотный и не изобилует специфическими сокращениями, то этот вариант парсера более чем достаточный.
mihmig
Пользуясь случаем спрошу — а нет ли свободных решений (можно не идеально работающих) для разбивки слов на слоги. Мне это нужно для генерирования картинок из текста — для чтения на устройствах, не поддерживающих нормальные программы для чтения.
mnv
Эту задачу решать не пробовал, но поиском нашел пару вариантов. Также можно глянуть ответы на stackoverflow.