Thursday, January 21, 2016

Алгоритм Стеммер Портера для русского языка на C++ (CPP)

Для реализации одной из задач по обработке текста мне было необходимо подсчитать количество различных слов в тексте.. но не просто различных, а целиком и полностью! :) Например, слова "себя" и "себе" - должны считаться одинаковыми. Поискав в сети немного информации, наткнулся на алгоритм Стеммер Портера, который способен выделить в слове его основу, отбросив всё ненужное. Нашёл несколько его реализаций, в том числе на PHP и JAVA. Но нужной мне реализации на языке С++ я найти так и не смог. Пришлось переводить код с Java  на С++. В C++ довольно скудная поддержка регулярных выражений, с несколькими ограничениями, поэтому пришлось упрощать некоторые выражения.
Кстати, во время разработки нашёл баг в классе std::regex. Так вот, если в регулярном выражении есть маленькая буква "я", то всё крашится и падает :-/ Все остальные русские символы, в том числе и большая буква "Я" не вызывают подобного эффекта. Именно поэтому пришлось все регулярные выражения записать в КАПСЕ.

Ниже приведу листинг класса на C++:
class StemmerPorter{
private:
static const string EMPTY;
static const string S1;
static const string S13;
static const string SN;
static const string const1;
static const regex PERFECTIVEGROUND;
static const regex REFLEXIVE;
static const regex ADJECTIVE;
static const regex PARTICIPLE;
static const regex VERB;
static const regex NOUN;
static const regex I;
static const regex P;
static const regex NN;
static const regex DERIVATIONAL;
static const regex DER;
static const regex SUPERLATIVE;

public:
StemmerPorter();
string get(string s);
};

const string StemmerPorter::const1 = "АЕИОУЫЭИЮЯ";
const string StemmerPorter::EMPTY = "";
const string StemmerPorter::S1 = "$1";
const string StemmerPorter::S13 = "$1$3";
const string StemmerPorter::SN = "Н";
const regex StemmerPorter::PERFECTIVEGROUND = regex("(ИВ|ИВШИ|ИВШИСЬ|ЫВ|ЫВШИ|ЫВШИСЬ|ВШИ|ВШИСЬ)$");
const regex StemmerPorter::REFLEXIVE = regex("(СЯ|СЬ)$");
const regex StemmerPorter::ADJECTIVE = regex("(ЕЕ|ИЕ|ЫЕ|ОЕ|ИМИ|ЫМИ|ЕЙ|ИЙ|ЫЙ|ОЙ|ЕМ|ИМ|ЫМ|ОМ|ЕГО|ОГО|ЕМУ|ОМУ|ИХ|ЫХ|УЮ|ЮЮ|АЯ|ЯЯ|ОЮ|ЕЮ)$");
const regex StemmerPorter::PARTICIPLE = regex("(.*)(ИВШ|ЫВШ|УЮЩ)$|([АЯ])(ЕМ|НН|ВШ|ЮЩ|Щ)$");
const regex StemmerPorter::VERB = regex("(.*)(ИЛА|ЫЛА|ЕНА|ЕЙТЕ|УЙТЕ|ИТЕ|ИЛИ|ЫЛИ|ЕЙ|УЙ|ИЛ|ЫЛ|ИМ|ЫМ|ЕН|ИЛО|ЫЛО|ЕНО|ЯТ|УЕТ|УЮТ|ИТ|ЫТ|ЕНЫ|ИТЬ|ЫТЬ|ИШЬ|УЮ|Ю)$|([АЯ])(ЛА|НА|ЕТЕ|ЙТЕ|ЛИ|Й|Л|ЕМ|Н|ЛО|НО|ЕТ|ЮТ|НЫ|ТЬ|ЕШЬ|ННО)$");
const regex StemmerPorter::NOUN = regex("(А|ЕВ|ОВ|ИЕ|ЬЕ|Е|ИЯМИ|ЯМИ|АМИ|ЕИ|ИИ|И|ИЕЙ|ЕЙ|ОЙ|ИЙ|Й|ИЯМ|ЯМ|ИЕМ|ЕМ|АМ|ОМ|О|У|АХ|ИЯХ|ЯХ|Ы|Ь|ИЮ|ЬЮ|Ю|ИЯ|ЬЯ|Я)$");
const regex StemmerPorter::I = regex("И$");
const regex StemmerPorter::P = regex("Ь$");
const regex StemmerPorter::NN = regex("НН$");
const regex StemmerPorter::DERIVATIONAL = regex(".*[^АЕИОУЫЭЮЯ]+[АЕИОУЫЭЮЯ].*ОСТЬ?$");
const regex StemmerPorter::DER = regex("ОСТЬ?$");
const regex StemmerPorter::SUPERLATIVE = regex("(ЕЙШЕ|ЕЙШ)$");

StemmerPorter::StemmerPorter(){
setlocale(0,"");
}
string StemmerPorter::get(string s){
transform(s.begin(), s.end(), s.begin(), ::toupper);
replace( s.begin(), s.end(), 'Ё', 'Е');
int k=0;
size_t pos = s.find_first_of(const1, 0);
if(pos != string::npos){
string pre = s.substr(0,pos+1);
string rv = s.substr(pos+1);
string temp = regex_replace(rv, PERFECTIVEGROUND, EMPTY);
if(rv.size()!=temp.size()){
rv=temp;
}
else{
rv = regex_replace(rv, REFLEXIVE, EMPTY);
            temp = regex_replace(rv, ADJECTIVE, EMPTY);
if(rv.size()!=temp.size()){
rv = temp;
rv = regex_replace(rv, PARTICIPLE, S13);
}
else{
temp = regex_replace(rv, VERB, S13);
if(rv.size()!=temp.size()){
rv=temp;
}
else{
rv=regex_replace(temp, NOUN, EMPTY);
}
}
}
rv=regex_replace(rv, I, EMPTY);
if(regex_match(rv, DERIVATIONAL)){
rv = regex_replace(rv, DER, EMPTY);
}
temp = regex_replace(rv, P, EMPTY);
if(temp.length()!=rv.length()){
rv=temp;
}
else{
rv=regex_replace(rv, SUPERLATIVE, EMPTY);
rv=regex_replace(rv, NN, SN);
}
s=pre+rv;
}
transform(s.begin(), s.end(), s.begin(), tolower);
return s;
}

Использовать данный класс очень просто:
StemmerPorter sp;
std::cout<<sp.get("Алгоритмы")<<std::endl;