mirror of https://github.com/explosion/spaCy.git
* Classes for Ukrainian; small fix in Russian. * Contributor agreement * pymorphy2 initialization split for ru and uk (#3327) * stop-words fixed * Unit-tests updated
This commit is contained in:
parent
386cec1979
commit
f1c3108d52
|
@ -8,17 +8,18 @@ from ...lemmatizer import Lemmatizer
|
|||
class RussianLemmatizer(Lemmatizer):
|
||||
_morph = None
|
||||
|
||||
def __init__(self, pymorphy2_lang='ru'):
|
||||
def __init__(self):
|
||||
super(RussianLemmatizer, self).__init__()
|
||||
try:
|
||||
from pymorphy2 import MorphAnalyzer
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
'The Russian lemmatizer requires the pymorphy2 library: '
|
||||
'try to fix it with "pip install pymorphy2==0.8"')
|
||||
'try to fix it with "pip install pymorphy2==0.8" '
|
||||
'or "pip install git+https://github.com/kmike/pymorphy2.git pymorphy2-dicts-uk" if you need Ukrainian too')
|
||||
|
||||
if RussianLemmatizer._morph is None:
|
||||
RussianLemmatizer._morph = MorphAnalyzer(lang=pymorphy2_lang)
|
||||
RussianLemmatizer._morph = MorphAnalyzer()
|
||||
|
||||
def __call__(self, string, univ_pos, morphology=None):
|
||||
univ_pos = self.normalize_univ_pos(univ_pos)
|
||||
|
|
|
@ -1,12 +1,239 @@
|
|||
from ..ru.lemmatizer import RussianLemmatizer
|
||||
# coding: utf8
|
||||
from ...symbols import (
|
||||
ADJ, DET, NOUN, NUM, PRON, PROPN, PUNCT, VERB, POS
|
||||
)
|
||||
from ...lemmatizer import Lemmatizer
|
||||
|
||||
|
||||
class UkrainianLemmatizer(RussianLemmatizer):
|
||||
class UkrainianLemmatizer(Lemmatizer):
|
||||
_morph = None
|
||||
|
||||
def __init__(self, pymorphy2_lang='ru'):
|
||||
def __init__(self):
|
||||
super(UkrainianLemmatizer, self).__init__()
|
||||
try:
|
||||
super(UkrainianLemmatizer, self).__init__(pymorphy2_lang='uk')
|
||||
except ImportError:
|
||||
from pymorphy2 import MorphAnalyzer
|
||||
if UkrainianLemmatizer._morph is None:
|
||||
UkrainianLemmatizer._morph = MorphAnalyzer(lang='uk')
|
||||
except (ImportError, TypeError):
|
||||
raise ImportError(
|
||||
'The Ukrainian lemmatizer requires the pymorphy2 library and dictionaries: '
|
||||
'try to fix it with "pip install git+https://github.com/kmike/pymorphy2.git pymorphy2-dicts-uk"')
|
||||
'try to fix it with'
|
||||
'"pip uninstall pymorphy2"'
|
||||
'"pip install git+https://github.com/kmike/pymorphy2.git pymorphy2-dicts-uk"')
|
||||
|
||||
|
||||
def __call__(self, string, univ_pos, morphology=None):
|
||||
univ_pos = self.normalize_univ_pos(univ_pos)
|
||||
if univ_pos == 'PUNCT':
|
||||
return [PUNCT_RULES.get(string, string)]
|
||||
|
||||
if univ_pos not in ('ADJ', 'DET', 'NOUN', 'NUM', 'PRON', 'PROPN', 'VERB'):
|
||||
# Skip unchangeable pos
|
||||
return [string.lower()]
|
||||
|
||||
analyses = self._morph.parse(string)
|
||||
filtered_analyses = []
|
||||
for analysis in analyses:
|
||||
if not analysis.is_known:
|
||||
# Skip suggested parse variant for unknown word for pymorphy
|
||||
continue
|
||||
analysis_pos, _ = oc2ud(str(analysis.tag))
|
||||
if analysis_pos == univ_pos \
|
||||
or (analysis_pos in ('NOUN', 'PROPN') and univ_pos in ('NOUN', 'PROPN')):
|
||||
filtered_analyses.append(analysis)
|
||||
|
||||
if not len(filtered_analyses):
|
||||
return [string.lower()]
|
||||
if morphology is None or (len(morphology) == 1 and POS in morphology):
|
||||
return list(set([analysis.normal_form for analysis in filtered_analyses]))
|
||||
|
||||
if univ_pos in ('ADJ', 'DET', 'NOUN', 'PROPN'):
|
||||
features_to_compare = ['Case', 'Number', 'Gender']
|
||||
elif univ_pos == 'NUM':
|
||||
features_to_compare = ['Case', 'Gender']
|
||||
elif univ_pos == 'PRON':
|
||||
features_to_compare = ['Case', 'Number', 'Gender', 'Person']
|
||||
else: # VERB
|
||||
features_to_compare = ['Aspect', 'Gender', 'Mood', 'Number', 'Tense', 'VerbForm', 'Voice']
|
||||
|
||||
analyses, filtered_analyses = filtered_analyses, []
|
||||
for analysis in analyses:
|
||||
_, analysis_morph = oc2ud(str(analysis.tag))
|
||||
for feature in features_to_compare:
|
||||
if (feature in morphology and feature in analysis_morph
|
||||
and morphology[feature] != analysis_morph[feature]):
|
||||
break
|
||||
else:
|
||||
filtered_analyses.append(analysis)
|
||||
|
||||
if not len(filtered_analyses):
|
||||
return [string.lower()]
|
||||
return list(set([analysis.normal_form for analysis in filtered_analyses]))
|
||||
|
||||
@staticmethod
|
||||
def normalize_univ_pos(univ_pos):
|
||||
if isinstance(univ_pos, str):
|
||||
return univ_pos.upper()
|
||||
|
||||
symbols_to_str = {
|
||||
ADJ: 'ADJ',
|
||||
DET: 'DET',
|
||||
NOUN: 'NOUN',
|
||||
NUM: 'NUM',
|
||||
PRON: 'PRON',
|
||||
PROPN: 'PROPN',
|
||||
PUNCT: 'PUNCT',
|
||||
VERB: 'VERB'
|
||||
}
|
||||
if univ_pos in symbols_to_str:
|
||||
return symbols_to_str[univ_pos]
|
||||
return None
|
||||
|
||||
def is_base_form(self, univ_pos, morphology=None):
|
||||
# TODO
|
||||
raise NotImplementedError
|
||||
|
||||
def det(self, string, morphology=None):
|
||||
return self(string, 'det', morphology)
|
||||
|
||||
def num(self, string, morphology=None):
|
||||
return self(string, 'num', morphology)
|
||||
|
||||
def pron(self, string, morphology=None):
|
||||
return self(string, 'pron', morphology)
|
||||
|
||||
def lookup(self, string):
|
||||
analyses = self._morph.parse(string)
|
||||
if len(analyses) == 1:
|
||||
return analyses[0].normal_form
|
||||
return string
|
||||
|
||||
|
||||
def oc2ud(oc_tag):
|
||||
gram_map = {
|
||||
'_POS': {
|
||||
'ADJF': 'ADJ',
|
||||
'ADJS': 'ADJ',
|
||||
'ADVB': 'ADV',
|
||||
'Apro': 'DET',
|
||||
'COMP': 'ADJ', # Can also be an ADV - unchangeable
|
||||
'CONJ': 'CCONJ', # Can also be a SCONJ - both unchangeable ones
|
||||
'GRND': 'VERB',
|
||||
'INFN': 'VERB',
|
||||
'INTJ': 'INTJ',
|
||||
'NOUN': 'NOUN',
|
||||
'NPRO': 'PRON',
|
||||
'NUMR': 'NUM',
|
||||
'NUMB': 'NUM',
|
||||
'PNCT': 'PUNCT',
|
||||
'PRCL': 'PART',
|
||||
'PREP': 'ADP',
|
||||
'PRTF': 'VERB',
|
||||
'PRTS': 'VERB',
|
||||
'VERB': 'VERB',
|
||||
},
|
||||
'Animacy': {
|
||||
'anim': 'Anim',
|
||||
'inan': 'Inan',
|
||||
},
|
||||
'Aspect': {
|
||||
'impf': 'Imp',
|
||||
'perf': 'Perf',
|
||||
},
|
||||
'Case': {
|
||||
'ablt': 'Ins',
|
||||
'accs': 'Acc',
|
||||
'datv': 'Dat',
|
||||
'gen1': 'Gen',
|
||||
'gen2': 'Gen',
|
||||
'gent': 'Gen',
|
||||
'loc2': 'Loc',
|
||||
'loct': 'Loc',
|
||||
'nomn': 'Nom',
|
||||
'voct': 'Voc',
|
||||
},
|
||||
'Degree': {
|
||||
'COMP': 'Cmp',
|
||||
'Supr': 'Sup',
|
||||
},
|
||||
'Gender': {
|
||||
'femn': 'Fem',
|
||||
'masc': 'Masc',
|
||||
'neut': 'Neut',
|
||||
},
|
||||
'Mood': {
|
||||
'impr': 'Imp',
|
||||
'indc': 'Ind',
|
||||
},
|
||||
'Number': {
|
||||
'plur': 'Plur',
|
||||
'sing': 'Sing',
|
||||
},
|
||||
'NumForm': {
|
||||
'NUMB': 'Digit',
|
||||
},
|
||||
'Person': {
|
||||
'1per': '1',
|
||||
'2per': '2',
|
||||
'3per': '3',
|
||||
'excl': '2',
|
||||
'incl': '1',
|
||||
},
|
||||
'Tense': {
|
||||
'futr': 'Fut',
|
||||
'past': 'Past',
|
||||
'pres': 'Pres',
|
||||
},
|
||||
'Variant': {
|
||||
'ADJS': 'Brev',
|
||||
'PRTS': 'Brev',
|
||||
},
|
||||
'VerbForm': {
|
||||
'GRND': 'Conv',
|
||||
'INFN': 'Inf',
|
||||
'PRTF': 'Part',
|
||||
'PRTS': 'Part',
|
||||
'VERB': 'Fin',
|
||||
},
|
||||
'Voice': {
|
||||
'actv': 'Act',
|
||||
'pssv': 'Pass',
|
||||
},
|
||||
'Abbr': {
|
||||
'Abbr': 'Yes'
|
||||
}
|
||||
}
|
||||
|
||||
pos = 'X'
|
||||
morphology = dict()
|
||||
unmatched = set()
|
||||
|
||||
grams = oc_tag.replace(' ', ',').split(',')
|
||||
for gram in grams:
|
||||
match = False
|
||||
for categ, gmap in sorted(gram_map.items()):
|
||||
if gram in gmap:
|
||||
match = True
|
||||
if categ == '_POS':
|
||||
pos = gmap[gram]
|
||||
else:
|
||||
morphology[categ] = gmap[gram]
|
||||
if not match:
|
||||
unmatched.add(gram)
|
||||
|
||||
while len(unmatched) > 0:
|
||||
gram = unmatched.pop()
|
||||
if gram in ('Name', 'Patr', 'Surn', 'Geox', 'Orgn'):
|
||||
pos = 'PROPN'
|
||||
elif gram == 'Auxt':
|
||||
pos = 'AUX'
|
||||
elif gram == 'Pltm':
|
||||
morphology['Number'] = 'Ptan'
|
||||
|
||||
return pos, morphology
|
||||
|
||||
|
||||
PUNCT_RULES = {
|
||||
"«": "\"",
|
||||
"»": "\""
|
||||
}
|
||||
|
|
|
@ -13,8 +13,10 @@ from __future__ import unicode_literals
|
|||
STOP_WORDS = set("""а
|
||||
або
|
||||
адже
|
||||
аж
|
||||
але
|
||||
алло
|
||||
б
|
||||
багато
|
||||
без
|
||||
безперервно
|
||||
|
@ -23,6 +25,7 @@ STOP_WORDS = set("""а
|
|||
більше
|
||||
біля
|
||||
близько
|
||||
бо
|
||||
був
|
||||
буває
|
||||
буде
|
||||
|
@ -36,22 +39,27 @@ STOP_WORDS = set("""а
|
|||
були
|
||||
було
|
||||
бути
|
||||
бывь
|
||||
в
|
||||
важлива
|
||||
важливе
|
||||
важливий
|
||||
важливі
|
||||
вам
|
||||
вами
|
||||
вас
|
||||
ваш
|
||||
ваша
|
||||
ваше
|
||||
вашим
|
||||
вашими
|
||||
ваших
|
||||
ваші
|
||||
вашій
|
||||
вашого
|
||||
вашої
|
||||
вашому
|
||||
вашою
|
||||
вашу
|
||||
вгорі
|
||||
вгору
|
||||
вдалині
|
||||
весь
|
||||
вже
|
||||
ви
|
||||
від
|
||||
|
@ -66,7 +74,15 @@ STOP_WORDS = set("""а
|
|||
вони
|
||||
воно
|
||||
восьмий
|
||||
все
|
||||
всею
|
||||
всі
|
||||
всім
|
||||
всіх
|
||||
всього
|
||||
всьому
|
||||
всю
|
||||
вся
|
||||
втім
|
||||
г
|
||||
геть
|
||||
|
@ -102,16 +118,15 @@ STOP_WORDS = set("""а
|
|||
досить
|
||||
другий
|
||||
дуже
|
||||
дякую
|
||||
е
|
||||
є
|
||||
ж
|
||||
же
|
||||
життя
|
||||
з
|
||||
за
|
||||
завжди
|
||||
зазвичай
|
||||
зайнята
|
||||
зайнятий
|
||||
зайняті
|
||||
зайнято
|
||||
занадто
|
||||
зараз
|
||||
зате
|
||||
|
@ -119,22 +134,28 @@ STOP_WORDS = set("""а
|
|||
звідси
|
||||
звідусіль
|
||||
здається
|
||||
зі
|
||||
значить
|
||||
знову
|
||||
зовсім
|
||||
ім'я
|
||||
і
|
||||
із
|
||||
її
|
||||
їй
|
||||
їм
|
||||
іноді
|
||||
інша
|
||||
інше
|
||||
інший
|
||||
інших
|
||||
інші
|
||||
її
|
||||
їй
|
||||
їх
|
||||
й
|
||||
його
|
||||
йому
|
||||
каже
|
||||
ким
|
||||
кілька
|
||||
кого
|
||||
кожен
|
||||
кожна
|
||||
|
@ -143,13 +164,13 @@ STOP_WORDS = set("""а
|
|||
коли
|
||||
кому
|
||||
краще
|
||||
крейдуючи
|
||||
кругом
|
||||
крім
|
||||
куди
|
||||
ласка
|
||||
ледве
|
||||
лише
|
||||
люди
|
||||
людина
|
||||
м
|
||||
має
|
||||
майже
|
||||
мало
|
||||
мати
|
||||
|
@ -164,20 +185,27 @@ STOP_WORDS = set("""а
|
|||
мій
|
||||
мільйонів
|
||||
мною
|
||||
мого
|
||||
могти
|
||||
моє
|
||||
мож
|
||||
моєї
|
||||
моєму
|
||||
моєю
|
||||
може
|
||||
можна
|
||||
можно
|
||||
можуть
|
||||
можхо
|
||||
мої
|
||||
мор
|
||||
моїй
|
||||
моїм
|
||||
моїми
|
||||
моїх
|
||||
мою
|
||||
моя
|
||||
на
|
||||
навіть
|
||||
навіщо
|
||||
навколо
|
||||
навкруги
|
||||
нагорі
|
||||
над
|
||||
|
@ -190,10 +218,21 @@ STOP_WORDS = set("""а
|
|||
наш
|
||||
наша
|
||||
наше
|
||||
нашим
|
||||
нашими
|
||||
наших
|
||||
наші
|
||||
нашій
|
||||
нашого
|
||||
нашої
|
||||
нашому
|
||||
нашою
|
||||
нашу
|
||||
не
|
||||
небагато
|
||||
небудь
|
||||
недалеко
|
||||
неї
|
||||
немає
|
||||
нерідко
|
||||
нещодавно
|
||||
|
@ -206,17 +245,22 @@ STOP_WORDS = set("""а
|
|||
них
|
||||
ні
|
||||
ніби
|
||||
ніж
|
||||
ній
|
||||
ніколи
|
||||
нікуди
|
||||
нім
|
||||
нічого
|
||||
ну
|
||||
нх
|
||||
нього
|
||||
ньому
|
||||
о
|
||||
обидва
|
||||
обоє
|
||||
один
|
||||
одинадцятий
|
||||
одинадцять
|
||||
однак
|
||||
однієї
|
||||
одній
|
||||
одного
|
||||
|
@ -225,11 +269,16 @@ STOP_WORDS = set("""а
|
|||
он
|
||||
особливо
|
||||
ось
|
||||
п'ятий
|
||||
п'ятнадцятий
|
||||
п'ятнадцять
|
||||
п'ять
|
||||
перед
|
||||
перший
|
||||
під
|
||||
пізніше
|
||||
пір
|
||||
після
|
||||
по
|
||||
повинно
|
||||
подів
|
||||
|
@ -240,18 +289,14 @@ STOP_WORDS = set("""а
|
|||
потім
|
||||
потрібно
|
||||
почала
|
||||
прекрасне
|
||||
прекрасно
|
||||
початку
|
||||
при
|
||||
про
|
||||
просто
|
||||
проте
|
||||
проти
|
||||
п'ятий
|
||||
п'ятнадцятий
|
||||
п'ятнадцять
|
||||
п'ять
|
||||
раз
|
||||
разу
|
||||
раніше
|
||||
рано
|
||||
раптом
|
||||
|
@ -259,6 +304,7 @@ STOP_WORDS = set("""а
|
|||
роки
|
||||
років
|
||||
року
|
||||
році
|
||||
сам
|
||||
сама
|
||||
саме
|
||||
|
@ -271,15 +317,15 @@ STOP_WORDS = set("""а
|
|||
самого
|
||||
самому
|
||||
саму
|
||||
світу
|
||||
свого
|
||||
своє
|
||||
своєї
|
||||
свої
|
||||
своїй
|
||||
своїх
|
||||
свою
|
||||
сеаой
|
||||
себе
|
||||
сих
|
||||
сім
|
||||
сімнадцятий
|
||||
сімнадцять
|
||||
|
@ -307,7 +353,17 @@ STOP_WORDS = set("""а
|
|||
також
|
||||
там
|
||||
твій
|
||||
твого
|
||||
твоє
|
||||
твоєї
|
||||
твоєму
|
||||
твоєю
|
||||
твої
|
||||
твоїй
|
||||
твоїм
|
||||
твоїми
|
||||
твоїх
|
||||
твою
|
||||
твоя
|
||||
те
|
||||
тебе
|
||||
|
@ -319,15 +375,19 @@ STOP_WORDS = set("""а
|
|||
тисяч
|
||||
тих
|
||||
ті
|
||||
тієї
|
||||
тією
|
||||
тій
|
||||
тільки
|
||||
тім
|
||||
то
|
||||
тобі
|
||||
тобою
|
||||
того
|
||||
тоді
|
||||
той
|
||||
том
|
||||
тому
|
||||
тою
|
||||
треба
|
||||
третій
|
||||
три
|
||||
|
@ -345,7 +405,6 @@ STOP_WORDS = set("""а
|
|||
усім
|
||||
усіма
|
||||
усіх
|
||||
усію
|
||||
усього
|
||||
усьому
|
||||
усю
|
||||
|
@ -363,6 +422,7 @@ STOP_WORDS = set("""а
|
|||
цими
|
||||
цих
|
||||
ці
|
||||
цієї
|
||||
цій
|
||||
цього
|
||||
цьому
|
||||
|
@ -375,11 +435,24 @@ STOP_WORDS = set("""а
|
|||
через
|
||||
четвертий
|
||||
чи
|
||||
чиє
|
||||
чиєї
|
||||
чиєму
|
||||
чиї
|
||||
чиїй
|
||||
чиїм
|
||||
чиїми
|
||||
чиїх
|
||||
чий
|
||||
чийого
|
||||
чийому
|
||||
чим
|
||||
численна
|
||||
численне
|
||||
численний
|
||||
численні
|
||||
чию
|
||||
чия
|
||||
чого
|
||||
чому
|
||||
чотири
|
||||
|
@ -392,6 +465,8 @@ STOP_WORDS = set("""а
|
|||
ще
|
||||
що
|
||||
щоб
|
||||
щодо
|
||||
щось
|
||||
я
|
||||
як
|
||||
яка
|
||||
|
@ -400,5 +475,5 @@ STOP_WORDS = set("""а
|
|||
які
|
||||
якій
|
||||
якого
|
||||
якщо
|
||||
""".split())
|
||||
якої
|
||||
якщо""".split())
|
||||
|
|
|
@ -52,6 +52,7 @@ def RU(request):
|
|||
@pytest.fixture()
|
||||
def UK(request):
|
||||
pymorphy = pytest.importorskip('pymorphy2')
|
||||
pymorphy_lang = pytest.importorskip('pymorphy2.lang')
|
||||
return util.get_lang_class('uk')()
|
||||
|
||||
@pytest.fixture()
|
||||
|
@ -183,6 +184,7 @@ def ru_tokenizer():
|
|||
@pytest.fixture(scope='session')
|
||||
def uk_tokenizer():
|
||||
pymorphy = pytest.importorskip('pymorphy2')
|
||||
pymorphy_lang = pytest.importorskip('pymorphy2.lang')
|
||||
return util.get_lang_class('uk').Defaults.create_tokenizer()
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
|
|
|
@ -82,7 +82,7 @@ def test_uk_tokenizer_splits_open_appostrophe(uk_tokenizer, text):
|
|||
assert len(tokens) == 2
|
||||
assert tokens[0].text == "'"
|
||||
|
||||
|
||||
@pytest.mark.xfail # https://github.com/explosion/spaCy/issues/3327
|
||||
@pytest.mark.parametrize('text', ["Тест''"])
|
||||
def test_uk_tokenizer_splits_double_end_quote(uk_tokenizer, text):
|
||||
tokens = uk_tokenizer(text)
|
||||
|
|
Loading…
Reference in New Issue