- Подписка на печатную версию:
- Подписка на электронную версию:
- Подшивки старых номеров журнала (печатные версии)
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]) echo “yes” else 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