Бібліотека Strutext обробки текстів на C++ - реалізація лексичного рівня

Базові принципи
Цей текст є продовженням поста про бібліотеці Strutext обробки текстів на мові C++. Тут буде описана реалізація лексичного рівня представлення мови, зокрема, реалізація морфології.



За поданням автора, є наступні основні завдання, які необхідно вирішити при реалізації програмної моделі лексичного рівня мови подання:
  1. Виділення з вихідного тексту ланцюжків символів, що мають сенс, тобто представлення тексту у вигляді послідовності слів.
  2. Ототожнення виділених ланцюжків як елементів лексичних типів.
  3. Визначення для виділеної ланцюжки його лексичних атрибутів (про них трохи нижче).
Лексичні типи зазвичай представляють як кінцеві безлічі ланцюжків символів, що мають в пропозиціях мови один і той же зміст. Елементи лексичного класу зазвичай називають словоформами, само безліч словоформ — парадигмою, а лексичний тип — словом або леммой. Наприклад, лексичний тип «мама» складається з словоформ { мама, мами, мамою, ..., мами, мамам,… }.

Лексичний типи поділяються на синтаксичні категорії — частини мови. Частина мови визначає роль, яку слово відіграє в реченні мови. Ця важлива роль при визначенні правильності місця слова в реченні і, отже, визначенні сенсу пропозиції. Відомі частини мови російської мови: іменник, прикметник, дієслово, прислівник і т.д.

Словоформи лексичного типу можуть мати властивості. Такі властивості ще називають лексичними атрибутами або лексичними ознаками. Види цих властивостей залежать від синтаксичної категорії, до якої належить даний лексичний тип. Наприклад, для іменників важливу роль відіграє відмінок словоформи, а для дієслів такого атрибуту не є.

Які конкретно синтаксичні категорії використовуються для групування лексичних типів і які у них є лексичні атрибути залежить як від реалізованого мови, так і від конкретної реалізованої моделі лексичного аналізу. Нижче буде розглянуто лексична модель АОТ.

Лексичні неоднозначності
Може так статися, що в процесі вилучення слів із вихідного тексту, виникнуть неоднозначності. Тут розглядаються неоднозначності двох родів:
  1. Неоднозначності першого роду виникають у процесі привласнення виділеної з тексту ланцюжком символів лексичного типу. Розглянемо приклад «мама мила раму». Тут ланцюжок символів «мила» може бути дієсловом «мити», а також може бути іменником «мило». Такі випадки неоднозначності називаються ще лексичної омонімією.
  2. Неоднозначності другого роду виникають в процесі нарізки вихідного тексту на ланцюжки слів. У більшості природних мов слова відокремлені один від одного пробілами, хоча цей принцип іноді може порушуватися (в якості прикладу можна навести композити в німецькій мові). Але в мовах програмування є цікаві приклади. Розглянемо, наприклад, вираз вигляду «a >> b» у мові C++. У класичному C це вираз трактується однозначно: ідентифікатор «a», оператор правого побітового зсуву ">>", ідентифікатор «b». Але в останніх версіях мови C++ цей вислів може означати кінець списку шаблонних параметрів, коли в якості останнього параметра у списку також виступає шаблон. У цьому випадку послідовність слів буде така: ідентифікатор «a», кінець списку параметрів шаблону ">", кінець списку параметрів шаблону ">", ідентифікатор «b».
У цьому тексті ми розглядаємо тільки лексичні неоднозначності першого роду.

Морфологічна модель словника АОТ
В бібліотеці Strutext реалізована морфологічна модель від АОТ. Тому приділимо її опис деякий місце.

У словнику АОТ кожний лексичний тип задається двома параметрами:
  1. Рядком основи (кореня слова), до якої приєднуються суфікси для утворення словоформ.
  2. Номером парадигми відмінювання, яка являє собою список пар (суфікс, набір лексичних ознак).


Комбінацій наборів лексичних ознак є відносно небагато, вони перераховані у спеціальному файлі і кожна така комбінація закодована двохбуквеним кодом. Наприклад:
аа A З мр,од,їм
аб A З мр,од,рд
Еф A З мр,од,рд,2
...
іа Y П прев,мр,од,їм,од,але
іб Y П прев,мр,од,рд,од,але
...
кб a Р дст,нст,1л,од
кв a Р дст,нст,1л,мн
кг a Р дст,нст,2л,од
...

Тут перший елемент кожного рядка — це дволітерний код набору, третій елемент — код частини мови (З — іменник, П — прикметник, дієслово тощо), далі через кому перераховані коди граматичних ознак.

Файл опису словника складається з п'яти розділів, з яких два розділу найбільш важливі. Цей розділ опису парадигм відмінювання і розділ основ (лексичних типів). Кожен рядок у цьому розділі являє собою парадигму відмінювання. У розділі опису типів лексичних разом з основою задається номер рядка парадигми відмінювання.

Розглянемо, наприклад, слово «зеленка». Лексичний тип цього слова у словнику АОТ задається рядком виду
ЗЕЛЕН 15 12 1 Фа -

Тут число 15 — це номер парадигми відмінювання в розділі парадигм. Виглядає рядок даної парадигми наступним чином:
%КА*га%КИ*гб%КЕ*гв%КУ*рр%КОЙ*гд%КОЮ*гд%КЕ*ге%КИ*гж%ОК*гз%КАМ*гі%КИ*гй%КАМИ*цк%КАХ*гл

Кожна пара в парадигмі відокремлена від іншого символом "%", а елементи пар відокремлюються один від одного символом ".*". Перша пара (КА, га) задає словоформу зелен + ка = зеленка і має набір лексичних атрибутів: га = G З жр, од, їм = іменник жіночого роду, однини, називного відмінка. Відповідним чином можна розшифрувати й інші пари парадигми.

Спосіб кодування слів, що використовується в АОТ, має свої переваги і недоліки. Обговорювати їх тут ми не будемо, зазначимо лише цікавий факт: у словнику є лексичні типи з порожньою основою. Наприклад, слово «людина» в множині представлена словоформой «люди», яка не має загальної основи, з формою «людина». Тому це слово доводиться задавати простим перерахуванням словоформ:
%ОСІБ*аа%ЛЮДИНУ*аб%ЛЮДИНІ*ав%ЛЮДИНУ аг%ЛЮДИНОЮ*пекло%ЛЮДИНУ*ае%ЛЮДИ*аж%ЛЮДЕЙ*аз%ОСІБ*аз%ЛЮДЕЙ*аі%ЛЮДЕЙ*аі%ЛЮДЕЙ*ай%ЛЮДЬМИ*ак%ЛЮДЬМИ*ак%ЛЮДЕЙ*ал%ЛЮДЯХ*ал

Цю парадигму можна використовувати і з іншими словами (мають непорожній корінь), таким як боголюдина і мавполюдина.

Розглянемо трохи докладніше набір синтаксичних категорій і відповідних їм лексичних атрибутів словника АОТ.

Синтаксичні категорії словника АОТ
Як вже було зазначено вище, синтаксичні категорії словника АОТ задаються в окремому файлі і являють собою набори рядків, в яких двохбуквеним кодами задаються частина мови та набір лексичних атрибутів. В бібліотеці Strutext частини мови та їх атрибути представлені у вигляді ієрархії класів на C++. Розглянемо цю реалізацію докладніше.

Моделі синтаксичних категорій словника АОТ задані в директорії «morpho/models». Представлені моделі для російської та англійської мов. Розглянемо деякі фрагменти файли morpho/models/rus_model.h, в якому представлено опис моделі російської мови.

Базовий клас для всіх моделей — це абстрактний клас PartOfSpeech, який містить мітку мови у вигляді перерахування, а також задає віртуальний метод для повернення цієї мітки:
class PartOfSpeech : private boost::noncopyable {
public:
/// Type of smart pointer to the class object.
typedef boost::shared_ptr<PartOfSpeech> Ptr;

/// Language tag definitions.
enum LanguageTag {
UNKNOWN_LANG = 0 ///< Unknown language.

, RUSSIAN_LANG = 1 ///< Russian language.
, ENGLISH_LANG = 2 ///< English language.
};

/// Language tag.
virtual LanguageTag GetLangTag() const = 0;

/// Virtual for destruction abstract class.
virtual ~PartOfSpeech() {}
};


Від цього класу успадкований базовий клас для всіх синтаксичних категорій російської мови:
struct RussianPos : public PartOfSpeech {
/// Type of smart pointer to the class object.
typedef boost::shared_ptr<RussianPos> Ptr;

/// Possible parts of speech.
enum PosTag {
UNKNOWN_PS = 0 ///< Unknown part of speech.

, NOUN_PS = 1 ///< іменник
, ADJECTIVE_PS = 2 ///< прикметник
, PRONOUN_NOUN_PS = 3 ///< займенник-іменник
, VERB_PS = 4 ///< дієслово в особовій формі
, PARTICIPLE_PS = 5 ///< причастя
, ADVERB_PARTICIPLE_PS = 6 ///< деепричастие
, PRONOUN_PREDICATIVE_PS = 7 ///< займенник-предикатив
, PRONOUN_ADJECTIVE_PS = 8 ///< местоименное прикметник
, NUMERAL_QUANTITATIVE_PS = 9 ///< числівник (кількісне)
, NUMERAL_ORDINAL_PS = 10 ///< порядкове числівник
, ADVERB_PS = 11 ///< прислівник
, PREDICATE_PS = 12 ///< предикатив
, PREPOSITION_PS = 13 ///< привід
, CONJUCTION_PS = 14 ///< союз
, INTERJECTION_PS = 15 ///< вигук
, PARTICLE_PS = 16 ///< частка
, INTRODUCTORY_WORD_PS = 17 ///< вступне слово

, UP_BOUND_PS
};

/// Number.
enum Number {
UNKNOUN_NUMBER = 0 ///< Unknown number.

, SINGULAR_NUMBER = 0x01 ///< Єдине.
, PLURAL_NUMBER = 0x02 ///< Множина.
};

/// Language.
enum Lang {
NORMAL_LANG = 0 // Normal language.

, SLANG_LANG = 1
, ARCHAIZM_LANG = 2
, INFORMAL_LANG = 3
};

/// Gender definitions.
enum Gender {
UNKNOWN_GENDER = 0 ///< Unknown gender value.

, MASCULINE_GENDER = 0x01 ///< чоловіча
, FEMININE_GENDER = 0x02 ///< жіноча
, NEUTER_GENDER = 0x04 ///< середній
};

/// Case definition.
enum Case {
UNKNOWN_CASE = 0 ///< Unknown case.

, NOMINATIVE_CASE = 1 ///< називний
, GENITIVE_CASE = 2 ///< родовий
, GENITIVE2_CASE = 3 ///< другий родовий
, DATIVE_CASE = 4 ///< давальний
, ACCUSATIVE_CASE = 5 ///< знахідний
, INSTRUMENTAL_CASE = 6 ///< орудний
, PREPOSITIONAL_CASE = 7 ///< прийменниковий
, PREPOSITIONAL2_CASE = 8 ///< другий прийменниковий
, VOCATIVE_CASE = 9 ///< звательный
};

/// Time.
enum Time {
UNKNOWN_TIME = 0 ///< Unknown time.

, PRESENT_TIME = 0x01 ///< даний
, FUTURE_TIME = 0x02 ///< майбутнє
, PAST_TIME = 0x04 ///< минув
};

/// Person.
enum Person {
UNKNOWN_PERSON = 0 ///< Unknown person.

, FIRST_PERSON = 0x01 ///< перше
, SECOND_PERSON = 0x02 ///< друге
, THIRD_PERSON = 0x04 ///< третє
};

/// Entity kind.
enum Entity {
UNKNOWN_ENTITY = 0 ///< Unknown entity, ordinal for words.

, ABBREVIATION_ENTITY = 1 ///< абревіатури.
, FIRST_NAME_ENTITY = 2 ///< імена.
, MIDDLE_NAME_ENTITY = 3 ///< по батькові.
, FAMILY_NAME_ENTITY = 4 ///< прізвища.
};

/// Animation.
enum Animation {
UNKNOWN_ANIMATION = 0

, ANIMATE_ANIMATION = 0x01 ///< одушевлений.
, INANIMATE_ANIMATION = 0x02 ///< неживий.
};

/// Voice defintion.
enum Voice {
UNKNOWN_VOICE = 0 ///< Unknown voice.

, ACTIVE_VOICE = 0x01 ///< дійсний заставу.
, PASSIVE_VOICE = 0x02 ///< пасивний стан.
};

/// Language tag.
LanguageTag GetLangTag() const { return RUSSIAN_LANG; }

/// Class is absract one -- virtual city.
virtual ~RussianPos() {}

/// Get part of speech tag.
virtual PosTag GetPosTag() const = 0;

/// Serialization implementaion.
virtual void Serialize(uint32_t& out) const = 0;

/// Desirialization implementation.
virtual void Deserialize(const uint32_t& in) = 0;

/// Write POS signature.
static void WritePosSign(PosTag pos, uint32_t& out) {
// Write to lower 5 bits.
out |= static_cast<uint32_t>(pos);
}

/// Read POS signature.
static PosTag ReadPosSign(const uint32_t& in) {
return PosTag(in & 0x1f);
}
};

У класі задані мітки синтаксичних категорій у вигляді перерахування PosTag, а також визначено лексичні атрибути. Крім, власне, граматичної складової, в класі визначено методи Serialize і Deserialize для перетворення/з бінарний формат. Для кожного синтаксичного типу визначено перетворення в чотири байти, представлені типом uint32_t.

Клас RussianPos абстрактний, від нього успадковані класи, що представляють конкретні синтаксичні категорії. Наприклад, клас Noun задає іменник:
struct Noun : public RussianPos {
Noun()
: number_(UNKNOUN_NUMBER)
, lang_(NORMAL_LANG)
, gender_(UNKNOWN_GENDER)
, case_(UNKNOWN_CASE)
, entity_(UNKNOWN_ENTITY) {}

/// Get part of speech tag.
PosTag GetPosTag() const { return NOUN_PS; }

/**
* \brief Serialization implementaion.
*
* Binary map of the object:
* 13 3 4 3 2 2 5
* ----------------------------------------------------------- 
* Unused | Entity | Case | Gender | Lang | Number | POS tag |
* -----------------------------------------------------------
*
* \param[out] ob The buffer to write to.
*/
void Serialize(uint32_t& ob) const {
ob |= static_cast<uint32_t>(number_) << 5;
ob |= static_cast<uint32_t>(lang_) << 7;
ob |= static_cast<uint32_t>(gender_) << 9;
ob |= static_cast<uint32_t>(case_) << 12;
ob |= static_cast<uint32_t>(entity_) << 16;
}

/**
* \brief Desirialization implementaion.
*
* Binary map of the object:
* 13 3 4 3 2 2 5
* ----------------------------------------------------------- 
* Unused | Entity | Case | Gender | Lang | Number | POS tag |
* -----------------------------------------------------------
*
* \param ib The buffer to write to.
*/
void Deserialize(const uint32_t& ib) {
number_ = static_cast<Number>((ib & 0x0060) >> 5);
lang_ = static_cast<Lang>((ib & 0x0180) >> 7);
gender_ = static_cast<Gender>((ib & 0x0e00) >> 9);
case_ = static_cast<Case>((ib & 0xf000) >> 12);
entity_ = static_cast<Entity>((ib & 0x070000) >> 16);
}

Number number_;
Lang lang_;
Gender gender_;
Case case_;
Entity entity_;
};

У класі іменників зберігаються лексичні атрибути: число, вид мови (звичайний, анахронізм, розмовний тощо), підлога, відмінок і ознака імені або абревіатури.

Кінцеві автомати для кодування словників
Для зберігання словників, а також ефективного вилучення зі словника даних слова, в бібліотеці Strutext використовуються кінцеві автомати. Кінцеві автомати задаються відповідними C++ типами директорії автоматів.

Нагадаємо, кінцевий автомат задається функцією переходів, що деяким парам (стан, символ) зіставляє деякий стан: delta: Q x V --> Q. Є одне початковий стан, в якому автомат починає свою роботу і деяку кількість «допускають» станів. Автомат читає вхідні рядка посимвольно, якщо для поточного стану і прочитаного символу функція переходів дає у відповідність деякий стан, то автомат «переходить» у цей новий стан, після чого цикл читання нового символу починається заново. Автомат може зупиниться у двох випадках: якщо немає переходу по парі (поточний стан, прочитаний символ) і якщо прочитана весь ланцюжок символів до кінця. У першому випадку вхідні ланцюжок вважається не допускається даний автоматом, у другому випадку ланцюжок допускається, якщо після зупинки автомат знаходиться в одному з допускають станів.

Таким чином, кожен раз, коли прочитаний символ вхідний ланцюжка, автомат стикається з завданню пошуку відповідності парі (стан, символ) нового стану. В бібліотеці Strutext реалізація цієї функції пошуку виділена в окремий клас під назвою Transition. Автомат являє собою масив об'єктів класу Transition, заданих для кожного стану (автоматів/fsm.h):
template < typename TransImpl>
struct FiniteStateMachine {
/// Type of transition table.
typedef TransImpl Transitions;
...
/// State definition.
struct State {
Transitions trans_; ///< Move table.
bool is_accepted_; ///< Is the state accepptable.

/// Default initialization.
explicit State(bool is_accepted = false) : is_accepted_(is_accepted) {}
};
/// Type of states' list.
typedef std::vector<State> StateTable
...
StateTable states_; ///< The table of states.
};

Тут параметр шаблону TransImpl якраз і представляє функцію переходів.

В бібліотеці Strutext запрограмовано два способи реалізації функції переходів. Один спосіб заснований на звичайному std::map (автоматів/flex_transitions.h), де в якості ключа виступає код символу, а значення — номер стану. Інший спосіб (автоматів/flat_transitions.h) заснований на розрідженому масиві, коли виділяється масив, відповідний можливим кодами символів. В кожному елементі масиву знаходиться код стану. Значення нуль зарезервовано за некоректним станів, тобто означає, що переходу не є. Якщо ненульове значення, то дана пара (індекс масиву = код символу, номер стану в комірці масиву) задає перехід.

Клас FiniteStateMachine не в змозі сказати про вхідний ланцюжку що-небудь, крім того, що дана ланцюжок допускається. Для зберігання додаткової інформації про допускаються ланцюжках необхідно додати атрибути до допускає станів. Це зроблено в шаблонному класі AttributeFsm. Клас бере в якості параметра шаблону реалізацію функції переходів і тип атрибута для допускає стану. Слід зазначити, що атрибути можна приєднувати не тільки до допускає станів (хоча незрозуміло, чи має це сенс), а також, що до стану можна приєднати більше одного атрибута, всі вони зберігаються у векторі.

Зберігання словника в кінцевому автоматі задає для функції переходів кінцевого автомата цього словника деревоподібну структуру. Для такої структури використовується також термін trie, введений Д. Батогом. В бібліотеці Strutext є реалізація такого кінцевого автомата у файлі автоматів/trie.h:
template < class Trans, typename Attribute>
struct Trie : public AttributeFsm<Trans, Attribute> {
/// Chain identifier type.
typedef Attribute ChainId;

/// Attribute FSM type.
typedef AttributeFsm<Trans, Attribute> AttributeFsmImpl;

/// Default initialization.
explicit Trie(size_t rsize = AttributeFsmImpl::kReservedStateTableSize) : AttributeFsmImpl(rsize) {}

/// It may be base class.
virtual ~Trie() {}

/**
* \brief Adding chain of symbols.
*
* \param begin Iterator of the chain's begin.
* \param end Iterator of the chain's end.
* \param id Chain identifier.
*
* \return The number of last state of the chain.
*/
template < typename SymbolIterator>
StateId AddChain(SymbolIterator begin, SymbolIterator end, const ChainId& id);

/**
* \brief Adding chain of symbols.
*
* \param begin Iterator of the chain's begin.
* \param end Iterator of the chain's end.
*
* \return The number of last state of the chain.
*/
template < typename SymbolIterator>
StateId AddChain(SymbolIterator begin, SymbolIterator end);

/**
* \brief Search of the passed chain in the trie
*
* \param begin Iterator of the chain's begin.
* \param end Iterator of the chain's end.
* \result The reference to the list of attributes of the chain if any.
*/
template < typename SymbolIterator>
const typename AttributeFsmImpl::AttributeList& Search(SymbolIterator begin, SymbolIterator end) const;
};

З коду видно, що є два основні методи: AddChain і Search. Останній метод примітний тим, що повертає посилання на вектор атрибутів, тобто при пошуку атрибути стану не копіюються. Якщо вхідна ланцюжок символів не знайдена, то вектор атрибутів буде порожній.

В бібліотеці Strutext також реалізований автомат Агв-Корасик для ефективного пошуку елементів словника в тексті. Реалізація представлена в автоматів/aho_corasick.h. Виклад принципів і методів його реалізації виходить за рамки цього тексту, відзначимо тільки, що інтерфейс досить простий у використанні, а також є ітератор по ланцюжкам, знайденим в тексті.

Слід також зазначити, що всі автомати можуть бути серіалізовать/десериализованы у std::stream. Це дозволяє зберігати автомати у файлах на диску, тобто використовувати як сховища словників в бінарному форматі.

Морфологічний аналізатор
Морфологічний аналізатор являє собою бібліотеку у директорії morpho/morpholib. Основний інтерфейсний клас, Morphologist, розташовується у файлі morpho/morpholib/morpho.h.

Перш ніж описувати інтерфейс і реалізацію класу, спочатку опишемо основні принципи, на яких ґрунтується ця реалізація.

По-перше, є словник основ, які реалізовані в об'єкті класу Trie.
По-друге, кожній основі допускає стані дані у відповідність парадигма відмінювання (раніше, це вектор пар (суфікс, набір лексичних атрибутів). Набір атрибутів представлений екземпляром класу, успадкованого від PartOfSpeech).
По-третє, кожному лексичного типу дано у відповідність унікальний числовий ідентифікатор, номер основи в словнику.

Таким чином, для розпізнавання переданої словоформи як слова необхідно пошукати по автомату основу (буде знайдений ідентифікатор лексичного типу, відповідного цій основі), потім по закінченню знайти відповідні атрибути. Все це треба робити з урахуванням неоднозначностей, як при пошуку основ, так і при визначенні закінчень. Код для пошуку наступного:
/**
* \brief Implementation of morphological analysis of passed form.
*
* \param text Input text in UTF-8 encoding.
* \param[out] lem_list List of лем within morphological attributes.
*/
void Analize(const std::string& text, LemList& lem_list) const {
// The first phase. Go throw the passed word text, encode symbol
// and remember symbol codes in the string. If found word on base
// some position, remember attribute and position for an each
// attribute.

// Try starts with empty bases
typedef std::list<std::pair<Attribute, size_t> > BaseList;
BaseList base_list;
strutext::автоматів::StateId state = strutext::автоматів::kStartState;
if (bases_trie_.IsAcceptable(state)) {
const typename Trie::AttributeList& attrs = bases_trie_.GetStateAttributes(state);
for (size_t i = 0; i < attrs.size(); ++i) {
base_list.push_back(std::make_pair(attrs[i], 0));
}
}

// Permorm the first phase.
std::string code_str;
typedef strutext::encode::Utf8Iterator<std::string::const_iterator> Utf8Iterator;
for (Utf8Iterator sym_it(text.begin(), text.end()); sym_it != Utf8Iterator(); ++sym_it) {
Code c = alphabet_.Encode(*sym_it);
code_str += c;
if (state != strutext::автоматів::kInvalidState) {
state = bases_trie_.Go(state, c);
if (bases_trie_.IsAcceptable(state)) {
const typename Trie::AttributeList& attrs = bases_trie_.GetStateAttributes(state);
for (size_t i = 0; i < attrs.size(); ++i) {
base_list.push_back(std::make_pair(attrs[i], code_str.size()));
}
}
}
}

// The second phase. Go throuth found the base list and find suffixes for them.
// If suffixes have been found then add them to the lemma list.
lem_list.clear();
for (BaseList::iterator base_it = base_list.begin(); base_it != base_list.end(); ++base_it) {
AttrMap attr;
attr.auto_attr_ = base_it->first;
SuffixStorage::AttrList att_list;
std::string suffix = code_str.substr(base_it->second);
// If suffix is empty empty suffix passed), add zero symbol to it.
if (suffix.empty()) {
suffix.push_back('\0');
}
if (const SuffixStorage::AttrList* att_list = suff_store_.SearchAttrs(attr.line_id_, suffix)) {
for (size_t i = 0; i < att_list->size(); ++i) {
lem_list.push_back(Lemma(attr.lem_id_, (*att_list)[i]));
}
}
}
}

Як видно, алгоритм визначення розділяється на два етапи. Спочатку виділяються основи (тут ще треба враховувати існування порожніх основ). Для кожної основи запам'ятовується її позиція у вхідному ланцюзі, щоб потім можна було виділити закінчення. На другому етапі проводиться пошук закінчень, відповідних виділеним основ. Якщо закінчення знайдено у відповідній даній основі парадигми відмінювання, то лексичні атрибути цього закінчення повертаються разом з ідентифікатором слова.

Клас Morphologist надає сервіс по генерації словоформ за номером основи і переданим лексичним атрибутів. Цим займається метод Generate:
/**
* \brief Generate form.
*
* \param lem_id The lemma identifier.
* \param attrs The attributes of the form.
* \return Generated text in UTF-8 encoding.
*/
std::string Generate(uint32_t lem_id, uint32_t attrs) const;

Також є метод GenAllForms породження всіх форм даного слова і метод GenMainForm, повертає головну форму слова. Для іменника це, очевидно, форма однини називного відмінка.

В директорії morpho/aot у файлі main.cpp реалізований парсер подання словника АОТ в оригінальному форматі, який як результат повертає подання в бінарному форматі, сумісному з бібліотекою морфології. Результуючий бінарний словник можна використовувати в класі Morphologist. Самі бінарні словники не зберігаються в репозиторії, але можуть бути згенеровані користувачем при необхідності. Для реалізації російського словника можна використовувати наступну команду:
./Release/bin/aot-parser-t ../morpho/aot/rus_tabs.txt -d ../morpho/aot/rus_morphs.txt -m ukr-b aot-rus.bin

В бінарному вигляді словник розмір словника трохи менше 20 Мб.

Для виділення словорформ з вихідного тексту можна використовувати визначений у utility/word_iterator.h клас WordIterator. Цей клас вважає словами послідовності символів (symbols::IsLetter). Повертає ітератор слово як юнікодні рядок. Перекодувати цей рядок в UTF-8 можна за допомогою функції GetUtf8Sequence, визначеної в encode::utf8_generator.h.

Післямова
Текст вийшов досить об'ємним і, ймовірно, складним для читання. Автор намагався спростити виклад, де це тільки було можливо, але враховуючи складність матеріалу, що таких місць у тексті мабуть було не багато.

Все ж автор має надію, що описувана в тексті бібліотека Strutext буде корисна і праця по її реалізації не буде марним.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.