Чтобы подготовить русскоязычные тексты для дальнейшего анализа, мне однажды понадобилось разбить их на предложения. Разумеется, автоматически. Что приходит на ум в первую очередь, если нужно разделить текст на предложения? Разбить по точкам — угадал?

Если вы начнете это делать, то довольно быстро столкнетесь с тем, что точка — это не всегда разделитель предложений (“т.к.”, “т.д.”, “т.п.”, “пр.”, “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)


  1. mihmig
    11.01.2017 11:28
    +1

    Пользуясь случаем спрошу — а нет ли свободных решений (можно не идеально работающих) для разбивки слов на слоги. Мне это нужно для генерирования картинок из текста — для чтения на устройствах, не поддерживающих нормальные программы для чтения.


    1. mnv
      11.01.2017 11:44

      Эту задачу решать не пробовал, но поиском нашел пару вариантов. Также можно глянуть ответы на stackoverflow.


  1. fijj
    11.01.2017 11:38
    +2

    А если предложение все же заканчивается на 'ул.' например: '… Пушкинская ул.' я так понимаю отработает не корректно и в конфиг не поможет?


    1. mnv
      11.01.2017 11:40

      К сожалению в этом случае и конфиг не помогает, по крайней мере я не нашел возможности задать более точные настройки. Пришлось смириться, так как подобные случаи — редкость.


  1. Carduelis
    11.01.2017 12:06
    -3

    А зачем для подобной задачи целый парсер, который даже не имеет встроенной библиотеки исключений?
    Разбивка по ". " [точка-пробел] дала бы такой же результат. Остальное — в исключение.


    1. mnv
      11.01.2017 12:29

      Там есть исключения и правила, которые определяют — когда исключения работают, а когда нет. Несколько примеров как работают исключения я описал.


  1. Shedar
    11.01.2017 12:33
    +1

    Для английского использовал spaCy. Выбрал, отчасти, потому что нужен был и POS tagger, который в нем тоже есть.


  1. ServPonomarev
    11.01.2017 13:40

    Вот ссылка. Сам пользуюсь решением от Солярикс и доволен.

    Хочу просто упомянуть не совсем тривиальные элементы токенизации:

    «у.е.»
    «по моему»
    «Васисуалий Пупкин»
    «В.И. Ленин»

    Это всё единичные токены. Без словаря вы такое не сделаете.


    1. 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.)
      


    1. kloppspb
      11.01.2017 14:20

      Вы часом не путаете «по моему [мнению это так | рукаву ползёт букашка]» и «по-моему»?


      1. buriy
        11.01.2017 15:17
        +1

        Речь идёт про парсинг реальных текстов, не все из которых написаны профессиональными лингвистами.
        Там ещё и не такие ошибки встречаются, и такие тексты тоже нужно правильно парсить.


        1. kloppspb
          11.01.2017 15:55

          А как вы собираетесь отличать правильный вариант от фантазии двоечника? IMHO, в спорных случаях парсер должен работать исключительно по правилам русского языка, иначе будет полный бардак. А здесь и спорного случая-то нет, во всяком случае без глубокого смыслового анализа текста.


          1. buriy
            11.01.2017 16:27
            +1

            Напомню: этот пост совершенно про другую задачу — разбиение текста на предложения.
            > в спорных случаях парсер должен работать исключительно по правилам русского языка, иначе будет полный бардак.
            Ваши слова означают, что вы не знаете, как решать задачу исправления ошибок для произвольных текстов, и умеете делать парсинг только для грамматичных текстов.
            Задача исправления ошибок комплексная, обычно решается на разных уровнях, потому что ошибки бывают совершенно разные.
            Конкретно, исправление для «по моему» чаще всего делается на уровне вероятностного морфологического парсера, который должен учитывать возможность отсутствия дефиса в словах. Также возможна корректировка принятого морфологическом парсером решения на уровне синтактико-семантического анализа.


  1. irastypain
    11.01.2017 15:45
    +1

    Было упомянуто исправление ошибок. А как быть, если была пропущена именно «точка»? Или какой-нибудь другой знак препинания, завершающий предложение.


    1. mnv
      11.01.2017 15:52

      С этим сложнее. Я так понял что Томита-парсер опирается именно на знаки препинания и правила там организованы вокруг знаков препинания и токенов.


      Тут от задачи зависит. Если нужно парсить новостные сайты например, то там текст более-менее аккуратно написан и грубые ошибки встречаются редко. Если нужно парсить текст из соцсетей, то нужно искать более продвинутый инструмент.


      1. irastypain
        11.01.2017 16:00

        По идее «правильный» парсер должен дополнительно опираться на синтаксические конструкции и семантику (тут сложнее). А пока по ощущениям: предложение есть то, что начинается с заглавной буквы и заканчивается на ". или! или? или ...", а в gzt файл как раз добавляются места спотыканий (то же «ул.»).

        В целом да, если текст более менее грамотный и не изобилует специфическими сокращениями, то этот вариант парсера более чем достаточный.