Чтобы подготовить русскоязычные тексты для дальнейшего анализа, мне однажды понадобилось разбить их на предложения. Разумеется, автоматически. Что приходит на ум в первую очередь, если нужно разделить текст на предложения? Разбить по точкам — угадал?
Если вы начнете это делать, то довольно быстро столкнетесь с тем, что точка — это не всегда разделитель предложений (“т.к.”, “т.д.”, “т.п.”, “пр.”, “S.T.A.L.K.E.R.”). Причем эти токены не всегда будут исключениями при разбивке текста на предложения. Например, “т.п.” может быть в середине предложения, а может и в конце.
Вопросительный и восклицательный знак тоже не всегда разделяют текст на предложения. Например, “Yahoo!”. Предложения могут разделять и другие знаки, например, двоеточие (когда следует список из отдельных утверждений).
Поэтому я долго не думая поискал готовый инструмент и остановился на Томита-парсере от Яндекса. О нем и расскажу.
Вообще, Томита-парсер — это мощный инструмент для извлечения фактов из текста. Сегментатор (разбивка текста на предложения) в нем — лишь часть проекта. Томита-парсер можно скачать сразу в виде бинарника и запускать из командной строки. Мне эта система понравилась тем, что она работает на основе правил, не прихотлива к ресурсам и дает возможность настраивать процесс сегментации. А также по моим наблюдениям в большинстве случаев отлично справляется с задачей.
Еще мне понравилось, что при возникновении вопросов можно задать их на github и иногда даже получить ответ.
Запускается Томита-парсер таким образом
То есть чтение происходит из stdin, вывод — в stdout.
Результат получаем примерно такой:
Одна строка — одно предложение. На этом примере видно, что разбивка прошла корректно.
На что обращаем внимание.
Эти особенности могут быть как плюсами так и минусами в зависимости от того, что вы дальше будете делать с полученным текстом. Я, например, дальше по полученному тексту строю синтаксические деревья с помощью SyntaxNet, а там как раз знаки препинания должны быть отделены пробелами, так что для меня это плюс.
Я столкнулся с тем, что при анализе предложений, содержащих адреса, система разбивает их некорректно. Пример:
Как видим, разбивка прошла некорректно. К счастью, такие вещи можно настраивать. Для этого в gzt файле прописываем
То есть просим считать, что после “ул.” предложение всегда продолжается. Пробуем:
Теперь все хорошо. Пример настроек я выложил на github.
О некоторых особенностях я упомянул выше. Пару слов о минусах инструмента на данный момент.
Первое — это документация. Она есть, но в ней описано не все. Попробовал сейчас поискать настройку, которую описал выше — не нашел.
Второе — это отсутствие легкой возможности работы с парсером в режиме демона. Обработка одного текста за 0.3-0.4 секунды с учетом загрузки всей системы в память для меня не критична, так как вся обработка идет в фоновых процессах и среди них есть гораздо более жирные задачи. Для кого-то это может стать узким местом.
Как и говорил выше, подаем входные данные в stdin, читаем из stdout. Пример ниже сделан на основе github.com/makhov/php-tomita:
Проверяем:
Мне в процессе работы над текстом регулярно попадаются проекты, в которых авторы делают сегментатор самостоятельно. Возможно потому, что с первого взгляда задача кажется чуть проще, чем на самом деле. Надеюсь статья будет полезна тем, кто собирается сделать очередной сегментатор в рамках своего проекта и сэкономит время, выбрав готовый вариант.
Буду рад узнать из комментариев, каким инструментом для разбивки текста на предложения пользуетесь вы?
Если вы начнете это делать, то довольно быстро столкнетесь с тем, что точка — это не всегда разделитель предложений (“т.к.”, “т.д.”, “т.п.”, “пр.”, “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) "Предложение два . " }
В завершение
Мне в процессе работы над текстом регулярно попадаются проекты, в которых авторы делают сегментатор самостоятельно. Возможно потому, что с первого взгляда задача кажется чуть проще, чем на самом деле. Надеюсь статья будет полезна тем, кто собирается сделать очередной сегментатор в рамках своего проекта и сэкономит время, выбрав готовый вариант.
Буду рад узнать из комментариев, каким инструментом для разбивки текста на предложения пользуетесь вы?
