Merge branch 'master' into develop

This commit is contained in:
Ines Montani 2019-02-25 15:54:55 +01:00
commit 76ce8b2662
5 changed files with 317 additions and 47 deletions

View File

@ -9,18 +9,19 @@ from ...compat import unicode_
class RussianLemmatizer(Lemmatizer): class RussianLemmatizer(Lemmatizer):
_morph = None _morph = None
def __init__(self, pymorphy2_lang='ru'): def __init__(self):
super(RussianLemmatizer, self).__init__() super(RussianLemmatizer, self).__init__()
try: try:
from pymorphy2 import MorphAnalyzer from pymorphy2 import MorphAnalyzer
except ImportError: except ImportError:
raise ImportError( raise ImportError(
"The Russian lemmatizer requires the pymorphy2 library: " "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: if RussianLemmatizer._morph is None:
RussianLemmatizer._morph = MorphAnalyzer(lang=pymorphy2_lang) RussianLemmatizer._morph = MorphAnalyzer()
def __call__(self, string, univ_pos, morphology=None): def __call__(self, string, univ_pos, morphology=None):
univ_pos = self.normalize_univ_pos(univ_pos) univ_pos = self.normalize_univ_pos(univ_pos)

View File

@ -1,15 +1,207 @@
# coding: utf8 # coding: utf8
from __future__ import unicode_literals from ...symbols import ADJ, DET, NOUN, NUM, PRON, PROPN, PUNCT, VERB, POS
from ...lemmatizer import Lemmatizer
from ..ru.lemmatizer import RussianLemmatizer
class UkrainianLemmatizer(RussianLemmatizer): class UkrainianLemmatizer(Lemmatizer):
def __init__(self, pymorphy2_lang="ru"): _morph = None
def __init__(self):
super(UkrainianLemmatizer, self).__init__()
try: try:
super(UkrainianLemmatizer, self).__init__(pymorphy2_lang="uk") from pymorphy2 import MorphAnalyzer
except ImportError:
if UkrainianLemmatizer._morph is None:
UkrainianLemmatizer._morph = MorphAnalyzer(lang="uk")
except (ImportError, TypeError):
raise ImportError( raise ImportError(
"The Ukrainian lemmatizer requires the pymorphy2 library and dictionaries: " "The Ukrainian lemmatizer requires the pymorphy2 library and
'try to fix it with "pip install git+https://github.com/kmike/pymorphy2.git pymorphy2-dicts-uk"' 'dictionaries: try to fix it with "pip uninstall pymorphy2" and'
'"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 = {"«": '"', "»": '"'}

View File

@ -6,8 +6,10 @@ STOP_WORDS = set(
"""а """а
або або
адже адже
аж
але але
алло алло
б
багато багато
без без
безперервно безперервно
@ -16,6 +18,7 @@ STOP_WORDS = set(
більше більше
біля біля
близько близько
бо
був був
буває буває
буде буде
@ -29,22 +32,27 @@ STOP_WORDS = set(
були були
було було
бути бути
бывь
в в
важлива
важливе
важливий
важливі
вам вам
вами вами
вас вас
ваш ваш
ваша ваша
ваше ваше
вашим
вашими
ваших
ваші ваші
вашій
вашого
вашої
вашому
вашою
вашу
вгорі вгорі
вгору вгору
вдалині вдалині
весь
вже вже
ви ви
від від
@ -59,7 +67,15 @@ STOP_WORDS = set(
вони вони
воно воно
восьмий восьмий
все
всею
всі
всім
всіх
всього всього
всьому
всю
вся
втім втім
г г
геть геть
@ -95,16 +111,15 @@ STOP_WORDS = set(
досить досить
другий другий
дуже дуже
дякую
е
є
ж
же же
життя
з з
за за
завжди завжди
зазвичай зазвичай
зайнята
зайнятий
зайняті
зайнято
занадто занадто
зараз зараз
зате зате
@ -112,22 +127,28 @@ STOP_WORDS = set(
звідси звідси
звідусіль звідусіль
здається здається
зі
значить значить
знову знову
зовсім зовсім
ім'я і
із
її
їй
їм
іноді іноді
інша інша
інше інше
інший інший
інших інших
інші інші
її
їй
їх їх
й
його його
йому йому
каже
ким ким
кілька
кого кого
кожен кожен
кожна кожна
@ -136,13 +157,13 @@ STOP_WORDS = set(
коли коли
кому кому
краще краще
крейдуючи крім
кругом
куди куди
ласка ласка
ледве
лише лише
люди м
людина має
майже майже
мало мало
мати мати
@ -157,20 +178,27 @@ STOP_WORDS = set(
мій мій
мільйонів мільйонів
мною мною
мого
могти могти
моє моє
мож моєї
моєму
моєю
може може
можна можна
можно можно
можуть можуть
можхо
мої мої
мор моїй
моїм
моїми
моїх
мою
моя моя
на на
навіть навіть
навіщо навіщо
навколо
навкруги навкруги
нагорі нагорі
над над
@ -183,10 +211,21 @@ STOP_WORDS = set(
наш наш
наша наша
наше наше
нашим
нашими
наших
наші наші
нашій
нашого
нашої
нашому
нашою
нашу
не не
небагато небагато
небудь
недалеко недалеко
неї
немає немає
нерідко нерідко
нещодавно нещодавно
@ -199,17 +238,22 @@ STOP_WORDS = set(
них них
ні ні
ніби ніби
ніж
ній
ніколи ніколи
нікуди нікуди
нім
нічого нічого
ну ну
нх
нього нього
ньому
о о
обидва
обоє обоє
один один
одинадцятий одинадцятий
одинадцять одинадцять
однак
однієї однієї
одній одній
одного одного
@ -218,11 +262,16 @@ STOP_WORDS = set(
он он
особливо особливо
ось ось
п'ятий
п'ятнадцятий
п'ятнадцять
п'ять
перед перед
перший перший
під під
пізніше пізніше
пір пір
після
по по
повинно повинно
подів подів
@ -233,18 +282,14 @@ STOP_WORDS = set(
потім потім
потрібно потрібно
почала почала
прекрасне початку
прекрасно
при при
про про
просто просто
проте проте
проти проти
п'ятий
п'ятнадцятий
п'ятнадцять
п'ять
раз раз
разу
раніше раніше
рано рано
раптом раптом
@ -252,6 +297,7 @@ STOP_WORDS = set(
роки роки
років років
року року
році
сам сам
сама сама
саме саме
@ -264,15 +310,15 @@ STOP_WORDS = set(
самого самого
самому самому
саму саму
світу
свого свого
своє своє
своєї
свої свої
своїй своїй
своїх своїх
свою свою
сеаой
себе себе
сих
сім сім
сімнадцятий сімнадцятий
сімнадцять сімнадцять
@ -300,7 +346,17 @@ STOP_WORDS = set(
також також
там там
твій твій
твого
твоє твоє
твоєї
твоєму
твоєю
твої
твоїй
твоїм
твоїми
твоїх
твою
твоя твоя
те те
тебе тебе
@ -312,15 +368,19 @@ STOP_WORDS = set(
тисяч тисяч
тих тих
ті ті
тієї
тією тією
тій
тільки тільки
тім
то
тобі тобі
тобою тобою
того того
тоді тоді
той той
том
тому тому
тою
треба треба
третій третій
три три
@ -338,7 +398,6 @@ STOP_WORDS = set(
усім усім
усіма усіма
усіх усіх
усію
усього усього
усьому усьому
усю усю
@ -356,6 +415,7 @@ STOP_WORDS = set(
цими цими
цих цих
ці ці
цієї
цій цій
цього цього
цьому цьому
@ -368,11 +428,24 @@ STOP_WORDS = set(
через через
четвертий четвертий
чи чи
чиє
чиєї
чиєму
чиї
чиїй
чиїм
чиїми
чиїх
чий
чийого
чийому
чим чим
численна численна
численне численне
численний численний
численні численні
чию
чия
чого чого
чому чому
чотири чотири
@ -385,6 +458,8 @@ STOP_WORDS = set(
ще ще
що що
щоб щоб
щодо
щось
я я
як як
яка яка
@ -393,6 +468,6 @@ STOP_WORDS = set(
які які
якій якій
якого якого
якщо якої
""".split() якщо""".split()
) )

View File

@ -179,7 +179,8 @@ def tt_tokenizer():
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def uk_tokenizer(): def uk_tokenizer():
pytest.importorskip("pymorphy2") pytest.importorskip("pymorphy2")
return get_lang_class("uk").Defaults.create_tokenizer() pytest.importorskip("pymorphy2.lang")
return util.get_lang_class("uk").Defaults.create_tokenizer()
@pytest.fixture(scope="session") @pytest.fixture(scope="session")

View File

@ -92,6 +92,7 @@ def test_uk_tokenizer_splits_open_appostrophe(uk_tokenizer, text):
assert tokens[0].text == "'" assert tokens[0].text == "'"
@pytest.mark.xfail(reason="See #3327")
@pytest.mark.parametrize("text", ["Тест''"]) @pytest.mark.parametrize("text", ["Тест''"])
def test_uk_tokenizer_splits_double_end_quote(uk_tokenizer, text): def test_uk_tokenizer_splits_double_end_quote(uk_tokenizer, text):
tokens = uk_tokenizer(text) tokens = uk_tokenizer(text)