Commits

Mikhail Korobov  committed 33d8761

ruscorpora.Tag class

  • Participants
  • Parent commits d73e2f7

Comments (0)

Files changed (6)

 Usage
 =====
 
-Obtaining corpora
------------------
+Corpus downloading
+------------------
 
 Download and unpack the archive with XML files from
 http://www.ruscorpora.ru/corpora-usage.html
 
-Using corpora
--------------
+Corpus reading
+--------------
 
 ``ruscorpora.parse_xml`` function parses single XML file and returns
 an iterator over sentences; each sentence is a list of ``ruscorpora.Token``
 
 ::
 
-    >>> import ruscorpora as rc
-    >>> for sent in rc.simplify(rc.parse('fiction.xml')):
+    >>> import ruscorpora as rnc
+    >>> for sent in rnc.simplify(rnc.parse('fiction.xml')):
     ...     print(sent)
 
+Working with tags
+-----------------
+
+``ruscorpora.Tag`` class is a convenient wrapper for tags used in
+ruscorpora::
+
+    >>> tag = rnc.Tag('S,f,inan=sg,nom')
+    >>> tag.POS
+    'S'
+    >>> tag.gender
+    'f'
+    >>> tag.animacy
+    'inan'
+    >>> tag.number
+    'sg'
+    >>> tag.case
+    'nom'
+    >>> tag.tense
+    None
+
+(there are also other attributes).
+
+Check if a grammeme is in tag::
+
+    >>> 'S' in tag
+    True
+    >>> 'V' in tag
+    False
+    >>> 'Foo' in tag
+    Traceback (most recent call last)
+    ...
+    ValueError: Grammeme is unknown: Foo
+
+Test tags equality::
+
+    >>> tag == rnc.Tag('S,f,inan=sg,nom')
+    True
+    >>> tag == 'S,f,inan=sg,nom'
+    True
+    >>> tag == rnc.Tag('S,f,inan=sg,acc')
+    False
+    >>> tag == 'S,f,inan=sg,acc'
+    False
+    >>> tag == 'Foo,inan'
+    Traceback (most recent call last)
+    ...
+    ValueError: Unknown grammemes: frozenset({Foo})
+
+Tags returned by ``rnc.simplify`` are wrapped with this class by default.
+
 Development
 ===========
 

File ruscorpora/__init__.py

 import warnings
 import functools
 from collections import namedtuple
+from .tagset import Tag
 
 Token = namedtuple('Token', 'text annotations')
 Annotation = namedtuple('Annotation', 'lex gr joined')
 
 
 def simplify(sents, remove_accents=True, join_split=True,
-             join_hyphenated=True, punct_tag='PNCT'):
+             join_hyphenated=True, punct_tag='PNCT', wrap_tags=True):
     """
     Simplify the result of ``sents`` parsing:
 
     * annotate punctuation with ``punct_tag``;
     * join split words into a single token (if ``join_split==True``);
     * join hyphenated words to a single token (if ``join_hyphenated==True``);
-    * remove accents (if ``remove_accents==True``).
+    * remove accents (if ``remove_accents==True``);
+    * convert string tag representation to ruscorpora.Tag instances
+      (if ``wrap_tags==True``).
     """
 
     def remove_extra_annotations(token):
 
             yield text, new_annotations
 
+    def with_wrapped_tags(sent):
+        for text, annotations in sent:
+            new_annotations = []
+            for ann in annotations:
+                new_annotations.append(ann._replace(gr=Tag(ann.gr)))
+            yield text, new_annotations
+
 
     for sent in sents:
         sent = map(remove_extra_annotations, sent)
 
         sent = fix_punct_tags(sent)
 
+        if wrap_tags:
+            sent = with_wrapped_tags(sent)
+
         yield [Token(*t) for t in sent]
 
 

File ruscorpora/tagset.py

+# -*- coding: utf-8 -*-
+"""
+Python wrapper for tags used in http://www.ruscorpora.ru/
+"""
+from __future__ import absolute_import, unicode_literals
+
+# Часть речи:
+POS_TAGS = frozenset([
+    'S',            # существительное (яблоня, лошадь, корпус, вечность)
+    'A',            # прилагательное (коричневый, таинственный, морской)
+    'NUM',          # числительное (четыре, десять, много)
+    'A-NUM',        # числительное-прилагательное (один, седьмой, восьмидесятый)
+    'V',            # глагол (пользоваться, обрабатывать)
+    'ADV',          # наречие (сгоряча, очень)
+    'PRAEDIC',      # предикатив (жаль, хорошо, пора)
+    'PARENTH',      # вводное слово (кстати, по-моему)
+    'S-PRO',        # местоимение-существительное (она, что)
+    'A-PRO',        # местоимение-прилагательное (который, твой)
+    'ADV-PRO',      # местоименное наречие (где, вот)
+    'PRAEDIC-PRO',  # местоимение-предикатив (некого, нечего)
+    'PR',           # предлог (под, напротив)
+    'CONJ',         # союз (и, чтобы)
+    'PART',         # частица (бы, же, пусть)
+    'INTJ',         # междометие (увы, батюшки)
+
+    'ANUM',         # XXX: так на самом деле называется 'A-NUM'
+    'NONLEX',
+])
+
+# Род:
+GENDERS = frozenset([
+    'm',    # мужской род (работник, стол)
+    'f',    # женский род (работница, табуретка)
+    'm-f',  # «общий род» (задира, пьяница)
+    'n',    # средний род (животное, озеро)
+])
+
+# Одушевленность:
+ANIMACY = frozenset([
+    'anim', # одушевленность (человек, ангел, утопленник)
+    'inan', # неодушевленность (рука, облако, культура)
+])
+
+# Число:
+NUMBERS = frozenset([
+    'sg', # единственное число (яблоко, гордость)
+    'pl', # множественное число (яблоки, ножницы, детишки)
+])
+
+# Падеж:
+CASES = frozenset([
+    'nom',  # именительный падеж (голова, сын, степь, сани, который)
+    'gen',  # родительный падеж (головы, сына, степи, саней, которого)
+    'dat',  # дательный падеж (голове, сыну, степи, саням, которому)
+    'acc',  # винительный падеж (голову, сына, степь, сани, который/которого)
+    'ins',  # творительный падеж (головой, сыном, степью, санями, которым)
+    'loc',  # предложный падеж ([о] голове, сыне, степи, санях, котором)
+    'gen2', # второй родительный падеж (чашка чаю)
+    'acc2', # второй винительный падеж (постричься в монахи; по два человека)
+    'loc2', # второй предложный падеж (в лесу, на оси)
+    'voc',  # звательная форма (Господи, Серёж, ребят)
+    'adnum', # счётная форма (два часа, три шара)
+])
+
+# Краткая/полная форма:
+SHORT_FULL = frozenset([
+    'brev', # краткая форма (высок, нежна, прочны, рад)
+    'plen', # полная форма (высокий, нежная, прочные, морской)
+])
+
+# Степень сравнения:
+DEGREES_OF_COMPARISON = frozenset([
+    'comp',     # сравнительная степень (глубже)
+    'comp2',    # форма «по+сравнительная степень» (поглубже)
+    'supr',     # превосходная степень (глубочайший)
+])
+
+# Вид:
+ASPECTS = frozenset([
+    'pf',  # совершенный вид (пошёл, встречу)
+    'ipf', # несовершенный вид (ходил, встречаю)
+])
+
+# Переходность:
+TRANSITIVITY = frozenset([
+    'intr', # непереходность (ходить, вариться)
+    'tran', # переходность (вести, варить)
+])
+
+# Залог:
+VOICES = frozenset([
+    'act',  # действительный залог (разрушил, разрушивший)
+    'pass', # страдательный залог (только у причастий: разрушаемый, разрушенный)
+    'med',  # медиальный, или средний залог (глагольные формы на -ся: разрушился и т.п.)
+])
+
+# Форма (репрезентация) глагола:
+VERB_FORMS = frozenset([
+    'inf',      # инфинитив (украшать)
+    'partcp',   # причастие (украшенный)
+    'ger',      # деепричастие (украшая)
+])
+
+# Наклонение:
+GRAMMATICAL_MOODS = frozenset([
+    'indic',    # изъявительное наклонение (украшаю, украшал, украшу)
+    'imper',    # повелительное наклонение (украшай)
+    'imper2',   # форма повелительного наклонения 1 л. мн. ч. на -те (идемте)
+])
+
+# Время:
+TENSES = frozenset([
+    'praet', # прошедшее время (украшали, украшавший, украсив)
+    'praes', # настоящее время (украшаем, украшающий, украшая)
+    'fut',   # будущее время (украсим)
+])
+
+# Лицо:
+PERSONS = frozenset([
+    '1p', # первое лицо (украшаю)
+    '2p', # второе лицо (украшаешь)
+    '3p', # третье лицо (украшает)
+])
+
+# Прочие признаки:
+OTHER_GRAMMEMES = frozenset([
+    'persn',    # личное имя (Иван, Дарья, Леопольд, Эстер, Гомер, Маугли)
+    'patrn',    # отчество (Иванович, Павловна) famn — фамилия (Николаев, Волконская, Гумбольдт)
+    'zoon',     # кличка животного (Шарик, Дочка)
+    '0',        # несклоняемое (шоссе, Седых)
+
+    # в справке почему-то нет
+
+    'obsc', # ругательство
+    'famn', # фамилия
+
+])
+
+# В корпусе со снятой грамматической омонимией предусмотрен ряд помет,
+# указывающих на нестандартность и/или особенности записи входящей
+# в Корпус словоформы:
+NON_STANDARD_GRAMMEMES = frozenset([
+    # отсутствие особенностей
+    'normal',
+
+    # («Аномальная форма») — различного рода морфологические аномалии, возможные
+    # у устаревших или просторечных нелитературных форм (три дни при нормативном три
+    # дня, ляжь при нормативном ляг)
+    'anom',
+
+    # («Искаженная форма»)  — орфографическое и/или фонетическое искажение слова, часто
+    # передающее различные особенности произношения (дэвушка, това’ищи, про-хо-ди, низнаю).
+    'distort',
+
+    # («Цифровая запись»)  — запись числительного, числительного-прилагательного или
+    # прилагательного (полностью или частично) при помощи цифр (73, LXXIII, 73-й, 22-летний). Для
+    # этих словоформ в поле «Лексема» также употребляется цифровая запись; число и падеж
+    # указываются только в тех случаях, когда выписано окончание (типа 14-му).
+    'ciph',
+
+    # («Инициал»)  — запись вида «заглавная буква с точкой» (М., Р.). В поле «Лексема» инициал
+    # не раскрывается; грамматические признаки не указываются.
+    'INIT',
+
+    # («Сокращение»)  — сокращенная запись (тов., гг., ч.). В поле «Лексема» сокращение (кроме
+    # инициалов) раскрывается, указывается грамматическая форма, соответствующая контексту.
+    # Специально отметим, что акронимы вроде ООН, вуз и усеченные слова вроде зав, зам,
+    # записываемые без точки и не раскрываемые при чтении, не получают пометы abbr и трактуются
+    # как обычные слова (склоняемые или несклоняемые).
+    'abbr',
+
+])
+
+# Граммемы, которые отсутствуют в корпусе, но могут быть приписаны
+# этим пакетом:
+CUSTOM_GRAMMEMES = frozenset([
+    'PNCT', # граммема для пунктуации
+])
+
+ALLOWED_GRAMMEMES = (POS_TAGS | GENDERS | ANIMACY | NUMBERS | CASES |
+                     SHORT_FULL | DEGREES_OF_COMPARISON | ASPECTS |
+                     TRANSITIVITY | VOICES | VERB_FORMS | GRAMMATICAL_MOODS |
+                     TENSES | PERSONS | OTHER_GRAMMEMES |
+                     NON_STANDARD_GRAMMEMES | CUSTOM_GRAMMEMES)
+
+
+class Tag(object):
+    def __init__(self, tag):
+        self._tag = tag
+
+        # Example: V,ipf,intr,act=n,sg,praet,indic
+        self._grammemes = self._split_to_grammemes(tag)
+        self._grammeme_set = frozenset(self._grammemes)
+
+        self._assert_grammemes_are_valid(self._grammeme_set)
+
+    @property
+    def POS(self):
+        return self._grammemes[0]
+
+    @property
+    def gender(self):
+        return self._grammatical_feature(GENDERS)
+
+    @property
+    def animacy(self):
+        return self._grammatical_feature(ANIMACY)
+
+    @property
+    def number(self):
+        return self._grammatical_feature(NUMBERS)
+
+    @property
+    def case(self):
+        return self._grammatical_feature(CASES)
+
+    @property
+    def short_full(self):
+        return self._grammatical_feature(SHORT_FULL)
+
+    @property
+    def degree_of_comparison(self):
+        return self._grammatical_feature(DEGREES_OF_COMPARISON)
+
+    @property
+    def aspect(self):
+        return self._grammatical_feature(ASPECTS)
+
+    @property
+    def transitivity(self):
+        return self._grammatical_feature(TRANSITIVITY)
+
+    @property
+    def voice(self):
+        return self._grammatical_feature(VOICES)
+
+    @property
+    def verb_form(self):
+        return self._grammatical_feature(VERB_FORMS)
+
+    @property
+    def mood(self):
+        return self._grammatical_feature(GRAMMATICAL_MOODS)
+
+    @property
+    def tense(self):
+        return self._grammatical_feature(TENSES)
+
+    @property
+    def person(self):
+        return self._grammatical_feature(PERSONS)
+
+    def __contains__(self, grammeme):
+        if grammeme in self._grammeme_set:
+            return True
+        else:
+            if grammeme in ALLOWED_GRAMMEMES:
+                return False
+            else:
+                raise ValueError("Grammeme is unknown: %s" % grammeme)
+
+    def __eq__(self, other):
+        if isinstance(other, Tag):
+            return other._grammeme_set == self._grammeme_set
+
+        grammemes = frozenset(self._split_to_grammemes(other))
+        self._assert_grammemes_are_valid(grammemes)
+
+        return grammemes == self._grammeme_set
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __repr__(self): # XXX: this is incorrect in Python 2.x
+        return "Tag(%r)" % self._tag
+
+    def __str__(self): # XXX: this is incorrect in Python 2.x
+        return self._tag
+
+    def _assert_grammemes_are_valid(self, grammemes):
+        unknown_grammemes = grammemes - ALLOWED_GRAMMEMES
+        if unknown_grammemes:
+            msg = "Unknown grammemes: %s" % str(unknown_grammemes)
+            raise ValueError(msg)
+
+    def _grammatical_feature(self, feature_set):
+        grammemes = feature_set & self._grammeme_set
+        if not grammemes:
+            return None
+        return next(iter(grammemes))
+
+    @classmethod
+    def _split_to_grammemes(cls, tag_txt):
+        return tag_txt.replace('=', ',').split(',')
+

File tests/test_reader.py

 # -*- coding: utf-8 -*-
 from __future__ import absolute_import, unicode_literals
 import io
-import ruscorpora as rc
+import ruscorpora as rnc
 
 def _parse(corpus_xml):
     corpus = '<?xml version="1.0" encoding="utf-8" ?>\n<corpus>\n%s\n</corpus>' % corpus_xml
     fp = io.BytesIO(corpus.encode('utf8'))
-    return list(rc.simplify(rc.parse_xml(fp)))
+    return list(rnc.simplify(rnc.parse_xml(fp)))
 
 
 def test_simple():
 
     assert _parse(corpus) == [
         [
-            ('«', [rc.Annotation(lex='«', gr='PNCT', joined=None)]),
-            ('Школа', [rc.Annotation(lex='школа', gr='S,f,inan=sg,nom', joined=None)]),
-            ('злословия', [rc.Annotation(lex='злословие', gr='S,n,inan=sg,gen', joined=None)]),
-            (' » ,-', [rc.Annotation(lex=' » ,-', gr='PNCT', joined=None)]),
-            ('СМИ', [rc.Annotation(lex='сми', gr='S,0=sg,nom', joined=None)]),
-            (' !', [rc.Annotation(lex=' !', gr='PNCT', joined=None)])
+            ('«', [rnc.Annotation(lex='«', gr='PNCT', joined=None)]),
+            ('Школа', [rnc.Annotation(lex='школа', gr='S,f,inan=sg,nom', joined=None)]),
+            ('злословия', [rnc.Annotation(lex='злословие', gr='S,n,inan=sg,gen', joined=None)]),
+            (' » ,-', [rnc.Annotation(lex=' » ,-', gr='PNCT', joined=None)]),
+            ('СМИ', [rnc.Annotation(lex='сми', gr='S,0=sg,nom', joined=None)]),
+            (' !', [rnc.Annotation(lex=' !', gr='PNCT', joined=None)])
         ]
     ]
 
     assert _parse(corpus) == [
         [
             ('Сегодня-завтра', [
-                rc.Annotation(lex='Сегодня', gr='ADV', joined='hyphen'),
-                rc.Annotation(lex='завтра', gr='ADV', joined='hyphen')]),
-            ('школа', [rc.Annotation(lex='школа', gr='S,f,inan=sg,nom', joined=None)])
+                rnc.Annotation(lex='Сегодня', gr='ADV', joined='hyphen'),
+                rnc.Annotation(lex='завтра', gr='ADV', joined='hyphen')]),
+            ('школа', [rnc.Annotation(lex='школа', gr='S,f,inan=sg,nom', joined=None)])
         ]
     ]
 
     """
     assert _parse(corpus) == [
         [
-            ('Злословия', [rc.Annotation(lex='злословие', gr='S,n,inan=sg,gen', joined=None)]),
-            (' -', [rc.Annotation(lex=' -', gr='PNCT', joined=None)]),
+            ('Злословия', [rnc.Annotation(lex='злословие', gr='S,n,inan=sg,gen', joined=None)]),
+            (' -', [rnc.Annotation(lex=' -', gr='PNCT', joined=None)]),
             ('полдюжины', [
-                rc.Annotation(lex='пол', gr='NUM', joined='together'),
-                rc.Annotation(lex='дюжина', gr='S,f,inan=sg,gen', joined='together')])
+                rnc.Annotation(lex='пол', gr='NUM', joined='together'),
+                rnc.Annotation(lex='дюжина', gr='S,f,inan=sg,gen', joined='together')])
         ]
     ]

File tests/test_tagset.py

+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+import pytest
+import ruscorpora as rnc
+
+def test_attributes():
+    tag = rnc.Tag('S,f,inan=sg,nom')
+    assert tag.POS == 'S'
+    assert tag.gender == 'f'
+    assert tag.animacy == 'inan'
+    assert tag.number == 'sg'
+    assert tag.case == 'nom'
+    assert tag.tense is None
+    assert tag.short_full is None
+    assert tag.aspect is None
+    assert tag.degree_of_comparison is None
+    assert tag.mood is None
+    assert tag.person is None
+    assert tag.transitivity is None
+    assert tag.verb_form is None
+    assert tag.voice is None
+
+def test_attributes2():
+    tag = rnc.Tag('V,ipf,intr,act=n,sg,praet,indic')
+
+    assert tag.POS == 'V'
+    assert tag.aspect == 'ipf'
+    assert tag.gender == 'n'
+    assert tag.mood == 'indic'
+    assert tag.tense == 'praet'
+    assert tag.transitivity == 'intr'
+
+def test_contains():
+    tag = rnc.Tag('V,ipf,intr,act=n,sg,praet,indic')
+    assert 'V' in tag
+    assert 'ipf' in tag
+    assert 'indic' in tag
+
+    assert 'S' not in tag
+    assert 'pl' not in tag
+
+    with pytest.raises(ValueError):
+        'Foo' in tag
+
+def test_eq():
+    assert rnc.Tag('V') == 'V'
+    assert rnc.Tag('S,f,inan=sg,nom') == 'S,f,inan,sg,nom'
+    assert rnc.Tag('S,f') == rnc.Tag('S,f')
+
+    assert rnc.Tag('S,f') != rnc.Tag('V')
+    assert rnc.Tag('V') != 'S,f'
+
+    with pytest.raises(ValueError):
+        rnc.Tag('S,f') == 'Foo'
 [testenv]
 deps=
     pytest
+    pytest-cov
+    coverage
 
 commands=
     py.test []