Главная » Статьи » FreePascal

Проверка орфографии (spell checking) в вашей программе

1. Немного изысканий
2. Экспортируемые функции hunspell
3. Текст FPC модуля реализации класса-обертки hunspell


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

1. Немного изысканий

Первым делом я набрал "spelling fpc" в строке поиска Google и тут же получил ссылку на статью сайта Free Pascal and Lazarus Wiki.Из статьи я выяснил, что существует библиотека hunspell, которая позволяет довольно просто реализовать проверку орфографии программах, написанных на FPC.

Кроме того, что сама библиотека hunspell является open source проектом под LGPL/GPL/MPL tri-license, масса словарей к ней также, как правило, распространяются под LGPL.

Таким образом был сделан выбор в пользу этой библиотеки. Оставалось найти саму библиотеку. В той же статье была ссылка на свободный проект tomboy-notes, ранние релизы которого распространялись с 64-х битной библиотекой libhunspell.dll, которая была скомпилирована с оригинального исходного кода проекта hunspell.
К сожалению исторически так сложилось, что мой старенький десктоп, на котором я проводил все свои экзерсисы по программированию, работал под 32-х битной Windows 7. Работоспособная версия библиотеки для 32-х бит была неожиданно найдена в папке bin, установленного на моем же десктопе редактора GIMP — это была DLL libhunspell-1.3-0.dll. Однако первоначальные попытки работать с этой DLL были неуспешны. Изучение секции импорта с помощью objdump.exe показало, что кроме стандартных KERNEL32.dll и msvcrt.dll ей нужны еще две: libgcc_s_sjlj-1.dll, libstdc++-6.dll, а они в свою очередь требовали наличия libwinpthread-1.dll. То есть libhunspell-1.3-0.dll компилировалась GCC и линковалась c флагом -shared-libgcc. Нужные для libhunspell-1.3-0.dll библиотеки нашлись в той же папке bin GIMP'а.

Так теперь нужны были hunspell словари русского языка . Поиск привел к двум словарям:

Найденные DLL и словари позволили начать работу с hunspell, изучить ее применение в программах на FPC и написать класс-оболочку упрощающий доступ к функционалу библотеки.

Чтобы не быть привязанным к найденным файлам DLL, я решил попробовать самому скомпилировать hunspell. Для работы с С++, на котором написана библиотеки, у меня уже были установлены MSYS  + MinGW GCC v9.2.0, а также IDE Code::Blocks 20.03. После этого, скачав исходный код hunspell с сайта проекта, я создал DLL-проект Code::Blocks на основе файлов каталога src/hunspell из ветки master проекта на Github. После этого оставалось лишь скомпилировать проект, не забыв определить BUILDING_LIBHUNSPELL, а при линковке установить флаги -static-libgcc и -static-libstdc++, чтобы исключить динамическую линковку DLL GCC. Выполнив эти процедуры на 32- и 64-битных системах, я получил необходимые DLL на основе текущей версии hunspell 1.7libhunspell-1.7_32.dll и libhunspell-1.7_64.dll, соответственно.

2. Экспортируемые функции hunspell

Анализ файла DLL и заголовочных файлов hunspell показал, что библиотека экспортирует свыше 80-ти объектов (функций  и методов классов) С++. Применение этих объектов в программе, написанной на FPC, практически невозможно из-за различного подхода к name mangling, принятого в С++ и FPC.

К счастью в секции экспорта имеется еще 16 реализованных на "чистом" С функций-оберток для методов класса Hunspell. Именно их легко применить в FPC программе.

Перечислим часть этих функций и их функционал:

LIBHUNSPELL_DLL_EXPORTED Hunhandle* Hunspell_create(const char* affpath, const char* dpath);

— создание экземпляра speller'а, связанного с файлами словаря affpath (расширение aff) и dpath (расширение dic).

LIBHUNSPELL_DLL_EXPORTED void Hunspell_destroy(Hunhandle* pHunspell);

— удаление раннее созданного экземпляра speller'а.

LIBHUNSPELL_DLL_EXPORTED int Hunspell_add_dic(Hunhandle* pHunspell, const char* dpath);

— загрузка дополнительного файла  в раннее созданный экземпляра speller'а.

LIBHUNSPELL_DLL_EXPORTED int Hunspell_spell(Hunhandle* pHunspell, const char* word);

— проверка слова word, при правильном слове возвращается результат не равный нулю, иначе — 0.

LIBHUNSPELL_DLL_EXPORTED char* Hunspell_get_dic_encoding(Hunhandle* pHunspell);

— возвращает тип кодировки файлов загруженного словаря.

LIBHUNSPELL_DLL_EXPORTED int Hunspell_suggest(Hunhandle* pHunspell, char*** slst, const char* word);

— возвращает список корректных вариантов неправильного слова word. В параметре slst передается указатель на переменную, где будет размещен указатель на массив строк, которые содержат список корректных вариантов слова. Результат функции — число вариантов, если корректных вариантов нет, то возвращается 0, а *slts устанавивается равным NULL

LIBHUNSPELL_DLL_EXPORTED int Hunspell_analyze(Hunhandle* pHunspell, char*** slst, const char* word);

— выполняет морфологический анализ слова, результат возвращается в *slst аналогично функции Hunspell_suggest.

LIBHUNSPELL_DLL_EXPORTED int Hunspell_stem(Hunhandle* pHunspell, char*** slst, const char* word);

— возвращает основное слово, определенное в словаре. Результат возвращается в *slst аналогично функции Hunspell_suggest.

LIBHUNSPELL_DLL_EXPORTED int Hunspell_add(Hunhandle* pHunspell, const char* word);

— добавляет в загруженный словарь (run-time) speller'а слово word, файл словаря не изменяется.

LIBHUNSPELL_DLL_EXPORTED int Hunspell_add_with_affix(Hunhandle* pHunspell, const char* word, const char* example);

— добавляет в загруженный (run-time) словарь speller'а слово word, изменяемые формы которого образуются аналогично example — слово определенное в текущем загруженном словаре. Файл словаря не изменяется.

LIBHUNSPELL_DLL_EXPORTED int Hunspell_remove(Hunhandle* pHunspell, const char* word);

— удаляет из загруженного (run-time) словаря speller'а слово word, файл словаря не изменяется.

LIBHUNSPELL_DLL_EXPORTED void Hunspell_free_list(Hunhandle* pHunspell, char*** slst, int n);

— освобождает память, выделенную в функциях Hunspell_suggest, Hunspell_analyze и Hunspell_stem, под список slst.

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

3. Текст FPC модуля реализации класса-обертки hunspell

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

По умолчанию принимается, что имя файла DLL-библиотеки libhunspell-1.7_32.dll или libhunspell-1.7_64.dll, а путь к библиотеке определяется по стандартным правилам Windows. В случае необходимости изменения умолчания следует определить переменные DefaultDLLPath и DefaultHunspellLibName до создания первого экземпляра класса THunSpeller.

{ Модуль интерфейса с динамической библиотекой Hunspell.
  Основан на коде, который имеется во многих местах на форуме Lazarus и Github.
  Предполагается, что он может свободно использоваться кем-либо для любых целей.
 
  OlegZ 2020
}
unit ZHunSpellers;

{$mode objfpc}{$H+}

interface

uses
  Windows,
  Classes, SysUtils,
  LazUTF8;

{$Inline on}

//----------------------------------------------------------------------------------------
const
  // Имя файла DLL по умолчанию
  {$IfDef WIN32}
  cDefaultHunspellLibName = 'libhunspell-1.7_32.dll';
  {$Else}
  {$IfDef WIN64}
  cDefaultHunspellLibName = 'libhunspell-1.7_64.dll';
  {$Else}
  {$Error Bad target OS}
  {$EndIf}
  {$EndIf}

//----------------------------------------------------------------------------------------
// Обработчик исключений для ошибок в модуле
type
  EHunSpellerError = class(Exception)
  end;

//----------------------------------------------------------------------------------------
type

  { THunSpeller }

  THunSpeller = class
    private
     FDictionaryFullName: string; // полное имя файла dic загруженного словаря
     FSpeller: Pointer; // указатель на экземпляр speller'а - результат Hunspell_create
     // возвращает True, если speller готов к работе
     function GetReady: boolean; inline;
     // возвращает кодировку загруженного словаря
     function GetDictEncoding: string;
     // внутренняя процедура выполнения функций Hunspell_suggest, Hunspell_analyze и Hunspell_stem
     procedure iProcWithList(aFunc: pointer; const aWord: string; aList: TStrings);

    public
     // создание speller без загрузки словаря
     constructor Create;
     // создание speller с загрузкой словаря, файл которого задан именем aFullDictName
     // считается, что файл аффиксов (aff) находится в том же каталоге
     constructor Create(const aFullDictName: string);
     destructor Destroy; override;

     // загрузка словаря из файла aFullDictName, предыдущий словарь закрывается
     procedure OpenDictionary(const aFullDictName: string);
     // закрытие загруженного словаря
     procedure CloseDictionary;

     //----------- обертки функций DLL -----------
     // проверка слова aWord,возвращает True, если слово правильное
     function Spell(const aWord: string): boolean;
     // в списке aList возвращаются варианты неправильного слова aWord
     // если вариантов нет, то возвращается пустой список
     procedure Suggest(const aWord: string; aList: TStrings);
     // в списке aList возвращаются результаты морфологического анализа слова aWord
     procedure Analyze(const aWord: string; aList: TStrings);
     // в списке aList возвращаются варианты основы слова aWord
     procedure Stem(const aWord: string; aList: TStrings);
     // слово aWord добавляется в загруженный (run-time) словарь
     procedure Add(const aWord: string);
     // слово aWord добавляется в загруженный (run-time) словарь
     // изменяемые формы слова aWord образуются аналогично словарному слову aExample
     procedure AddWithAffix(const aWord, aExample: string);
     // слово aWord удаляется из загруженного (run-time) словаря
     procedure Remove(const aWord: string);

     property Ready: boolean read GetReady;
     property DictionaryFullName: string read FDictionaryFullName;
     property DictEncoding: string read GetDictEncoding;
  end;

//----------------------------------------------------------------------------------------
var
  DefaultDLLPath: string = ''; // путь поиска файла DLL-библиотеки
  DefaultHunspellLibName: string = cDefaultHunspellLibName; // имя файла DLL-библиотеки

// возвращает полное имя файла загруженной DLL-библиотеки
function GetDLLFullName: string; 

property DLLFullName: string read GetDLLFullName;

//****************************************************************************************
//****************************************************************************************
implementation

//----------------------------------------------------------------------------------------
// DLL Library control
//----------------------------------------------------------------------------------------
var 
  FDLLFullName: string; // полное имя файла загруженной DLL-библиотеки
  FLibHandle: THandle = NilHandle; // дескриптор DLL-библиотеки
  FAllLibProcLoaded: boolean = false; // = True, если успешно загружены адреса всех используемых функций

  // адреса необходимых функций DLL
  HS_create: pointer;
  HS_destroy: pointer;
  HS_spell: pointer;
  HS_suggest: pointer;
  HS_analyze: pointer;
  HS_stem: pointer;
  HS_free_list: pointer;
  HS_add: pointer;
  HS_add_with_affix: pointer;
  HS_remove: pointer;
  HS_get_dic_encoding: pointer;

  FSpellersCount: integer = 0; // счетчик активных speller'ов

//----------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------

// возвращает полное имя файла загруженной DLL
function GetDLLFullName: string;
begin
 Result := FDLLFullName;
end;

// внутренняя функция получения адреса экспортируемой функции по имени
// при ошибке генерируется прерывание EHunSpellerError 
function iGetProcAddr(const aFuncName: string): pointer;
begin
  Result := system.GetProcAddress(FLibHandle, aFuncName);
  if not Assigned(Result) then
    raise EHunSpellerError.CreateFmt('Failed to find functions "%s" in "%s"',
     [aFuncName, FDLLFullName]);
end;

// получение адресов всех используемых функций DLL
procedure SetupFuncVars;
begin
  HS_create := iGetProcAddr('Hunspell_create');
  HS_destroy := iGetProcAddr('Hunspell_destroy');
  HS_spell := iGetProcAddr('Hunspell_spell');
  HS_suggest := iGetProcAddr('Hunspell_suggest');
  HS_analyze := iGetProcAddr('Hunspell_analyze');
  HS_stem := iGetProcAddr('Hunspell_stem');
  HS_free_list := iGetProcAddr('Hunspell_free_list');
  HS_add := iGetProcAddr('Hunspell_add');
  HS_add_with_affix := iGetProcAddr('Hunspell_add_with_affix');
  HS_remove := iGetProcAddr('Hunspell_remove');
  HS_get_dic_encoding := iGetProcAddr('Hunspell_get_dic_encoding');
  FAllLibProcLoaded := true;
end;

// загрузка DLL
procedure OpenLib;
const
  cBufLen = 1024;
var
  s: string;
begin
  s := DefaultDLLPath + DefaultHunspellLibName;
  FLibHandle := system.LoadLibrary(s);
  if FLibHandle = NilHandle then
    raise EHunSpellerError.CreateFmt('Failed to load library: %s', [FDLLFullName]);

  SetLength(s, cBufLen);
  GetModuleFileName(FLibHandle, @s[1], cBufLen);
  SetLength(s, strlen(@s[1]));
  FDLLFullName := WinCPToUTF8(s);
  SetupFuncVars;
end;

// выгрузка DLL
procedure CloseLib;
begin
  if (FLibHandle <> NilHandle) then begin
    system.FreeLibrary(FLibHandle);
    FLibHandle := NilHandle;
    FAllLibProcLoaded := false;
  end;
end;

//----------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------

{ THunSpeller }

// возвращает True, если экземпляр speller'а готов к работе
function THunSpeller.GetReady: boolean;
begin
  Result := (FLibHandle <> NilHandle) // DLL загружена
       and FAllLibProcLoaded // все используемые функции загружены
       and (FSpeller <> nil); // speller создан
end;

//----------------------------------------------------------------------------------------
// возвращает тип кодировки файла словаря
function THunSpeller.GetDictEncoding: string;
//hunspell.h -> char* Hunspell_get_dic_encoding(Hunhandle* pHunspell);
type
  THSFunc_get_dic_encoding = function(spell: Pointer): PChar; cdecl;
var
  p: pchar;
begin
  if not Ready then exit('Speller is not ready');

  p := THSFunc_get_dic_encoding(HS_get_dic_encoding)(FSpeller);
  if p = nil then exit('Encoding string not found');

  SetLength(Result, strlen(p));
  Move(p^, Result[1], strlen(p));
end;

//----------------------------------------------------------------------------------------
// создание экземпляра speller'а без открытия словаря
constructor THunSpeller.Create;
begin
  if FLibHandle = NilHandle then
    OpenLib;
end;

//----------------------------------------------------------------------------------------
// создание экземпляра speller'а с открытием словаря
constructor THunSpeller.Create(const aFullDictName: string);
begin
  if FLibHandle = NilHandle then
   OpenLib;
  OpenDictionary(aFullDictName);
end;

//----------------------------------------------------------------------------------------
destructor THunSpeller.Destroy;
begin
  CloseDictionary;
  inherited Destroy;
end;

//----------------------------------------------------------------------------------------
// открытие словаря из файла aFullDictName, 
// ранее открытый словарь предварительно закрывается
procedure THunSpeller.OpenDictionary(const aFullDictName: string);
//hunspell.h -> Hunhandle* Hunspell_create(const char* affpath, const char* dpath);
type
  THSFunc_create = function(aff_file: PChar; dict_file: PChar): Pointer; cdecl;
var
  FullAffName : string;
begin
  CloseDictionary;
  FDictionaryFullName := aFullDictName;
  FullAffName := UTF8Copy(aFullDictName, 1, UTF8Length(aFullDictName) - 3) + 'aff';
  FSpeller := THSFunc_create(HS_create)(PChar(UTF8ToWinCP(FullAffName)), PChar(UTF8ToWinCP(aFullDictName)));
  if FSpeller = nil then
    raise EHunSpellerError.CreateFmt('Failed to open Dictionary "%s"', [aFullDictName]);

  inc(FSpellersCount);
end;

//----------------------------------------------------------------------------------------
// закрытие словаря
procedure THunSpeller.CloseDictionary;
//hunspell.h -> Hunspell_destroy(Hunhandle* pHunspell);
type
  THSFunc_destroy = procedure(spell: Pointer); cdecl;
begin
  if Ready then begin
    THSFunc_destroy(HS_destroy)(FSpeller);
    FSpeller := nil;
    dec(FSpellersCount);
  end;
end;

//----------------------------------------------------------------------------------------
// проверка слова aWord,возвращает True, если слово правильное
function THunSpeller.Spell(const aWord: string): boolean;
//hunspell.h -> int Hunspell_spell(Hunhandle* pHunspell, const char* );
type
  THSFunc_spell = function(spell: Pointer; word: PChar): Boolean; cdecl;
begin
  if Ready then
    Result := THSFunc_spell(HS_spell)(FSpeller, PChar(aWord))
  else
    Result := false;
end;

//----------------------------------------------------------------------------------------
// приватная процедура вызова функций DLL, которые возвращают результат в виде списка слов:
// Hunspell_suggest, Hunspell_analyze и Hunspell_stem
procedure THunSpeller.iProcWithList(aFunc: pointer; const aWord: string; aList: TStrings);
type
  THSFuncWithList = function(spell: Pointer; out slst: PPChar; word: PChar): Integer; cdecl;
  //hunspell.h -> void Hunspell_free_list(Hunhandle* pHunspell, char*** slst, int n);
  THSFunc_free_list = procedure(spell: Pointer; var slst: PPChar; n: integer); cdecl;
var
  i, len: Integer;
  SugList, Words: PPChar;
begin
  aList.Clear;
  if not Ready then
    exit;

  try
    len := THSFuncWithList(aFunc)(FSpeller, SugList, PChar(aWord));
    Words := SugList;
    for i := 1 to len do begin
      aList.Add(PChar(Words^));
      Inc(Words);
    end;
  finally
    // освобождение списка слов
    THSFunc_free_list(HS_free_list)(FSpeller, SugList, len);
  end;
end;

//----------------------------------------------------------------------------------------
// в списке aList возвращаются варианты неправильного слова aWord
// если вариантов нет, то возвращается пустой список
procedure THunSpeller.Suggest(const aWord: string; aList: TStrings);
//hunspell.h -> int Hunspell_suggest(Hunhandle* pHunspell, char*** slst, const char* word);
begin
  iProcWithList(HS_suggest, aWord, aList);
end;

//----------------------------------------------------------------------------------------
// в списке aList возвращаются результаты морфологического анализа слова aWord
procedure THunSpeller.Analyze(const aWord: string; aList: TStrings);
//hunspell.h -> int Hunspell_analyze(Hunhandle* pHunspell, char*** slst, const char* word);
begin
  iProcWithList(HS_analyze, aWord, aList);
end;

//----------------------------------------------------------------------------------------
// в списке aList возвращаются варианты основы слова aWord
procedure THunSpeller.Stem(const aWord: string; aList: TStrings);
//hunspell.h -> int Hunspell_stem(Hunhandle* pHunspell, char*** slst, const char* word);
begin
  iProcWithList(HS_stem, aWord, aList);
end;

//----------------------------------------------------------------------------------------
// слово aWord добавляется в загруженный (run-time) словарь
procedure THunSpeller.Add(const aWord: string);
//hunspell.h -> int Hunspell_add(Hunhandle* pHunspell, const char* word);
type
  THSFunc_add = function(spell: Pointer; word: PChar): Integer; cdecl;
begin
  if Ready then
    THSFunc_add(HS_add)(FSpeller, Pchar(aWord));
end;

//----------------------------------------------------------------------------------------
// слово aWord добавляется в загруженный (run-time) словарь
// изменяемые формы слова aWord образуются аналогично словарному слову aExample
procedure THunSpeller.AddWithAffix(const aWord, aExample: string);
//hunspell.h -> int Hunspell_add_with_affix(Hunhandle* pHunspell, const char* word, const char* example);
type
  THSFunc_add_with_affix = function(spell: Pointer; word: PChar; example: PChar): Integer; cdecl;
begin
  if Ready then
    THSFunc_add_with_affix(HS_add_with_affix)(FSpeller, PChar(aWord), PChar(aExample));
end;

//----------------------------------------------------------------------------------------
// слово aWord удаляется из загруженного (run-time) словаря
procedure THunSpeller.Remove(const aWord: string);
//hunspell.h -> int Hunspell_remove(Hunhandle* pHunspell, const char* word);
type
  THSFunc_remove = function(spell: Pointer; word: PChar): Integer; cdecl;
begin
  if Ready then
    THSFunc_remove(HS_remove)(FSpeller, Pchar(aWord));
end;

//----------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------
finalization
  // выгрузка DLL при завершении работы программы
  CloseLib;
  // проверка завершения всех экземпляров speller'ов
  if FSpellersCount > 0 then
    raise EHunSpellerError.CreateFmt('Not disposed spellers: %d', [FSpellersCount]);
end.

В каталоге файлов сайта размещен материал, содержащий исходный код модуля, справочный файл в формате chm, а также файлы DLL для 32-х и 64-х разрядных ОС Windows, которые автор получил компиляцией исходного кода проекта  hunspell v.1.7 с помощью gcc (MinGW.org GCC Build-2) 9.2.0.

В продолжении темы проверки орфографии в программах FPC+Lazarus будет описана интеграция данного функционала со стандартным компонентом TMemo.

Продолжение следует...

К началу

Категория: FreePascal | Добавил: zoleg5763 (11.09.2020)
Просмотров: 434 | Теги: FreePacsal, проверка орфографии, spelling, Hunspell | Рейтинг: 0.0/0
Всего комментариев: 0
avatar