Вот я снова решил выдавать из себя что нибудь…
Всё началось с того, что пока я лежал в военном госпитале, меня посетила идея сделать свой Word на javascript, главная фишка которого заключалась бы в проверке орфографии и пунктуации. Пол месяца назад я был уволен в запас, и оказался дома, где меня целых пол года ждал ноутбук с Ubuntu и установленным Eclipse. Начался поиск информации о том, как можно разобрать слово на составляющие, и наткнулся я на стеммер Портера, алгоритм, позволяющий выделить неизменяемую основу слова. Для чего он мне нужен я так до сих пор и не понял, но уверен что в будущем он мне пригодится.
Алгоритм
Официальное описание алгоритма:
Step 1: Search for a PERFECTIVE GERUND ending. If one is found remove it, and that is then the end of step 1. Otherwise try and remove a REFLEXIVE ending, and then search in turn for (1) an ADJECTIVAL, (2) a VERB or (3) a NOUN ending. As soon as one of the endings (1) to (3) is found remove it, and terminate step 1.
Step 2: If the word ends with и (i), remove it.
Step 3: Search for a DERIVATIONAL ending in R2 (i.e. the entire ending must lie in R2), and if one is found, remove it.
Step 4: (1) Undouble н (n), or, (2) if the word ends with a SUPERLATIVE ending, remove it and undouble н (n), or (3) if the word ends ь (') (soft sign) remove it.
Переводить на русский язык смысла не вижу, есть Google Translate для тех кто не знает английский.
Реализация
Естественно, я как уважающий своё время человек, попытался найти реализацию этого алгоритма, но попытки оказались тщетны. Была найдена масса реализаций на PHP, Java, C, но для JavaScript — не было. Пришлось взять за основу одну из найденных реализаций на Java и переписать на JavaScript. Получилось следующее:
var stemmer = (function() {
var DICT = {
RVRE: /^(.*?[аеиоуыэюя])(.*)$/i,
PERFECTIVEGROUND_1: /([ая])(в|вши|вшись)$/gi,
PERFECTIVEGROUND_2: /(ив|ивши|ившись|ыв|ывши|ывшись)$/i,
REFLEXIVE: /(с[яь])$/i,
ADJECTIVE: /(ее|ие|ые|ое|ими|ыми|ей|ий|ый|ой|ем|им|ым|ом|его|ого|ему|ому|их|ых|ую|юю|ая|яя|ою|ею)$/i,
PARTICIPLE_1: /([ая])(ем|нн|вш|ющ|щ)$/gi,
PARTICIPLE_2: /(ивш|ывш|ующ)$/i,
VERB_1: /([ая])(ла|на|ете|йте|ли|й|л|ем|н|ло|но|ет|ют|ны|ть|ешь|нно)$/gi,
VERB_2: /(ила|ыла|ена|ейте|уйте|ите|или|ыли|ей|уй|ил|ыл|им|ым|ен|ило|ыло|ено|ят|ует|уют|ит|ыт|ены|ить|ыть|ишь|ую|ю)$/i,
NOUN: /(а|ев|ов|ие|ье|е|иями|ями|ами|еи|ии|и|ией|ей|ой|ий|й|иям|ям|ием|ем|ам|ом|о|у|ах|иях|ях|ы|ь|ию|ью|ю|ия|ья|я)$/i,
DERIVATIONAL: /.*[^аеиоуыэюя]+[аеиоуыэюя].*ость?$/i,
DER: /ость?$/i,
SUPERLATIVE: /(ейше|ейш)$/i,
I: /и$/i,
P: /ь$/i,
NN: /нн$/i
};
return function stemmer(word) {
word = word.replace(/ё/gi, 'e');
var wParts = word.match(DICT.RVRE);
if (!wParts) {
return word;
}
var start = wParts[1];
var rv = wParts[2];
var temp = rv.replace(DICT.PERFECTIVEGROUND_2, '');
if (temp == rv) {
temp = rv.replace(DICT.PERFECTIVEGROUND_1, '$1');
}
if (temp == rv) {
rv = rv.replace(DICT.REFLEXIVE, '');
temp = rv.replace(DICT.ADJECTIVE, '');
if (temp != rv) {
rv = temp;
temp = rv.replace(DICT.PARTICIPLE_2, '');
if (temp == rv) {
rv = rv.replace(DICT.PARTICIPLE_1, '$1');
}
} else {
temp = rv.replace(DICT.VERB_2, '');
if (temp == rv) {
temp = rv.replace(DICT.VERB_1, '$1');
}
if (temp == rv) {
rv = rv.replace(DICT.NOUN, '');
} else {
rv = temp;
}
}
} else {
rv = temp;
}
rv = rv.replace(DICT.I, '');
if (rv.match(DICT.DERIVATIONAL)) {
rv = rv.replace(DICT.DER, '');
}
temp = rv.replace(DICT.P, '');
if (temp == rv) {
rv = rv.replace(DICT.SUPERLATIVE, '');
rv = rv.replace(DICT.NN, 'н');
} else {
rv = temp;
}
return start + rv;
};
})();
Код получился простой и понятный, правда без комментариев…
Тест Портера проходит на 100%. Так же код работает довольно быстро, на моём Intel i3 Google Chrome 21 (dev‐билд) в секунду выделяет основу из порядка 300 тысяч слов, правда в Opera 12 это число равно примерно 70 тысячам, и не зря я не люблю оперу.
Пример работы
Ссылки
Моя реализация на JavaScript (jsfiddle)