Вот я снова решил выдавать из себя что нибудь…
Всё началось с того, что пока я лежал в военном госпитале, меня посетила идея сделать свой 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)