Commits

Mikhail Korobov committed a650d2b Merge

merge changes from master

  • Participants
  • Parent commits b960191, 821d341

Comments (0)

Files changed (15)

+
+*.pyc
+*.*~
+*.swp
+Thumbs.db
+
+.idea/
+.rope*
+PYSMELLTAGS
+reports
+
+.tox
+MANIFEST
 d3179ad2e629694ad9de38cd06a4229843ca8d6b 0.3.2
 0d3a526d52d5bf34eac477e5957c8c7b00782e69 0.3.3
 2f20c1b39d369bb36d8760a0a384348cfc9063cd 0.3.4
+78b66a1250795099d1cd435b687e8e87a35dc144 0.3.5
 История изменений
 =================
 
+0.3.5 (2013-06-30)
+------------------
+
+- Препроцессинг словаря: loc1/gen1/acc1 заменяются на loct/gent/accs;
+  варианты написания тегов унифицируются (чтоб их было меньше);
+- исправлено согласование слов с числительными;
+- при склонении слов в loc2/gen2/acc2/voct слово ставится в loct/gent/accs/nomn,
+  если вариантов с loc2/gen2/acc2/voct не найдено.
+
+Для полноценного обновления лучше обновить pymorphy2-dicts до версии >= 2.2.
+
 0.3.4 (2013-04-29)
 ------------------
 

File docs/user/grammemes.rst

 =======================
 
 В pymorphy2 используются словари OpenCorpora и :term:`граммемы <граммема>`,
-принятые в OpenCorpora (с небольшими добавлениями).
+принятые в OpenCorpora (с небольшими изменениями).
 
 Полный список граммем OpenCorpora доступен тут: http://opencorpora.org/dict.php?act=gram
 
 .. _russian-POS:
 
-Части речи
+Часть речи
 ----------
 
 ==========   =============================     =================================
 
 .. _russian-cases:
 
-Падежи
-------
+Падеж
+-----
 
-========   ===================    ===========================    ==============================
+========   ===================    ===========================    ================================
 Граммема   Значение               Пояснение                      Примеры
-========   ===================    ===========================    ==============================
+========   ===================    ===========================    ================================
 nomn       именительный           Кто? Что?                      **хомяк** ест
 gent       родительный            Кого? Чего?                    у нас нет **хомяка**
 datv       дательный              Кому? Чему?                    сказать **хомяку** спасибо
 loct       предложный             О ком? О чём? и т.п.           хомяка несут в **корзинке**
 voct       звательный             Его формы используются         **Саш**, пойдем в кино.
                                   при обращении к человеку.
-gen1       первый родительный     То же самое, что и             производство **сахара**;
-                                  родительный; указывается,      нет **яда**
-                                  когда у слова выделяется
-                                  форма gen2.
-gen2       второй родительный                                    ложка **сахару**;
-           (частичный)                                           стакан **яду**
+gen2       второй родительный                                    ложка **сахару**
+           (частичный)                                           *(gent - производство сахара)*;
+                                                                 стакан **яду**
+                                                                 *(gent - нет яда)*
 acc2       второй винительный                                    записался в **солдаты**
-loc1       первый предложный      То же самое, что               напомнить о **долге**;
-                                  и предложный; указывается,     монолог о **шкафе**;
-                                  когда у слова выделяется       писать о **снеге**
-                                  форма loc2.
-loc2       второй предложный                                     я у него в **долгу**;
-           (местный)                                             висит в **шкафу**;
+loc2       второй предложный                                     я у него в **долгу**
+           (местный)                                             *(loct - напоминать о долге)*;
+                                                                 висит в **шкафу**
+                                                                 *(loct - монолог о шкафе)*;
                                                                  весь в **снегу**
-========   ===================    ===========================    ==============================
+                                                                 *(loct - писать о снеге)*
+========   ===================    ===========================    ================================
 
 Падеж выделяется у существительных, полных прилагательных, полных причастий,
 числительных и местоимений. Получить его можно через атрибут case::
     >>> p.tag.case
     'datv'
 
+.. note::
+
+    В OpenCorpora (на июль 2013) есть еще падежи gen1 и loc1. Они указываются
+    вместо gent/loct, когда у слова есть форма gen2/loc2. В pymorphy2 gen1 и
+    loc1 заменены на gent/loct, чтоб с ними было проще работать.
+
+Число
+-----
+
+==========   =============================     =================================
+Граммема     Значение                          Примеры
+==========   =============================     =================================
+sing         единственное число                хомяк, говорит
+plur         множественное число               хомяки, говорят
+==========   =============================     =================================
+
+::
+
+    >>> p = morph.parse('люди')[0]
+    >>> p.tag.number
+    'plur'
+
 
 .. _non-standard-grammemes:
 
 LATN      Токен состоит из латинских букв (например, "foo-bar" или "Maßstab")
 PNCT      Пунктуация (например, ``,`` или ``!?`` или ``…``)
 NUMB      Число (например, "204")
+ROMN      Римское число (например, XI)
 ========  ===================================================================
 
 Пример::

File pymorphy2/analyzer.py

 
 
     def _inflect(self, form, required_grammemes):
-        grammemes = form[1].updated_grammemes(required_grammemes)
-
         possible_results = [f for f in self.get_lexeme(form)
                             if required_grammemes <= f[1].grammemes]
 
+        if not possible_results:
+            required_grammemes = self.TagClass.fix_rare_cases(required_grammemes)
+            possible_results = [f for f in self.get_lexeme(form)
+                                if required_grammemes <= f[1].grammemes]
+
+        grammemes = form[1].updated_grammemes(required_grammemes)
         def similarity(frm):
             tag = frm[1]
             return len(grammemes & tag.grammemes)

File pymorphy2/opencorpora_dict/compile.py

     ``out_path`` should be a name of folder where to put dictionaries.
     """
     from .parse import parse_opencorpora_xml
+    from .preprocess import simplify_tags
     from .storage import save_compiled_dict
 
     dawg.assert_can_create()
         return
 
     parsed_dict = parse_opencorpora_xml(opencorpora_dict_path)
+    simplify_tags(parsed_dict)
     compiled_dict = compile_parsed_dict(parsed_dict, prediction_options)
-
     save_compiled_dict(compiled_dict, out_path)
 
 
     lexemes = _join_lexemes(parsed_dict.lexemes, parsed_dict.links)
 
     logger.info('building paradigms...')
-    logger.debug("%20s %15s %15s %15s", "stem", "len(gramtab)", "len(words)", "len(paradigms)")
+    logger.debug("%20s %15s %15s %15s", "word", "len(gramtab)", "len(words)", "len(paradigms)")
 
     paradigm_popularity = collections.defaultdict(int)
 

File pymorphy2/opencorpora_dict/parse.py

             grammemes.append(grammeme)
             xml_clear_elem(elem)
 
-
         if elem.tag == 'lemma':
             if not lexemes:
                 logger.info('parsing xml:lemmas...')
     return ParsedDictionary(lexemes, links, grammemes, version, revision)
 
 
-def _tags_from_elem(elem):
+def _grammemes_from_elem(elem):
     return ",".join(g.get('v') for g in elem.findall('g'))
 
 
 def _word_forms_from_xml_elem(elem):
     """
-    Return a list of (word, tags) pairs given "lemma" XML element.
+    Return a list of (word, tag) pairs given "lemma" XML element.
     """
     lexeme = []
     lex_id = elem.get('id')
     base_info = elem.findall('l')
 
     assert len(base_info) == 1
-    base_tags = _tags_from_elem(base_info[0])
+    base_grammemes = _grammemes_from_elem(base_info[0])
 
     for form_elem in elem.findall('f'):
-        tags = _tags_from_elem(form_elem)
+        grammemes = _grammemes_from_elem(form_elem)
         form = form_elem.get('t').lower()
         lexeme.append(
-            (form, " ".join([base_tags, tags]).strip())
+            (form, " ".join([base_grammemes, grammemes]).strip())
         )
 
     return lex_id, lexeme

File pymorphy2/opencorpora_dict/preprocess.py

+# -*- coding: utf-8 -*-
+"""
+:mod:`pymorphy2.opencorpora_dict.preprocess` is a
+module for preprocessing parsed OpenCorpora dictionaries.
+
+The presence of this module means that pymorphy2 dictionaries are
+not fully compatible with OpenCorpora.
+"""
+from __future__ import absolute_import, unicode_literals
+import logging
+import collections
+logger = logging.getLogger(__name__)
+
+
+def simplify_tags(parsed_dict, skip_space_ambiguity=True):
+    """
+    This function simplifies tags in :param:`parsed_dict`.
+    :param:`parsed_dict` is modified inplace.
+    """
+    logger.info("simplifying tags: looking for tag spellings")
+    spellings = _get_tag_spellings(parsed_dict)
+
+    logger.info("simplifying tags: looking for spelling duplicates "
+                "(skip_space_ambiguity: %s)", skip_space_ambiguity)
+    tag_replaces = _get_duplicate_tag_replaces(spellings, skip_space_ambiguity)
+    logger.debug("%d duplicate tags will be removed", len(tag_replaces))
+
+    logger.info("simplifying tags: fixing")
+    for lex_id in parsed_dict.lexemes:
+        new_lexeme = [
+            (word, _simplify_tag(tag, tag_replaces))
+            for word, tag in parsed_dict.lexemes[lex_id]
+        ]
+        parsed_dict.lexemes[lex_id] = new_lexeme
+
+
+def _get_tag_spellings(parsed_dict):
+    """
+    Return a dict where keys are sets of grammemes found in dictionary
+    and values are counters of all tag spellings for these grammemes.
+    """
+    spellings = collections.defaultdict(lambda: collections.defaultdict(int))
+    for tag in _itertags(parsed_dict):
+        spellings[_get_grammemes(tag)][tag] += 1
+    return spellings
+
+
+def _get_duplicate_tag_replaces(spellings, skip_space_ambiguity):
+    replaces = {}
+    for grammemes in spellings:
+        tags = spellings[grammemes]
+        if _is_ambiguous(tags.keys(), skip_space_ambiguity):
+            items = sorted(tags.items(), key=lambda it: it[1], reverse=True)
+            top_tag = items[0][0]
+            for tag, count in items[1:]:
+                replaces[tag] = top_tag
+    return replaces
+
+
+def _is_ambiguous(tags, skip_space_ambiguity=True):
+    """
+    >>> _is_ambiguous(['NOUN sing,masc'])
+    False
+    >>> _is_ambiguous(['NOUN sing,masc', 'NOUN masc,sing'])
+    True
+    >>> _is_ambiguous(['NOUN masc,sing', 'NOUN,masc sing'])
+    False
+    >>> _is_ambiguous(['NOUN masc,sing', 'NOUN,masc sing'], skip_space_ambiguity=False)
+    True
+    """
+    if len(tags) < 2:
+        return False
+
+    if skip_space_ambiguity:
+        # if space position differs then skip this ambiguity
+        # XXX: this doesn't handle cases when space position difference
+        # is not the only ambiguity
+        space_pos = [tag.index(' ') if ' ' in tag else None
+                     for tag in map(str, tags)]
+        if len(space_pos) == len(set(space_pos)):
+            return False
+
+    return True
+
+
+def _simplify_tag(tag, tag_replaces):
+    tag = _replace_grammemes(tag)
+    return tag_replaces.get(tag, tag)
+
+
+def _replace_grammemes(tag):
+    return tag.replace('loc1', 'loct').replace('gen1', 'gent').replace('acc1', 'accs')
+
+
+def _get_grammemes(tag):
+    return frozenset(tag.replace(' ', ',', 1).split(','))
+
+
+def _itertags(parsed_dict):
+    for lex_id in parsed_dict.lexemes:
+        for word, tag in parsed_dict.lexemes[lex_id]:
+            yield _replace_grammemes(tag)

File pymorphy2/opencorpora_dict/storage.py

File contents unchanged.

File pymorphy2/tagset.py

         set(['plur', 'gent']),
     )
 
+    RARE_CASES = {
+        'gen2': 'gent',
+        'acc2': 'accs',
+        'loc2': 'loct',
+        'voct': 'nomn'
+    }
+
     def __init__(self, tag):
         self._str = tag
-
         # XXX: we loose information about which grammemes
         # belongs to lexeme and which belongs to form
         # (but this information seems useless for pymorphy2).
         return new_grammemes
 
     @classmethod
+    def fix_rare_cases(cls, grammemes):
+        """
+        Replace rare cases (loc2/voct/...) with common ones (loct/nomn/...).
+        """
+        return frozenset(cls.RARE_CASES.get(g,g) for g in grammemes)
+
+    @classmethod
     def _init_grammemes(cls, dict_grammemes):
         """
         Initialize various class attributes with grammeme

File pymorphy2/version.py

-__version__ = "0.3.4"
+__version__ = "0.3.5"

File requirements.txt

 dawg-python >= 0.5
-pymorphy2-dicts > 2.0, < 3.0
+pymorphy2-dicts >= 2.2, < 3.0

File tests/test_inflection.py

 import pytest
 
 from .utils import morph
+from pymorphy2.shapes import restore_word_case
 
 def with_test_data(data):
     return pytest.mark.parametrize(
     assert len(inflected_variants)
 
     inflected = inflected_variants[0]
-    assert inflected.word == result
+    assert restore_word_case(inflected.word, word) == result
 
 
 @with_test_data([
     assert_first_inflected_variant('орел', ['gent'], 'орла')
 
 
-# TODO: see https://github.com/kmike/pymorphy2/issues/8
+@with_test_data([
+    ('снег', ['gent'], 'снега'),
+    ('снег', ['gen2'], 'снегу'),
+    ('Боря', ['voct'], 'Борь'),
+])
+def test_second_cases(word, grammemes, result):
+    assert_first_inflected_variant(word, grammemes, result)
+
+
+@with_test_data([
+    ('валенок', ['gent'], 'валенка'),
+    ('валенок', ['gen2'], 'валенка'),  # there is no gen2
+    ('велосипед', ['loct'], 'велосипеде'), # о велосипеде
+    ('велосипед', ['loc2'], 'велосипеде'), # а тут второго предложного нет, в велосипеде
+    ('хомяк', ['voct'], 'хомяк'),        # there is not voct, nomn should be used
+    ('Геннадий', ['voct'], 'Геннадий'),  # there is not voct, nomn should be used
+])
+def test_case_substitution(word, grammemes, result):
+    assert_first_inflected_variant(word, grammemes, result)
+
+
 @pytest.mark.xfail
 @with_test_data([
     # доп. падежи, fixme
     ('лес', ['loct'], 'лесе'),   # о лесе
     ('лес', ['loc2'], 'лесу'),   # в лесу
-    ('велосипед', ['loct'], 'велосипеде'), # о велосипеде
-    ('велосипед', ['loc2'], 'велосипеде'), # а тут второго предложного нет, в велосипеде
+    ('острова', ['datv'], 'островам'),
 ])
-def test_loc2(word, grammemes, result):
+def test_best_guess(word, grammemes, result):
     assert_first_inflected_variant(word, grammemes, result)
-
-
-@pytest.mark.xfail
-def test_best_guess():
-    assert_first_inflected_variant('острова', ['datv'], 'островам')

File tests/test_numeral_agreement.py

     ("книгой", 'ablt', ["книгой", "книгами", "книгами"]),
     ("книге", 'loct', ["книге", "книгах", "книгах"]),
 
-    # ("час", "accs", ["час", "часа", "часов"]), # https://github.com/kmike/pymorphy2/issues/32
+    ("час", "accs", ["час", "часа", "часов"]), # see https://github.com/kmike/pymorphy2/issues/32
     ("день", "accs", ["день", "дня", "дней"]),
     ("минуту", "accs", ["минуту", "минуты", "минут"]),
 ])
 deps =
     pytest
     psutil
-    pymorphy2-dicts >2.0, <3.0
+    pymorphy2-dicts >=2.2, <3.0
 
 [testenv]
 deps=