Склонение слов и инициалов в Delphi/Freepascal

от автора

Добрый, предновогодний день всем! В этой статье я бы хотел рассказать, как мне пришлось вернуться в legacy-проект на паскале, причем буквально перед тем, как навсегда распрощаться и с ним, и с лазарусом, и с отсутствием темной темы из коробки.

В прошлый раз я объяснял, что не являюсь программистом по роду профессиональной деятельности, но использую любимое хобби для автоматизации всего, что попадается под руку в работе юриста. Я уверен, что 90% всей юридической волокиты может быть успешно автоматизировано: ведение разнообразных баз и карточек, составление документов по шаблонам, контроль сроков выполнения задач, использование любых вспомогательных сервисов, уже имеющих свои api, для прикручивания автоматизации на конкретном рабочем месте и т.д. К этому нужно стремиться, чтобы по заветам Айзека Азимова высвободить время юриста для реализации основной задачи — размышлять над условиями договора и разводить демагогию в суде.


Так вот, много лет назад я сделал очень большой проект для облегчения своей офисной работы. Он собирал все данные по товарным знакам и патентам (а их несколько сотен), контролировал сроки оплаты патентных пошлин, формировал платежные поручения, договоры, заявления, и, разумеется, выдавал разнообразные отчеты. Собственно, почему в прошедшем времени? Проект вполне рабочий. Вот только разработан был по всем возможным антипаттернам, со всеми велосипедами и костылями, какие только обнаружили на Земле. Возвращаться в этот ролтон (или доширак) код, чтобы его отрефакторить, ой, как не хотелось, ведь здесь идеально подходит мем «А давайте всё перепишем на…»

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

В один из прекрасных дней весь проект был пересобран на 64-разрядной платформе, и, к удивлению моему, упала самая любимая часть: генерация договоров и заявлений с полной автоматизацией всей грамматики и морфологии — склонение инициалов, должностей и прочих слов по нужным падежам, а также учет единственного/множественного числа. Все дело в том, что старинная дельфийская и проприетарная библиотека padegUC, перестала быть бесплатной в своей 64-битной версии.

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

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

IInitialsMorpher = interface   function GetInitials(Initials: string): CasesResponse;   function GetWordsCase(Words: string): CasesResponse;   function GetGenderAndInitials(Initials: string; var Gender: TGender): CasesResponse; end;

Для универсальности интерфейс объявляет три функции, возвращающих примерно одно и то же, но по-разному: как инициалы (т.е. с заглавными буквами), как обычные слова, что подходит, например, для склонения должности или наименования объекта, и инициалы с дополнительным определением рода, чтобы выбирать между который и которая и т.п. Структура CasesResponse представляет собой обычный строковый массив с названием падежа в качестве индекса, а Gender — литеральное перечисление:

TWordCase = (Nominative, Gentitive, Dative, Accusative, Instrumental, Prepositional); TGender = (Male, Female, UnrecognizedGender); CasesResponse = array[TWordCase] of string;

Для реализации интерфейса я рассматривал нескольких кандидатов. В итоге остановился на следующих трех, и сразу укажу на достоинства и недостатки:

Сервис

Достоинства

Недостатки

DaData

— Отличная документация и идеальные примеры

— Корректная работа с инициалами с учетом национальных особенностей народов России

— Определение рода

— Склонение ФИО — платный сервис

— не меняет число

Pymorphy

github

— MIT лицензия, в оригинале существует как библиотека python

— Мощнейший морфологический анализатор на словарях Corpora (правильно разберет любой новояз)

— Масса информации на выходе, в т.ч. род, число, разбор на морфемы и лексемы, словарное ядро корня и т.д.

— работает только в отношении отдельного слова, не словосочетания, т.е. не учитывает контекст

Morphos

— MIT лицензия, в оригинале существует как библиотека php

— Корректная работа с инициалами

— Определение рода

— для меня нет, но для некоторых может быть важен беспорядок слов в запросе

Я поэкспериментировал со всеми этими сервисами, но для своих целей остановился на использовании Morphos, а подстановку единственного/множественного числа в нескольких участках текстов реализовал простыми строковыми шаблонами.

Поскольку все сервисы представляют собой rest api, а выдача результата чаще всего происходит в формате json, функцию, взаимодействующую с сервером по http, сразу выносим в общие утилиты:

generic function JSONfromRestUri<T>(Uri: string): T; var   HTTPSender: THTTPSend;   JSONStreamer: TJSONDeStreamer;   Json: TJSONObject; begin   HTTPSender := THTTPSend.Create;   JSONStreamer := TJSONDeStreamer.Create(nil);   HTTPSender.Clear;   Result := T.Create;   if not HTTPSender.HTTPMethod('GET', Uri) then    		raise EInOutError.Create(RESTOUT_ERROR);   JSON := GetJSON(HTTPSender.Document) as TJSONObject;   JSONStreamer.JSONToObject(JSON, Result);   FreeAndNil(JSON);   FreeAndNil(JSONStreamer);   FreeAndNil(HTTPSender); end;

Функция использует библиотеки Freepascal Synapse и Fpjson, поэтому соответствующие модули (httpsend, fpjson, fpjsonrtti) должны быть установлены и включены в uses. Десериализация ответа сервера из json в объект происходит с использованием rtti, т.е. все свойства такого объекта, во-первых, должны объявляться в секции published, во-вторых, иметь не самые сложные типы (примитивы, массивы, списки), и в-третьих, называться идентично полям в json. Скорее всего, декораторы и аннотация в стиле @SerializedName здесь не завезена, ну или я не нашел.

Строка запроса для сервиса Morphos выглядит следующим образом:

MORPHOS_URL = 'http://morphos.io/api/inflect-name?name=%s&_format=json';

Ответ сервера представляет собой json с массивом из шести строк, внутри — слово/словосочетание из запроса, склоненное по всем падежам русского языка в стандартном порядке ИРВДТП, само слово из запроса в поле name и определенный род в поле gender:

{     "name": "Иванов Иван",     "cases": [         "Иванов Иван",         "Иванова Ивана",         "Иванову Ивану",         "Иванова Ивана",         "Ивановым Иваном",         "об Иванове Иване"     ],     "gender": "m" }

Приступим же к конкретной реализации интерфейса IInitialsMorpher для сервиса Morpher. Сначала объявим класс, в который данные из json будут автоматически десериализоваться библиотеками Fpjson (в документации говорится, что классы должны быть потомками TPersistent):

TMorphosResponse = class(TPersistent)   private     fCases: TStrings;     fGender: string;     fName: string;   public     constructor Create;     destructor Destroy; override;   published     property name: string read fName write fName;     property cases: TStrings read fCases write fCases;     property gender: string read fGender write fGender;   end;

Добавляем класс реализации интерфейса:

TMorphosImpl = class(TInterfacedObject, IInitialsMorpher)   public     function GetInitials (Initials: string): CasesResponse;     function GetWordsCase (Words: string): CasesResponse;     function GetGenderAndInitials(Initials: string; var Gender: TGender): CasesResponse;   end;

Основной алгоритм сосредоточен в одной функции, а две другие лишь паразитируют на ней:

function TMorphosImpl.GetGenderAndInitials(Initials: string; var Gender: TGender): CasesResponse; var   inf: TWordCase;   i: integer = 0;   response: TMorphosResponse; begin   response := specialize JSONfromRestUri<TMorphosResponse>                 (Replacetext(Format(MORPHOS_URL, [Initials]), ' ', '+'));   for inf in TWordCase do begin       Result[inf] := response.cases[i];       inc(i);   end;   case response.gender of        'm': Gender := Male;        'f': Gender := Female;        else Gender := UnrecognizedGender;   end;   FreeAndNil(response); end;

Это функция, возвращающая массив склоненных ФИО и род в отдельном свойстве. Не трудно догадаться, что для реализации первой функции (только ФИО) нужно вызвать её же, но отбросить род, а для реализации другой — результат первой функции привести к нижнему регистру:

function TMorphosImpl.GetInitials(Initials: string): CasesResponse; var   MokeGender: TGender = UnrecognizedGender; begin   Result := GetGenderAndInitials(Initials, MokeGender); end;  function TMorphosImpl.GetWordsCase(Words: string): CasesResponse; var   inf: TWordCase; begin   Result := GetInitials(Words);   for inf in TWordCase do     Result[inf] := UTF8LowerString(Result[inf]); end;

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

Для себя я выбрал полу-фабрику полу-фасад, чтобы уже в этом классе добавить еще одну фичу (на самом деле не так уж и нужную, но вдруг, интернет все-таки кончится?) — кеширование результатов на диск для сокращения накладных расходов на http-запросы. И если для Morphos это не особо актуально, то для DaData даже очень, поскольку каждый запрос стоит 10 копеек.

Подробное описание реализации кэша я здесь не стану приводить, поскольку оно выходит за рамки темы. Скажу лишь, что для себя выбрал хранение значений в json файле и обработку их в памяти с помощью НashMap-типа из Generics.Collections — библиотеки, портированной в freepascal из delphi. Ключом при этом являются сами инициалы, и потенциально использование хэш-массива даст оптимальную скорость поиска на больших данных (от которых по идее нужно держаться подальше с помощью разных вариантов авто очистки кэша, ведь иногда и http-запрос может отработать быстрее, чем загрузка огромного словаря в память из файла и дальнейший поиск).

В конечном итоге использование имплементации Morphos выглядит так:

Morpher := TMorphFabric.Create(MORPHOS); //... response := Morpher.GetInitials(Text) StringList.AddStrings(response);

Для всех заинтересовавшихся свою получившуюся библиотеку с тестовым оконным приложением для win я буду хранить в открытом репозитории. Дорабатывать, скорее всего, я уже в ней ничего не стану, поскольку сейчас погрузился в открытое море мобильной разработки (kotlin) и python для реализации того самого мема «переписать всё на…»

Всех с наступающим Новым 2020+’1′ годом!

ссылка на оригинал статьи https://habr.com/ru/post/535532/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *