From ed297b6594a66d8af590e187680056a9b7ff6b49 Mon Sep 17 00:00:00 2001 From: NeonXP Date: Wed, 27 Nov 2013 04:06:49 +0400 Subject: [PATCH] first commit --- .gitignore | 2 + README.md | 36 +++++++++++ composer.json | 17 ++++++ phpunit.xml.dist | 20 ++++++ src/NXP/Stemmer.php | 138 ++++++++++++++++++++++++++++++++++++++++++ tests/StemmerTest.php | 85 ++++++++++++++++++++++++++ tests/bootstrap.php | 11 ++++ 7 files changed, 309 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml.dist create mode 100644 src/NXP/Stemmer.php create mode 100644 tests/StemmerTest.php create mode 100644 tests/bootstrap.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7f8ac3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.idea/ +/vendor/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..629dd98 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# Стеммер Портера для русского языка + +## Описание + +Данный стеммер является заменой расширению stem_russian_unicode. + +## Сравнение с расширением stem_russian_unicode + +*Плюсы:* + +1. не требовать внешних расширений для PHP. Стеммер написан целиком на PHP. +2. избавиться от проблем с юникодом. stem_russian_unicode зависит от SET_LOCALE и может при неверном значении портить строки с юникодом. +3. быть легко изменяемым под конкретные требования проекта. В случае расширения, при изменении логики работы его придётся пересобирать. + +*Минусы:* + +1. в силу того, что этот стеммер написан на PHP с использованием регулярных выражений, он должен проигрывать в скорости работы скомпилированному расширению, написанному на C. +2. Требует для своей работы PHP версии >=5.4 (возможно, добавлю поддержку версии 5.3) + +## Использование + + getWordBase($word); + } + $result = implode(' ', $stemmed); + +## Отличия от классического стеммера Портера + +Единственное отличие заключается в том, что в данной реализации буква «ё» является самостоятельной гласной, а не буквой «е» + +## Лицензия GPLv3 diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..13a12f0 --- /dev/null +++ b/composer.json @@ -0,0 +1,17 @@ +{ + "name": "nxp/russian-porter-stemmer", + "description": "Russian porter stemmer", + "minimum-stability": "stable", + "keywords": ["russian", "stemmer"], + "homepage": "http://github.com/NeonXP/RussianStemmer", + "license": "GPLv2", + "authors": [ + { + "name": "Alexander 'NeonXP' Kiryukhin", + "email": "frei@neonxp.info" + } + ], + "autoload": { + "psr-0": {"NXP": "src/"} + } +} \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..d373eb2 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,20 @@ + + + + + + + ./tests/ + + + \ No newline at end of file diff --git a/src/NXP/Stemmer.php b/src/NXP/Stemmer.php new file mode 100644 index 0000000..03b159b --- /dev/null +++ b/src/NXP/Stemmer.php @@ -0,0 +1,138 @@ +word = $word; + $this->findRegions(); + //Шаг 1 + //Найти окончание PERFECTIVE GERUND. Если оно существует – удалить его и завершить этот шаг. + if (!$this->removeEndings($this->regexPerfectiveGerunds, $this->RV)) { + //Иначе, удаляем окончание REFLEXIVE (если оно существует). + $this->removeEndings($this->regexReflexives, $this->RV); + //Затем в следующем порядке пробуем удалить окончания: ADJECTIVAL, VERB, NOUN. Как только одно из них найдено – шаг завершается. + if (!($this->removeEndings( + [ + $this->regexParticiple[0] . $this->regexAdjective, + $this->regexParticiple[1] . $this->regexAdjective + ], + $this->RV + ) || $this->removeEndings($this->regexAdjective, $this->RV)) + ) { + if (!$this->removeEndings($this->regexVerb, $this->RV)) { + $this->removeEndings($this->regexNoun, $this->RV); + } + } + } + //Шаг 2 + //Если слово оканчивается на и – удаляем и. + $this->removeEndings($this->regexI, $this->RV); + //Шаг 3 + //Если в R2 найдется окончание DERIVATIONAL – удаляем его. + $this->removeEndings($this->regexDerivational, $this->R2); + //Шаг 4 + //Возможен один из трех вариантов: + //Если слово оканчивается на нн – удаляем последнюю букву. + if ($this->removeEndings($this->regexNN, $this->RV)) { + $this->word .= 'н'; + } + //Если слово оканчивается на SUPERLATIVE – удаляем его и снова удаляем последнюю букву, если слово оканчивается на нн. + $this->removeEndings($this->regexSuperlative, $this->RV); + //Если слово оканчивается на ь – удаляем его. + $this->removeEndings($this->regexSoftSign, $this->RV); + + return $this->word; + } + + public function removeEndings($regex, $region) + { + $prefix = mb_substr($this->word, 0, $region, 'utf8'); + $word = mb_substr($this->word, $region, null, 'utf8'); + if (is_array($regex)) { + if (preg_match('/.+[а|я]' . $regex[0] . '/u', $word)) { + $this->word = $prefix . preg_replace('/' . $regex[0] . '/u', '', $word); + return true; + } + $regex = $regex[1]; + } + if (preg_match('/.+' . $regex . '/u', $word)) { + $this->word = $prefix . preg_replace('/' . $regex . '/u', '', $word); + return true; + } + + return false; + } + + private function findRegions() + { + $state = 0; + for ($i = 1; $i < mb_strlen($this->word, 'utf8'); $i++) { + $prevChar = mb_substr($this->word, $i - 1, 1, 'utf8'); + $char = mb_substr($this->word, $i, 1, 'utf8'); + switch ($state) { + case 0: + if ($this->isVowel($char)) { + $this->RV = $i + 1; + $state = 1; + } + break; + case 1: + if ($this->isVowel($prevChar) && !$this->isVowel($char)) { + $this->R1 = $i + 1; + $state = 2; + } + break; + case 2: + if ($this->isVowel($prevChar) && !$this->isVowel($char)) { + $this->R2 = $i + 1; + return; + } + break; + } + } + } + + private function isVowel($char) + { + return (strpos($this->vowel, $char) !== false); + } +} \ No newline at end of file diff --git a/tests/StemmerTest.php b/tests/StemmerTest.php new file mode 100644 index 0000000..2d2db93 --- /dev/null +++ b/tests/StemmerTest.php @@ -0,0 +1,85 @@ + 'результат', + 'в' => 'в', + 'вавиловка' => 'вавиловк', + 'вагнера' => 'вагнер', + 'вагон' => 'вагон', + 'вагона' => 'вагон', + 'вагоне' => 'вагон', + 'вагонов' => 'вагон', + 'вагоном' => 'вагон', + 'вагоны' => 'вагон', + 'важная' => 'важн', + 'важнее' => 'важн', + 'важнейшие' => 'важн', + 'важнейшими' => 'важн', + 'важничал' => 'важнича', + 'важно' => 'важн', + 'важного' => 'важн', + 'важное' => 'важн', + 'важной' => 'важн', + 'важном' => 'важн', + 'важному' => 'важн', + 'важности' => 'важност', + 'важностию' => 'важност', + 'важность' => 'важност', + 'важностью' => 'важност', + 'важную' => 'важн', + 'важны' => 'важн', + 'важные' => 'важн', + 'важный' => 'важн', + 'важным' => 'важн', + 'важных' => 'важн', + 'вазах' => 'ваз', + 'вазы' => 'ваз', + 'вакса' => 'вакс', + 'вакханка' => 'вакханк', + 'вал' => 'вал', + 'валандался' => 'валанда', + 'валентина' => 'валентин', + 'валериановых' => 'валерианов', + 'валерию' => 'валер', + 'валетами' => 'валет', + 'вали' => 'вал', + 'валил' => 'вал', + 'валился' => 'вал', + 'валится' => 'вал', + 'валов' => 'вал', + 'вальдшнепа' => 'вальдшнеп', + 'вальс' => 'вальс', + 'вальса' => 'вальс', + 'вальсе' => 'вальс', + 'вальсишку' => 'вальсишк', + 'вальтера' => 'вальтер', + 'валяется' => 'валя', + 'валялась' => 'валя', + 'валялись' => 'валя', + 'валялось' => 'валя', + 'валялся' => 'валя', + 'валять' => 'валя', + 'валяются' => 'валя', + 'вам' => 'вам', + 'вами' => 'вам', + ]; + foreach ($testWords as $word => $base) { + $this->assertEquals($base, $stemmer->getWordBase($word)); + } + } +} \ No newline at end of file diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..8eb3e7a --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,11 @@ +