Разделение текста на предложения с помощью Томита-парсера
Если вы начнете это делать, то довольно быстро столкнетесь с тем, что точка — это не всегда разделитель предложений («т.к.», «т.д.», «т.п.», «пр.», «S.T. A.L.K. E.R.»). Причем эти токены не всегда будут исключениями при разбивке текста на предложения. Например, «т.п.» может быть в середине предложения, а может и в конце.
Вопросительный и восклицательный знак тоже не всегда разделяют текст на предложения. Например, «Yachoo!». Предложения могут разделять и другие знаки, например, двоеточие (когда следует список из отдельных утверждений).
Поэтому я долго не думая поискал готовый инструмент и остановился на Томита-парсере от Яндекса. О нем и расскажу.
Вообще, Томита-парсер — это мощный инструмент для извлечения фактов из текста. Сегментатор (разбивка текста на предложения) в нем — лишь часть проекта. Томита-парсер можно скачать сразу в виде бинарника и запускать из командной строки. Мне эта система понравилась тем, что она работает на основе правил, не прихотлива к ресурсам и дает возможность настраивать процесс сегментации. А также по моим наблюдениям в большинстве случаев отлично справляется с задачей.
Еще мне понравилось, что при возникновении вопросов можно задать их на 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:
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]);
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) "Предложение два . "
}
В завершение
Мне в процессе работы над текстом регулярно попадаются проекты, в которых авторы делают сегментатор самостоятельно. Возможно потому, что с первого взгляда задача кажется чуть проще, чем на самом деле. Надеюсь статья будет полезна тем, кто собирается сделать очередной сегментатор в рамках своего проекта и сэкономит время, выбрав готовый вариант.
Буду рад узнать из комментариев, каким инструментом для разбивки текста на предложения пользуетесь вы?