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;

2 comments:

  1. Спасибо большое за алгоритм!
    Сам работаю на Qt, чуть поправил его под свои нужды. За оптимальность не ручаюсь, но вроде все работает!


    class StemmerPorter{
    private:
    static const QString EMPTY;
    static const QString S1;
    static const QString S13;
    static const QString SN;
    static const QString const1;
    static const QRegExp PERFECTIVEGROUND;
    static const QRegExp REFLEXIVE;
    static const QRegExp ADJECTIVE;
    static const QRegExp PARTICIPLE;
    static const QRegExp VERB;
    static const QRegExp NOUN;
    static const QRegExp I;
    static const QRegExp P;
    static const QRegExp NN;
    static const QRegExp DERIVATIONAL;
    static const QRegExp DER;
    static const QRegExp SUPERLATIVE;

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

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

    StemmerPorter::StemmerPorter(){
    setlocale(0,"");
    }
    QString StemmerPorter::get(QString s){

    s = s.toUpper(); // В оригинальном с++, по заявлению автора, а regex есть баг при использовании символа 'я'
    s.replace( "Ё", "Е");
    int k=0;
    size_t pos = s.indexOf(const1, 0);
    if(pos != size_t(-1)){ // проверка на npos. Не самая удачная, но рабочая замена
    QString pre = s.left(pos+1);
    QString rv = s.mid(pos+1);
    QString temp = rv.replace(PERFECTIVEGROUND, EMPTY);
    if(rv.size()!=temp.size()){
    rv=temp;
    }
    else{
    rv = rv.replace(REFLEXIVE, EMPTY);
    temp = rv.replace(ADJECTIVE, EMPTY);
    if(rv.size()!=temp.size()){
    rv = temp;
    rv = rv.replace(PARTICIPLE, S13);
    }
    else{
    temp = rv.replace(VERB, S13);
    if(rv.size()!=temp.size()){
    rv=temp;
    }
    else{
    rv= temp.replace(NOUN, EMPTY);
    }
    }
    }
    rv=rv.replace(I, EMPTY);
    if( DERIVATIONAL.exactMatch(rv)){
    rv = rv.replace(DER, EMPTY);
    }
    temp = rv.replace(P, EMPTY);
    if(temp.length()!=rv.length()){
    rv=temp;
    }
    else{
    rv=rv.replace(SUPERLATIVE, EMPTY);
    rv=rv.replace(NN, SN);
    }
    s=pre+rv;
    }
    return s.toLower();
    }

    ReplyDelete
  2. Виноват, не учел пару особенностей. Переписал алгоритм, чуть усложнил, теперь работает хорошо и без зависимости от регистра

    const QRegExp StemmerPorter::const1 = QRegExp("\\b[Б-ДЖЗЙ-НП-ТФ-Щ]+[АЕИОУЫЭЮЯ][А-Я]+\\b", Qt::CaseInsensitive);
    const QRegExp StemmerPorter::const2 = QRegExp("\\b[А-Я]+[АЕИОУЫЭЮЯ][А-Я]+\\b", Qt::CaseInsensitive);
    const QRegExp StemmerPorter::vowCheck = QRegExp("[АЕИОУЫЭЮЯ]", Qt::CaseInsensitive);
    const QString StemmerPorter::EMPTY = "";
    const QString StemmerPorter::SN = "Н";
    const QRegExp StemmerPorter::PERFECTIVEGROUND = QRegExp("(ИВ|ИВШИ|ИВШИСЬ|ЫВ|ЫВШИ|ЫВШИСЬ|ВШИ|ВШИСЬ)\\b", Qt::CaseInsensitive);
    const QRegExp StemmerPorter::REFLEXIVE = QRegExp("(СЯ|СЬ)\\b", Qt::CaseInsensitive);
    const QRegExp StemmerPorter::ADJECTIVE = QRegExp("(ЕЕ|ИЕ|ЫЕ|ОЕ|ИМИ|ЫМИ|ЕЙ|ИЙ|ЫЙ|ОЙ|ЕМ|ИМ|ЫМ|ОМ|ЕГО|ОГО|ЕМУ|ОМУ|ИХ|ЫХ|УЮ|ЮЮ|АЯ|ЯЯ|ОЮ|ЕЮ)\\b", Qt::CaseInsensitive);
    const QRegExp StemmerPorter::PARTICIPLE = QRegExp("(.*)(ИВШ|ЫВШ|УЮЩ)\\b|([АЯ])(ЕМ|НН|ВШ|ЮЩ|Щ)\\b", Qt::CaseInsensitive);
    const QRegExp StemmerPorter::VERB = QRegExp("(ИЛА|ЫЛА|ЕНА|ЕЙТЕ|УЙТЕ|ИТЕ|ИЛИ|ЫЛИ|ЕЙ|УЙ|ИЛ|ЫЛ|ИМ|ЫМ|ЕН|ИЛО|ЫЛО|ЕНО|ЯТ|УЕТ|УЮТ|ИТ|ЫТ|ЕНЫ|ИТЬ|ЫТЬ|ИШЬ|УЮ|Ю)\\b|([АЯ])(ЛА|НА|ЕТЕ|ЙТЕ|ЛИ|Й|Л|ЕМ|Н|ЛО|НО|ЕТ|ЮТ|НЫ|ТЬ|ЕШЬ|ННО)\\b", Qt::CaseInsensitive);
    const QRegExp StemmerPorter::NOUN = QRegExp("(А|ЕВ|ОВ|ИЕ|ЬЕ|Е|ИЯМИ|ЯМИ|АМИ|ЕИ|ИИ|И|ИЕЙ|ЕЙ|ОЙ|ИЙ|Й|ИЯМ|ЯМ|ИЕМ|ЕМ|АМ|ОМ|О|У|АХ|ИЯХ|ЯХ|Ы|Ь|ИЮ|ЬЮ|Ю|ИЯ|ЬЯ|Я)\\b", Qt::CaseInsensitive);
    const QRegExp StemmerPorter::I = QRegExp("И\\b", Qt::CaseInsensitive);
    const QRegExp StemmerPorter::P = QRegExp("Ь\\b", Qt::CaseInsensitive);
    const QRegExp StemmerPorter::NN = QRegExp("НН\\b", Qt::CaseInsensitive);
    const QRegExp StemmerPorter::DERIVATIONAL = QRegExp(".*[^АЕИОУЫЭЮЯ]+[АЕИОУЫЭЮЯ].*ОСТЬ?\\b", Qt::CaseInsensitive);
    const QRegExp StemmerPorter::DER = QRegExp("ОСТЬ?\\b", Qt::CaseInsensitive);
    const QRegExp StemmerPorter::SUPERLATIVE = QRegExp("(ЕЙШЕ|ЕЙШ)\\b", Qt::CaseInsensitive);

    StemmerPorter::StemmerPorter(){
    setlocale(0,"");
    }
    QString StemmerPorter::get(QString s){

    s.replace( "Ё", "Е");
    int pos = -2;
    if(const1.exactMatch(s)){
    pos = s.indexOf(vowCheck);
    } else if(const2.exactMatch(s)){
    pos = s.indexOf(vowCheck,1);
    }

    if(pos > 0){
    QString pre = s.left(pos+1);
    QString rv = s.mid(pos+1);
    QString temp = rv.replace(PERFECTIVEGROUND, EMPTY);
    if(rv.size()!=temp.size()){
    rv=temp;
    }
    else{
    rv = rv.replace(REFLEXIVE, EMPTY);
    temp = rv.replace(ADJECTIVE, EMPTY);
    if(rv.size()!=temp.size()){
    rv = temp;
    rv = rv.replace(PARTICIPLE, EMPTY);
    }
    else{
    temp = rv.replace(VERB, EMPTY);
    if(rv.size()!=temp.size()){
    rv=temp;
    }
    else{
    rv= temp.replace(NOUN, EMPTY);
    }
    }
    }
    rv=rv.replace(I, EMPTY);
    if( DERIVATIONAL.exactMatch(rv)){
    rv = rv.replace(DER, EMPTY);
    }
    temp = rv.replace(P, EMPTY);
    if(temp.length()!=rv.length()){
    rv=temp;
    }
    else{
    rv=rv.replace(SUPERLATIVE, EMPTY);
    rv=rv.replace(NN, SN);
    }
    s=pre+rv;
    }
    return s;
    }

    ReplyDelete