Русский
Русский
English
Статистика
Реклама

Склонение слов и инициалов в DelphiFreepascal

Добрый, предновогодний день всем! В этой статье я бы хотел рассказать, как мне пришлось вернуться в 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' годом!

Источник: habr.com
К списку статей
Опубликовано: 29.12.2020 18:11:47
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Delphi

Lazarus

Freepascal

Склонение слов

Склонение фио

Restapi

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru