diff --git a/spacy/lang/ru/lemmatizer.py b/spacy/lang/ru/lemmatizer.py index 8833f1a16..300d61c52 100644 --- a/spacy/lang/ru/lemmatizer.py +++ b/spacy/lang/ru/lemmatizer.py @@ -9,18 +9,19 @@ from ...compat import unicode_ 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) diff --git a/spacy/lang/uk/lemmatizer.py b/spacy/lang/uk/lemmatizer.py index 867cd3943..f41f36fe2 100644 --- a/spacy/lang/uk/lemmatizer.py +++ b/spacy/lang/uk/lemmatizer.py @@ -1,15 +1,207 @@ # coding: utf8 -from __future__ import unicode_literals - -from ..ru.lemmatizer import RussianLemmatizer +from ...symbols import ADJ, DET, NOUN, NUM, PRON, PROPN, PUNCT, VERB, POS +from ...lemmatizer import Lemmatizer -class UkrainianLemmatizer(RussianLemmatizer): - def __init__(self, pymorphy2_lang="ru"): +class UkrainianLemmatizer(Lemmatizer): + _morph = None + + 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"' + "The Ukrainian lemmatizer requires the pymorphy2 library and + '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 = {"«": '"', "»": '"'} diff --git a/spacy/lang/uk/stop_words.py b/spacy/lang/uk/stop_words.py index 83e86d937..cdf24dd70 100644 --- a/spacy/lang/uk/stop_words.py +++ b/spacy/lang/uk/stop_words.py @@ -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() ) diff --git a/spacy/tests/conftest.py b/spacy/tests/conftest.py index 2d50e3048..99d4eb197 100644 --- a/spacy/tests/conftest.py +++ b/spacy/tests/conftest.py @@ -179,7 +179,8 @@ def tt_tokenizer(): @pytest.fixture(scope="session") def uk_tokenizer(): 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") diff --git a/spacy/tests/lang/uk/test_tokenizer.py b/spacy/tests/lang/uk/test_tokenizer.py index 860d21953..f744b32b0 100644 --- a/spacy/tests/lang/uk/test_tokenizer.py +++ b/spacy/tests/lang/uk/test_tokenizer.py @@ -92,6 +92,7 @@ def test_uk_tokenizer_splits_open_appostrophe(uk_tokenizer, text): assert tokens[0].text == "'" +@pytest.mark.xfail(reason="See #3327") @pytest.mark.parametrize("text", ["Тест''"]) def test_uk_tokenizer_splits_double_end_quote(uk_tokenizer, text): tokens = uk_tokenizer(text)