LXF100-101:Урок грамматики

Материал из Linuxformat.

Перейти к: навигация, поиск
Aspell и Enchant Снабдите свои программы функцией проверки орфографии

Содержание

Очепятки не пройдут

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


Потребность в движках проверки орфографии растет с развитием общения через Интернет. Если раньше ошибки правописания подсвечивались разве что в текстовых редакторах и процессорах, то нынче это делают и браузеры, и программы для обмена мгновенными сообщениями. Сложно сказать, улучшает ли компьютерная проверка орфографии грамотность человека. С одной стороны, пользователь сразу видит свои ошибки и в следующий раз может написать слово правильно. Однако, полагаясь в грамотности на машину, не потакает ли человек лени своего разума?

На этом уроке мы не будем задаваться столь философскими вопросами, а просто рассмотрим, как задействовать движок проверки правописания Aspell в своих программах. Мы будем использовать API для языка С, а для большей наглядности практического применения ряд примеров будет дан с привязкой к GTK+ (мы публиковали серию статей о нем в период с LXF86 по LXF95, так что вы найдете все эти уроки по ссылке), однако, изложенные общие принципы работы применимы к любой библиотеке. В конце материала будет уделено немного места еще одному движку – Enchant, для сравнения. Также обратите внимание на Hunspell – он уже используется в OpenOffice.org, а со временем должен заменить MySpell и в продуктах Mozilla.

ЧАСТЬ 1: GNU Aspell

Aspell (http://aspell.net) – один из самых популярных движков проверки орфографии. Он состоит из консольной программы (которой, помимо прочего, можно передавать данные через канал), а также библиотеки с «сишным» API, хотя сам Aspell написан на С++. Работы с каналами мы касаться не будем.

Описанию API в документации Aspell уделено мало внимания – несколько примеров да примечания. Чтобы использовать Aspell в своей программе, надо изучить его исходные тексты – причем не только заголовочные файлы. Но сначала не будет излишним прочесть эту статью.

Начнем с проверки, установлена ли LibAspell в системе пользователя, и включения ее в параметры компилятора. По какой-то причине библиотека не оснащена модулем для pkg-config, поэтому искать pc-файл бесполезно. Вместо этого, в случае Autotools, следует добавить в файл configure.in (который будет обработан autogen.sh для создания скрипта configure) макросы для проверки наличия заголовочного файла aspell.h:

AC_CHECK_HEADER(aspell.h,
        LIBS=”$LIBS -laspell”
        AC_DEFINE(HAVE_LIBASPELL, 1, [use aspell]),
            [AC_MSG_ERROR([cannot find header for libaspell])]
        )

Напомню формат макроса AC_CHECK_HEADERS:

AC_CHECK_HEADERS (имя заголовочного файла, [действие в положительном случае], [действие в отрицательном])

Итак, мы проверяем, есть ли в системе файл aspell.h. Если есть, то в файле config.h определяется флаг HAVE_LIBASPELL, чтобы потом в коде программы мы могли написать что-то вроде:

#ifdef HAVE_LIBASPELL
 #include “aspell.h”
 #endif

В приведенном выше примере, для вывода сообщения об ошибке был использован макрос AC_MSG_ERROR. Помимо прочего, он вызывает прекращение работы сценария configure. То есть, если aspell.h не была найден, то и настройка исходных текстов будет провалена. Если такое поведение нежелательно, а наличие aspell.h – необязательное условие для сборки вашей программы, то проверка может выглядеть немного иначе:

AC_CHECK_HEADER(aspell.h,
            LIBS=”$LIBS -laspell”
            AC_DEFINE(HAVE_LIBASPELL, 1, [use aspell]),
                         echo “aspell.h not found”
            )

Здесь, вместо использования сурового макроса мы просто выводим сообщение, что aspell.h не найден.

Приступим к работе с библиотекой. Первым делом создадим экземпляр класса AspellConfig. Класс этот служит для управления всякими настройками. Именно всякими, поскольку он не входит в инкапсуляцию других классов Aspell, но может быть применен для изменения настроек других классов. Создадим экземпляр:

AspellConfig *config = new_aspell_config ();

После окончания работы его надо будет удалить при помощи функции delete_aspell_config(). Значения полей в классе можно менять при помощи функции aspell_config_replace(). Формат вызова таков:

aspell_config_replace (config, имя переменной-поля, новое значение);

Давайте для примера настроим класс под русскую локаль и кодировку UTF-8:

aspell_config_replace (config, “lang”, “ru”);
 aspell_config_replace (config, “encoding”, “UTF-8);

Замечу, что у пользователя может быть другая локаль, тогда ваше ‘ru’ будет бесполезным. Поэтому лучше определить локаль программно. Для этого нужно прочитать значение переменной окружения ‘LANG;. В GTK+ это делается вот так:

gchar *lang = g_getenv (“LANG”);
 if (lang)
    aspell_config_replace (config, “lang”, lang);
 else
    aspell_config_replace (config, “lang”, “C”);
 aspell_config_replace (config, “encoding”, “UTF-8);

Если, вдруг, переменная ‘LANG’ не установлена, то мы используем значение ‘C’ (стандартная англоязычная локаль). Что до кодировки, то указывая UTF-8, мы сообщаем движку Aspell, какую кодировку будем использовать для обмена данными с ним. Изнутри Aspell восьмибитный, но мы можем передавать ему текст в UTF-8 и получать его обратно тоже в UTF-8. Полученным текстом может быть, например, список предположительно правильных написаний для данного (переданного в параметре функции проверки) слова. Конечно, никто не мешает вам использовать другую кодировку, но в современных условиях UTF-8 – лучшая, если ваша библиотека виджетов ее поддерживает.

Еще одна подробность – в указанной вами кодировке Aspell будет сохранять новые слова в пользовательском словаре. Aspell создает такие словари в отдельных файлах (по одному на каждый использованный языковый модуль – то есть на тот модуль, куда было виртуально добавлено слово). Файлы эти лежат в домашнем каталоге пользователя. Например, имя файла с пользовательским словарем для русского языка – ~/.aspell.ru.pws.

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

Чтобы обеспечить программу возможностью использовать любой доступный модуль проверки орфографии, необходимо получить список установленных модулей. В приведенном ниже примере мы получаем такой список и выводим имена модулей в консоль:

const AspellDictInfo *entry;
 AspellConfig *config = new_aspell_config ();
 AspellDictInfoList *dlist = get_aspell_dict_info_list (config);
 AspellDictInfoEnumeration *dels = aspell_dict_info_list_elements (dlist);
 while ((entry = aspell_dict_info_enumeration_next (dels)) != 0)
      if (entry)
         printf (“%s\n”, entry->name);
 delete_aspell_dict_info_enumeration (dels);
 delete_aspell_config (config);

Итогом работы этого кода будет нечто вроде:

ru
en
en_CA

и так далее.

Полученные названия можно использовать в aspell_config_replace(), чтобы установить локаль движка Aspell:

aspell_config_replace (config, “lang”, локаль);

Теперь у нас есть все знания для того, чтобы настроить язык и кодировку. Но этого мало. Надо создать на основе этих данных экземпляр класса, отвечающего за проверку орфографии. Для этого API предоставляет нам две функции, которые мы рассмотрим подробно:

struct AspellCanHaveError* new_aspell_speller (struct AspellConfig *config);
 struct AspellSpeller* to_aspell_speller (struct AspellCanHaveError *obj);

Внутри первой функции происходят любопытные вещи. Aspell, как уже упоминалось выше, написан на С++, и «сишной» структуре AspellCanHaveError соответствует внутренний класс CanHaveError, а AspellSpeller – класс Speller. Итак, функция new_aspell_speller() создает экземпляр класса Speller. В случае ошибки new_aspell_speller() возвращает экземпляр CanHaveError. В противном же случае возвращается Speller, которого нужно «вытащить» из CanHaveError функцией to_aspell_speller(). Я понимаю, что API можно было сделать более логичным.

Вот пример. Сначала вызываем new_aspell_speller:

AspellCanHaveError *possible_err = new_aspell_speller(spell_config);

На этом этапе мы не знаем, что вернулось в possible_err на самом деле – экземпляр AspellCanHaveError или AspellSpeller. Поэтому объявляем spell_checker и проверяем possible_err – то есть вернулась ли ошибка. Если да, то печатаем сообщение об ошибке, а если нет, то функцией to_aspell_speller приводим possible_err к типу AspellSpeller.

AspellSpeller *spell_checker;
 if (aspell_error_number (possible_err) != 0)
     printf (“%s\n”, aspell_error_message (possible_err));
 else
    spell_checker = to_aspell_speller (possible_err);

Пара слов об освобождении памяти. Память для AspellCanHaveError освобождается с помощью функции delete_aspell_can_have_error(), а для AspellSpeller – delete_aspell_speller(). В приведенном выше коде, будь он в рабочей программе, освобождать память следовало бы так. Если вернулся экземпляр AspellSpeller, то память для AspellCanHaveError уже не нужно освобождать, потому что на самом-то деле в possible_err хранится не экземпляр AspellCanHaveError, а экземпляр AspellSpeller, и вызовы обеих функций уничтожения объекта приведут к ошибке сегментации во второй из них.

Лучший вариант работы с памятью таков. Если вернулся AspellCanHaveError – выполняем для него delete_aspell_can_have_error() и завершаем код проверки орфографии. Если вернулся AspellSpeller, то проверяем орфографию и очищаем память с помощью delete_aspell_speller(), однако delete_aspell_can_have_error() для possible_err уже НЕ вызываем.

Тонкая красная линия

Получив на руки работоспособный экземпляр AspellSpeller, мы, наконец, можем проверить написание слова. Для этого служит функция aspell_speller_check():

int aspell_speller_check (struct AspellSpeller *speller, const char *word, int word_size);

Она возвращает нулевое значение, если слова нет в словаре, 1 – если есть, и -1 в случае возникновения ошибки (в движке). То есть в рабочем коде, если нам нужно просто проверить, есть слово в словаре или нет, и нас не волнует возможная внутренняя ошибка, то можно писать так:

if (! aspell_speller_check (параметры)) {
    делаем что-то - например, подчеркиваем ошибочное слово
 }

Рассмотрим параметры функции поподробнее. Со speller’ом все понятно. Слово word, передаваемое для проверки, должно быть в той кодировке, которую вы задали для словарного модуля. word_size – размер (в байтах) этого слова, может быть -1, если мы имеем дело со строкой, завершающейся нулем (‘\0’). Обычно так оно и есть.

Попробуем Aspell в деле. Как проверить орфографию в стандартном виджете текстового редактора GTK+ (да и в GtkSourceView, см. LXF97)?

Как вы, наверное, знаете, изменение атрибутов участков текста в GtkTextView осуществляется с помощью объектов-тэгов. Тэг несет в себе параметры шрифта, цвет и так далее. Тэги хранятся в таблице, которая подключается к GtkTextBuffer (именно к буферу, а не к GtkTextView). Сначала надо создать таблицу, а затем поместить в нее тэг.

Давайте создадим пустую таблицу:

GtkTextTagTable *tags_table = gtk_text_tag_table_new ();

Затем создадим тэг, которым будем подсвечивать ошибочные слова:

GtkTextTag *tag_spell_err = gtk_text_tag_new (“spell_err”);

Обратите внимание – мы даем тэгу имя spell_err, чтобы потом иметь возможность обратиться к нему. Назначим тэгу следующие свойства: цвет букв (переднего плана, foreground) и подчеркивание (underline):

g_object_set (G_OBJECT (tag_spell_err), “foreground”, “red”, NULL);
 g_object_set (G_OBJECT (tag_spell_err), “underline”, PANGO_UNDERLINE_NORMAL, NULL);

В данном примере мы использовали стиль PANGO_UNDERLINE_NORMAL – обычное подчеркивание прямой линией. Но в библиотеке Pango, начиная с версии 1.4, появился стиль PANGO_UNDERLINE_ERROR, созданный специально для подчеркивания ошибок (волнистая линия мелким зигзагом). Как написать код, проверяющий версию Pango и устанавливающий стиль подчеркивания в зависимости от нее? А вот так:

#if defined(PANGO_VERSION_CHECK) && PANGO_VERSION_CHECK(1,4,0)
 g_object_set (G_OBJECT (tag_spell_err), “underline”, PANGO_UNDERLINE_ERROR, NULL);
 #else
 g_object_set (G_OBJECT (tag_spell_err), “underline”, PANGO_ UNDERLINE_SINGLE, NULL);
 #endif

Теперь тэг можно поместить в таблицу:

gtk_text_tag_table_add (tags_table, tag_spell_err);

Создадим отдельный текстовый буфер с указанной таблицей тэгов:

GtkTextBuffer *text_buffer = gtk_text_buffer_new (tags_table);

У GtkTextView уже есть буфер по умолчанию. Заменим его на новый буфер с нашей таблицей тэгов:

gtk_text_view_set_buffer (text_view, text_buffer);

Альтернативный вариант вариант – не создавать свой буфер и таблицу, а получить указатель на уже существующий буфер с помощью функции gtk_text_buffer_get_tag_table(), и добавить тэг в полученную таблицу. Решение зависит от вас и от архитектуры вашей программы.

Приведем код, отвечающий непосредственно за проверку содержимого текстового буфера на ошибки:

GtkTextIter iter;
 GtkTextIter a;
 GtkTextIter b;
 gchar *p = NULL;
 gchar *text;
 gtk_text_buffer_get_iter_at_offset (text_buffer, &iter, 0);
 if (gtk_text_iter_starts_word (&iter))
      a = iter;
 do
   {
     if (gtk_text_iter_starts_word (&iter))
        {
          b = iter;
          if (gtk_text_iter_backward_char (&b))
              a = iter;
          if (gtk_text_iter_forward_word_end (&iter))
              if (gtk_text_iter_ends_word (&iter))
                 {
                   text = gtk_text_iter_get_slice (&a, &iter);
                   if (text)
                      {
                        if (g_utf8_strlen (text, -1) > 1)
                           {
                             if (! aspell_speller_check (text_buffer, text, -1))
                                {
                                  gtk_text_buffer_apply_tag (text_buffer, gtk_text_tag_
 table_lookup(gtk_text_buffer_get_tag_table (text_buffer), “spell_err”), &a, &iter);
                                    g_free (text);
                                    continue;
                                  }
                             }
                          g_free (text);
                        }
                   }
            }
        }
 while (gtk_text_iter_forward_char (&iter));
 }

Неплохо, правда? Вначале мы получаем итератор iter, указывающий на начало буфера. Также у нас есть вспомогательные итераторы a и b, которых мы используем для более точного последовательного перебора букв в буфере. Перебирая в цикле один символ за другим, мы получаем слова. Текст каждого слова помещаем в переменную text и передаем эту переменную в функцию aspell_speller_check(). Если слова нет в словаре, то применяем цветовой тэг к участку в буфере, отмеченному итераторами, которые ограничивают текущее слово. Тэг применяется с помощью функции gtk_text_buffer_apply_tag(). Этой функции передаются следующие параметры: text_buffer – текстовый буфер, указатель на тэг. Его мы извлекаем по имени из таблицы тэгов, которая назначена данному буферу. Делается это так:

gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table (text_buffer), “spell_err”)

Оставшиеся два параметра – итераторы, задающие начало и конец отмечаемого тэгом участка в буфере. Вот вам задача для самостоятельного решения: перед вызовом кода подчеркивания ошибок надо убрать возможное прежнее подчеркивание, ведь есть вероятность, что пользователь уже проверял орфографию, а затем исправил некоторые слова. Поэтому, прежде чем заново подчеркивать ошибки, найдите в буфере все тэги с именем “spell_err” и удалите их.

Помощь зала

Помимо того, что Aspell может сообщить вам о наличии или отсутствии слова в словаре, он также дает список возможных вариантов верного написания предложенного слова. В приведенном ниже коде предполагается, что ошибочное слово хранится в переменной error_word:

AspellWordList *suggestions = aspell_speller_suggest (speller, error_word, -1);
 if (! suggestions)
    ; //например, выходим по return
 //иначе получаем список предположений:
 AspellStringEnumeration *elements = aspell_word_list_elements
 (suggestions);
 const char *word;
 //и перебирая их (elements) по одному, печатаем в консоль:
 while (word = aspell_string_enumeration_next (elements))
        printf (“%s \n”, word);
 //удаляем объект:
 delete_aspell_string_enumeration (elements);

Не буду вдаваться в подробности внутреннего устройства этих функций – скажу лишь, что API можно было сократить наполовину. Память, полученную от aspell_speller_suggest(), освобождать не нужно – возвращается указатель на const. Объект доступен до следующего вызова вышеупомянутой функции.

Вот мы и рассмотрели основные функции Aspell. Напоследок расскажу еще об одной, которой программисты пользуются очень часто – это добавление нового слова в словарь. Пример очевиден:

aspell_speller_add_to_personal (speller, word, strlen (word));
 aspell_speller_save_all_word_lists (speller);

Размер слова указывается в байтах, поэтому годится обычная strlen, даже если слово у вас хранится в UTF-8. И не забывайте вызывать aspell_speller_save_all_word_lists(), иначе слово хотя и добавитсяв текущую сессию проверки орфографии, но не будет сохранено в словарном файле.

ЧАСТЬ 2: Enchant

Enchant – побочный продукт разработчиков AbiWord (http://www.abisource.com/enchant). Собственно говоря, это и не движок сам по себе, а программная прослойка, предоставляющая доступ к другим движкам проверки орфграфии, как-то: Aspell, ISpell, MySpell, Uspell, Hspell, AppleSpell и Hunspell. Разработчики позаботились об удобном подключении библиотеки при компиляции – то бишь предоставили пакет для pkg-config. Поэтому, чтобы проверить наличие библиотеки и подключить ее к своей программе, надо добавить в configure.in примерно следующее:

echo -n “checking for enchant... “
 if pkg-config --exists enchant ; then
              LIBS=$LIBS `pkg-config --libs enchant `”
              CFLAGS=$CFLAGS `pkg-config --cflags enchant `”
              AC_DEFINE(ENCHANT_SUPPORTED, 1, [ENCHANT_
 SUPPORTED])
              echoyeselse
              echo “no”
 fi
        Ну, а в коде программы:
 #ifdef ENCHANT_SUPPORTED
 #include “enchant.h”
 #endif

Общение с Enchant происходит в кодировке UTF-8, вам не нужно указывать это напрямую. Инициализация словаря выполняется просто:

EnchantBroker *broker = enchant_broker_init ();
 EnchantDict *dict = NULL;
 gchar *lang = g_getenv (“LANG”);
 if (lang)
    dict = enchant_broker_request_dict (broker, lang);

Вначале создается брокер, у которого запрашиваем словарь: брокер ими заведует. В приведенном выше примере мы просим у брокера: «Дай нам словарь для языка текущей локали». Но ведь известно, что Enchant поддерживает одновременно несколько движков проверки орфографии. Стало быть, Enchant даст нам (прозрачно, разумеется) тот движок, в котором установлен модуль проверки нужного нам языка. Но что если такой модуль есть для нескольких движков? Например, для Aspell и MySpell? Для этих целей существует файл /usr/share/Enchant/Enchant.ordering, в котором задаются приоритеты движков. Расположение этого файла у вас может быть иным – всё зависит от того, где установлен Enchant. Может также существовать аналогичный пользовательский файл, хранящийся в ~/.enchant.

Далее, где именно Enchant ищет словари? Смотря какие. Для MySpell, Ispell, и Uspell по умолчанию – в подкаталогах /usr/share/enchant (опять же, у вас может быть иначе). Например, словарь MySpell ищется в /usr/share/enchant/myspell. Что до Aspell, то к нему Enchant ищет «общие» словари, без всякой привязки к конкретной установке.

Для проверки слова на правильность написания надо использовать следующую функцию:

int
 enchant_dict_check (EnchantDict *dict, const char *const word, ssize_tlen)

Она возвращает 0, если слово присутствует в словаре, положительное значение – если слова там нет, и отрицательное в случае внутренней ошибки движка. Параметры функции: dict – экземпляр словаря, word – передаваемое для проверки слово (в UTF8), len – длина этого слова в байтах, можно использовать strlen либо значение -1, если строка завершается нулем.

Получение списка предположительно верных написаний слова:

size_t out_n_suggs;
 gchar **words = enchant_dict_suggest (dict, s, -1, &out_n_suggs);

Здесь слова-предположения помещаются в строковой массив words. Количество элементов массива возвращается в переменной out_n_suggs. А чтобы освободить память, отведенную под этот массив, нужно сделать так:

enchant_dict_free_string_list (dict, words);

Наконец, после работы с брокером и словарем нужно тоже очищать память:

if (dict)
    enchant_broker_free_dict (broker, dict);
 if (broker)
    enchant_broker_free (broker);

Легко видеть, что API у Enchant более простое, чем у Aspell. Использование того или иного движка зависит от программы. Мне кажется, что наилучшее решение – это поддержка сразу нескольких движков в зависимости от того, какие из них установлены. Собственно, этим и занимается Enchant, но Aspell более традиционен для Linux и работает «из коробки» в большинстве дистрибутивов. Кроме того, даже при наличии Enchant, Aspell с его 70 словарями наверняка будет использоваться тем же Enchant в качестве движка. LXF

Личные инструменты
  • Купить электронную версию
  • Подписаться на бумажную версию