{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Полезные практики, неочевидные моменты"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Неоднозная грамматика"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Есть примитивная рекурсивная грамматика:\n",
"```\n",
"A -> a | a a\n",
"B -> A B | A\n",
"```\n",
"\n",
"Есть строка \"a a a\". Парсер может разобрать её 3 способами:\n",
"```\n",
"(a) (a) (a)\n",
"(a) (a a)\n",
"(a a) (a)\n",
"```\n",
"\n",
"Yargy парсер перебирает все варианты разбора. Используем непубличный метод `extract`, посмотрим все варианты:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"R0 -> R1 R0 | R1\n",
"R1 -> 'a' | 'a' 'a'\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n",
"\n",
"\n"
],
"text/plain": [
"Graph(nodes=[...], edges=[...])"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n",
"\n",
"\n"
],
"text/plain": [
"Graph(nodes=[...], edges=[...])"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n",
"\n",
"\n"
],
"text/plain": [
"Graph(nodes=[...], edges=[...])"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"from yargy.parser import prepare_trees\n",
"from yargy import Parser, or_, rule\n",
"\n",
"A = or_(\n",
" rule('a'),\n",
" rule('a', 'a') \n",
")\n",
"B = A.repeatable()\n",
"display(B.normalized.as_bnf)\n",
"\n",
"parser = Parser(B)\n",
"matches = parser.extract('a a a')\n",
"for match in matches:\n",
" # кроме 3-х полных разборов, парсёр найдёт ещё 7 частичных: (a) _ _, (a a) _, (a) (a) _, ...\n",
" # не будем их показывать\n",
" if len(match.tokens) == 3:\n",
" display(match.tree.as_dot)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Число вариантов быстро растёт. Для строки \"a x 10\", парсер переберёт 89 разборов. Для \"a x 20\" — 979 и потратит заметное количество времени.\n",
"\n",
"При работе с естественным русским языком, мы построянно сталкиваемся с неоднозначными грамматиками. Например, список из трёх взысканий по арбиражному делу: \"5 тыс. р. штраф пени 3 тыс. р. необоснованного обогащения\". Эскиз грамматики:\n",
"```\n",
"MONEY -> INT тыс. р.\n",
"TYPE -> штраф | пени | необоснованное обогащение\n",
"\n",
"# 1. \"5 тыс. р. штраф\"\n",
"# 2. \"штраф 5 тыс. р.\"\n",
"# 3. \"3 тыс. р.\" — только сумма\n",
"# 4. \"пени\" — только тип\n",
"PENALTY -> MONEY TYPE | TYPE MONEY | MONEY | TYPE\n",
"\n",
"PENALTIES -> PENALTY+\n",
"```\n",
"\n",
"Получаем много вариантов разбора:\n",
"```\n",
"(5 тыс. р. штраф) (пени) (3 тыс. р. необоснованного обогащения)\n",
"(5 тыс. р. штраф) (пени) (3 тыс. р.) (необоснованного обогащения)\n",
"(5 тыс. р.) (штраф) (пени) (3 тыс. р.) (необоснованного обогащения)\n",
"(5 тыс. р. штраф) (пени 3 тыс. р.) (необоснованного обогащения)\n",
"...\n",
"```\n",
"\n",
"Самый просто способ избежать комбинаторного взрыва числа разборов — ограничить `repeatable`. Вместо `PENALTIES = PENALTY.repeatable()`, напишем `PENALTIES = PENALTY.repeatable(max=5)`. Такое правило отбросить 6-е и последующие взыскания, но завершится в ограниченное время."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## CappedParser"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Есть ещё один способ избежать комбинаторного взрыва числа разборов: выключать парсер, когда число состояний превысило порог. `CappedParser` наследует `Parser`, оборачивает внутренние методы `chart`, `predict`, `scan`, `complete` — шаги алгоритма Earley-парсера:"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"a a a \n",
"OK\n",
"a a a a \n",
"OK\n",
"a a a a a \n",
"OK\n",
"a a a a a a \n",
"TooManyStates\n",
"a a a a a a a \n",
"TooManyStates\n",
"a a a a a a a a \n",
"TooManyStates\n",
"a a a a a a a a a \n",
"TooManyStates\n"
]
}
],
"source": [
"class TooManyStates(Exception): pass\n",
"\n",
"\n",
"def capped(method):\n",
" def wrap(self, column, *args):\n",
" before = len(column.states)\n",
" method(self, column, *args)\n",
" after = len(column.states)\n",
"\n",
" self.states += (after - before)\n",
" if self.cap and self.states > self.cap:\n",
" raise TooManyStates\n",
"\n",
" return wrap\n",
"\n",
"\n",
"class CappedParser(Parser):\n",
" def reset(self):\n",
" self.states = 0\n",
"\n",
" def __init__(self, *args, cap=None, **kwargs):\n",
" self.cap = cap\n",
" self.reset()\n",
" Parser.__init__(self, *args, **kwargs)\n",
"\n",
" def chart(self, *args, **kwargs):\n",
" self.reset()\n",
" return Parser.chart(self, *args, **kwargs)\n",
"\n",
" predict = capped(Parser.predict)\n",
" scan = capped(Parser.scan)\n",
" complete = capped(Parser.complete)\n",
" \n",
"\n",
"parser = CappedParser(B, cap=100)\n",
"for size in range(3, 10):\n",
" text = 'a ' * size\n",
" print(text)\n",
" try:\n",
" parser.match(text)\n",
" except TooManyStates:\n",
" print('TooManyStates')\n",
" else:\n",
" print('OK')\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Порядок аргументов в `or_` имеет значение"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Когда разборов больше одного, парсер возвращает самый левый вариант:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n",
"\n",
"\n"
],
"text/plain": [
"Graph(nodes=[...], edges=[...])"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"A = or_(\n",
" rule('a'),\n",
" rule('a', 'a') \n",
")\n",
"B = A.repeatable()\n",
"\n",
"parser = Parser(B)\n",
"match = parser.match('a a a')\n",
"match.tree.as_dot"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Переставим местами `a a` и `a`, результат поменяется:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n",
"\n",
"\n"
],
"text/plain": [
"Graph(nodes=[...], edges=[...])"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"A = or_(\n",
" rule('a', 'a'),\n",
" rule('a')\n",
")\n",
"B = A.repeatable()\n",
"\n",
"parser = Parser(B)\n",
"match = parser.match('a a a')\n",
"match.tree.as_dot"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"На практике это важно. В примере со взыскиниями грамматика:\n",
"```\n",
"PENALTY -> MONEY TYPE | TYPE MONEY | MONEY | TYPE\n",
"```\n",
"\n",
"Левый разбор, не то, что ожидалось:\n",
"```\n",
"(5 тыс. р. штраф) (пени 3 тыс. р.) (необоснованного обогащения)`\n",
"```\n",
"\n",
"Переставим аргументы:\n",
"```\n",
"PENALTY -> MONEY TYPE | TYPE | TYPE MONEY | MONEY\n",
"```\n",
"\n",
"Получим:\n",
"```\n",
"(5 тыс. р. штраф) (пени) (3 тыс. р. необоснованного обогащения)`\n",
"```\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## IdTokenizer"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`Parser` принимает на вход текст. Первым делом парсер разделяет текст на токены. Токенизатор передаётся необязательным аргументом `tokenizer`: `Parser(RULE, tokenizer=Tokenizer())`. Токенизатор по-умолчанию — `yargy.tokenizer.MorphTokenizer`.\n",
"\n",
"Бывает нужно обработать уже токенизированный текст. Например, есть два парсера, нужно обоими обработать один текст. Хотим сэкономить время, не токенизировать текст дважды. Заведём парсер-обёртку, он ничего не делает, принимает и возращает токены:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"from yargy.tokenizer import (\n",
" Tokenizer,\n",
" MorphTokenizer,\n",
" EOL\n",
")\n",
"\n",
"\n",
"# Стандартный токенизатор. Удаляем правило для переводом строк.\n",
"# Обычно токены с '\\n' только мешаются.\n",
"TOKENIZER = MorphTokenizer().remove_types(EOL)\n",
"\n",
"\n",
"class IdTokenizer(Tokenizer):\n",
" def __init__(self, tokenizer):\n",
" self.tokenizer = tokenizer\n",
"\n",
" # Используется при инициализации morph_pipeline, caseless_pipeline.\n",
" # Строки-аргументы pipeline нужно разделить на слова. Как разделить,\n",
" # например, \"кейс| |dvd-диска\" или \"кейс| |dvd|-|диска\"? Используем стандартный токенизатор.\n",
" def split(self, text):\n",
" return self.tokenizer.split(text)\n",
"\n",
" # Используется при инициализации предикатов. Например, есть предикат type('INT').\n",
" # Поддерживает ли токенизатор тип INT?\n",
" def check_type(self, type):\n",
" return self.tokenizer.check_type(type)\n",
"\n",
" @property\n",
" def morph(self):\n",
" return self.tokenizer.morph\n",
"\n",
" def __call__(self, tokens):\n",
" return tokens\n",
"\n",
"\n",
"ID_TOKENIZER = IdTokenizer(TOKENIZER)\n",
"\n",
"tokens = TOKENIZER('a a a a')\n",
"parser = Parser(B, tokenizer=ID_TOKENIZER)\n",
"parser.match(tokens);"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## `ValueError: no .interpretation(...) for root rule`"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Есть два правила, хотим найти факты, где сработало одно из них:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"from yargy.interpretation import fact\n",
"\n",
"\n",
"F = fact('F', ['a'])\n",
"G = fact('G', ['b'])\n",
"\n",
"\n",
"A = rule('a').interpretation(F.a).interpretation(F)\n",
"B = rule('b').interpretation(G.b).interpretation(G)\n",
"C = or_(A, B)\n",
"parser = Parser(C)\n",
"\n",
"match = parser.match('a')\n",
"# match.fact ValueError"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Ожидаем `F(a='a')`, получаем `ValueError: no .interpretation(...) for root rule`. На вершине-корне нет пометки контруктора факта:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n",
"\n",
"\n"
],
"text/plain": [
"Graph(nodes=[...], edges=[...])"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"C.as_dot"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Создадим прокси-факт:"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n",
"\n",
"\n"
],
"text/plain": [
"Graph(nodes=[...], edges=[...])"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/plain": [
"F(\n",
" a='a'\n",
")"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"Proxy = fact('Proxy', ['value'])\n",
"\n",
"C = or_(A, B).interpretation(Proxy.value).interpretation(Proxy)\n",
"display(C.as_dot)\n",
"\n",
"parser = Parser(C)\n",
"match = parser.match('a')\n",
"match.fact.value\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## `TypeError: mixed types`"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Набор аргументов `or_` бывает двух видов:\n",
"1. Все предикаты, тогда результат — предикат\n",
"2. Все `rule`, тогда результат — `rule`\n",
"\n",
"Иногда правило состоит из одного предиката, передаём его в `or_`, получаем ошибку:"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [],
"source": [
"from yargy.predicates import caseless\n",
"\n",
"A = rule('a')\n",
"B = caseless('b')\n",
"# or_(A, B) # TypeError: mixed types: [, ]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Явно завернём предикат в `rule`:"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [],
"source": [
"B = rule(caseless('b'))\n",
"C = or_(A, B)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Машинное обучение и Yargy"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Есть текст размеченный BIO-тегами:"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [],
"source": [
"text = '15 апреля в Симферополе Леонид Рожков ...'\n",
"tags = 'B I O B B I O'.split()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`Parser` принимает необязательный аргумент `tagger`: `Parser(RULE, tagger=Tagger)`. `Tagger` принимает и возвращает список токенов. Добавим внешнюю разметку `tags` в токены. Используем предикат `tag`, извлечём сущности:"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"['15', 'апреля']\n",
"['Симферополе']\n",
"['Леонид', 'Рожков']\n"
]
}
],
"source": [
"from yargy.tagger import Tagger\n",
"from yargy.predicates import tag\n",
"\n",
"\n",
"class Tagger(Tagger):\n",
" # Все возможные теги. Используется при инициализации предиката tag.\n",
" # Если пользователь создаст tag('FOO'), будет ошибка\n",
" tags = {'B', 'I', 'O'}\n",
"\n",
" def __call__(self, tokens):\n",
" for token, tag in zip(tokens, tags):\n",
" yield token.tagged(tag)\n",
"\n",
"\n",
"RULE = rule(\n",
" tag('B'),\n",
" tag('I').repeatable().optional()\n",
")\n",
"parser = Parser(RULE, tagger=Tagger())\n",
"\n",
"matches = parser.findall(text)\n",
"for match in matches:\n",
" print([_.value for _ in match.tokens])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Пропустить часть текста"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Есть текст \"взыскать 5 тыс. р. штрафа, а также пени и неустойку\". Нужно выделить 3 взыскания \"5 тыс. р. штраф\", \"пени\", \"неустойка\", пропустить подстроки \", а также\", \"и\". Запустить парсер 2 раза: сначала выделим взыскания, удалим лишние токены, запустим парсер ещё раз: "
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[9, 25), [35, 39), [42, 51)]\n",
"['5', 'тыс', '.', 'р', '.', 'штрафа', 'пени', 'неустойку']\n"
]
}
],
"source": [
"from yargy.pipelines import morph_pipeline\n",
"\n",
"text = 'взыскать 5 тыс. р. штрафа, а также пени и неустойку'\n",
"tokens = list(TOKENIZER(text))\n",
"\n",
"\n",
"PAYMENT = morph_pipeline([\n",
" '5 тыс. р. штраф',\n",
" 'пени',\n",
" 'неустойка'\n",
"])\n",
"parser = Parser(PAYMENT, tokenizer=ID_TOKENIZER)\n",
"\n",
"matches = parser.findall(tokens)\n",
"spans = [_.span for _ in matches]\n",
"print(spans)\n",
"\n",
"\n",
"def is_inside_span(token, span):\n",
" token_span = token.span\n",
" return span.start <= token_span.start and token_span.stop <= span.stop\n",
"\n",
"\n",
"def select_span_tokens(tokens, spans):\n",
" for token in tokens:\n",
" if any(is_inside_span(token, _) for _ in spans):\n",
" yield token\n",
"\n",
"\n",
"tokens = list(select_span_tokens(tokens, spans))\n",
"print([_.value for _ in tokens])\n",
"\n",
"PAYMENTS = PAYMENT.repeatable()\n",
"parser = Parser(PAYMENTS, tokenizer=ID_TOKENIZER)\n",
"match = parser.match(tokens)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Генерация правил"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"В Yargy все правила описываются на языке Python. Создадим функцию, которая генерирует правило. Например, правило для текста в скобочка и кавычках:"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"['[', 'a', 'b', ']']\n",
"['«', 'e', 'f', '»']\n"
]
}
],
"source": [
"from yargy import not_\n",
"from yargy.predicates import eq\n",
"\n",
"\n",
"def bounded(start, stop):\n",
" return rule(\n",
" eq(start),\n",
" not_(eq(stop)).repeatable(),\n",
" eq(stop)\n",
" )\n",
"\n",
"\n",
"BOUNDED = or_(\n",
" bounded('[', ']'),\n",
" bounded('«', '»')\n",
")\n",
"parser = Parser(BOUNDED)\n",
"matches = parser.findall('[a b] {c d} «e f»')\n",
"for match in matches:\n",
" print([_.value for _ in match.tokens])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Правило — аналог `join` в Python:"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [],
"source": [
"from yargy.predicates import in_\n",
"\n",
"\n",
"def joined(ITEM, SEP):\n",
" return rule(\n",
" ITEM,\n",
" rule(\n",
" SEP,\n",
" ITEM\n",
" ).repeatable().optional()\n",
" )\n",
"\n",
"\n",
"SEP = in_(',;')\n",
"JOINED = joined(BOUNDED, SEP)\n",
"parser = Parser(JOINED)\n",
"match = parser.match('[a b], [c d], [e f g]')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Правило для BIO-разметки:"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"['Симферополе']\n",
"['Леонид', 'Рожков']\n"
]
}
],
"source": [
"def bio(type):\n",
" return rule(\n",
" tag('B-%s' % type),\n",
" tag('I-%s' % type).repeatable().optional()\n",
" )\n",
"\n",
"\n",
"PER = bio('PER')\n",
"LOC = bio('LOC')\n",
"\n",
"\n",
"text = '15 апреля в Симферополе Леонид Рожков ...'\n",
"tags = 'B-DATE I-DATE O B-LOC B-PER I-PER O'.split()\n",
"\n",
"\n",
"class Tagger(Tagger):\n",
" tags = {'B-PER', 'I-PER', 'B-LOC', 'I-LOC', 'O'}\n",
"\n",
" def __call__(self, tokens):\n",
" for token, tag in zip(tokens, tags):\n",
" yield token.tagged(tag)\n",
"\n",
"\n",
"RULE = or_(PER, LOC)\n",
"parser = Parser(RULE, tagger=Tagger())\n",
"matches = parser.findall(text)\n",
"for match in matches:\n",
" print([_.value for _ in match.tokens])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Генерация `pipeline`"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Создадим `pipeline` из словаря пар \"полное название\", \"сокращение\":"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"['Акционерное', 'общество']\n",
"['Акционерном', 'обществе']\n",
"['АО']\n",
"['СК']\n"
]
}
],
"source": [
"from yargy.pipelines import (\n",
" morph_pipeline,\n",
" pipeline\n",
")\n",
"from yargy import interpretation as interp\n",
"\n",
"\n",
"TYPES = [\n",
" ('Общество с ограниченной ответственностью', 'ООО'),\n",
" ('Акционерное общество', 'АО'),\n",
" ('Страховая компания', 'СК'),\n",
" ('Строительная компания', 'СК')\n",
"]\n",
"\n",
"TYPE = or_(\n",
" morph_pipeline([\n",
" name for name, abbr in TYPES\n",
" ]),\n",
" pipeline([\n",
" abbr for name, abbr in TYPES\n",
" ])\n",
")\n",
"\n",
"RULE = TYPE.repeatable()\n",
"parser = Parser(RULE)\n",
"matches = parser.findall('Акционерное общество, в Акционерном обществе; АО, СК')\n",
"for match in matches:\n",
" print([_.value for _ in match.tokens])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Наследование `fact`"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`fact` создаёт Python-класс, отнаследуемся, добавим методы и атрибуты. Например, есть ссылка на статьи \"ст. 15-17 п.1\", результат список объектов `Ref(art=15, punkt=1), Ref(art=16, punkt=1), ...`:"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"ст. 15-17 п.1\n",
"[Ref(art=15, punkt=1), Ref(art=16, punkt=1), Ref(art=17, punkt=1)]\n",
"ст. 15 п.2\n",
"[Ref(art=15, punkt=2)]\n",
"ст. 16\n",
"[Ref(art=16, punkt=None)]\n"
]
}
],
"source": [
"from collections import namedtuple\n",
"\n",
"from yargy.predicates import type\n",
"\n",
"\n",
"Ref_ = namedtuple(\n",
" 'Ref',\n",
" ['art', 'punkt']\n",
")\n",
"\n",
"\n",
"Art = fact(\n",
" 'Art',\n",
" ['start', 'stop']\n",
")\n",
"class Art(Art):\n",
" def range(self):\n",
" if self.stop:\n",
" return range(self.start, self.stop + 1)\n",
" else:\n",
" return [self.start]\n",
"\n",
"\n",
"Punkt = fact(\n",
" 'Punkt',\n",
" ['number']\n",
")\n",
"\n",
"\n",
"Ref = fact(\n",
" 'Ref',\n",
" ['art', 'punkt']\n",
")\n",
"class Ref(Ref):\n",
" def range(self):\n",
" for art in self.art.range():\n",
" punkt = (\n",
" self.punkt.number\n",
" if self.punkt\n",
" else None\n",
" )\n",
" yield Ref_(art, punkt)\n",
" \n",
" \n",
"INT = type('INT')\n",
"\n",
"ART = rule(\n",
" 'ст', '.',\n",
" INT.interpretation(Art.start.custom(int)),\n",
" rule(\n",
" '-',\n",
" INT.interpretation(Art.stop.custom(int))\n",
" ).optional()\n",
").interpretation(Art)\n",
"\n",
"PUNKT = rule(\n",
" 'п', '.',\n",
" INT.interpretation(Punkt.number.custom(int))\n",
").interpretation(Punkt)\n",
"\n",
"REF = rule(\n",
" ART.interpretation(Ref.art),\n",
" PUNKT.optional().interpretation(Ref.punkt)\n",
").interpretation(Ref)\n",
"\n",
"parser = Parser(REF)\n",
"lines = [\n",
" 'ст. 15-17 п.1',\n",
" 'ст. 15 п.2',\n",
" 'ст. 16'\n",
"]\n",
"for line in lines:\n",
" print(line)\n",
" match = parser.match(line)\n",
" print(list(match.fact.range()))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Есть периоды \"1917-1918г.\", \"21 век\", приведём их к единому формату: `Period(1917, 1919)`, `Period(2000, 2100)`."
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"1917-1918г.\n",
"Period(start=1917, stop=1919)\n",
"21 век\n",
"Period(start=2000, stop=2100)\n",
"1990г.\n",
"Period(start=1990, stop=1991)\n"
]
}
],
"source": [
"Period_ = namedtuple('Period', ['start', 'stop'])\n",
"\n",
"\n",
"Year = fact(\n",
" 'Year',\n",
" ['value']\n",
")\n",
"class Year(Year):\n",
" @property\n",
" def normalized(self):\n",
" return Period_(self.value, self.value + 1)\n",
"\n",
"\n",
"YearRange = fact(\n",
" 'YearRange',\n",
" ['start', 'stop']\n",
")\n",
"class YearRange(YearRange):\n",
" @property\n",
" def normalized(self):\n",
" return Period_(self.start, self.stop + 1)\n",
" \n",
" \n",
"Century = fact(\n",
" 'Century',\n",
" ['value']\n",
")\n",
"class Century(Century):\n",
" @property\n",
" def normalized(self):\n",
" start = (self.value - 1) * 100\n",
" return Period_(start, start + 100)\n",
"\n",
"\n",
"Period = fact(\n",
" 'Period',\n",
" ['value']\n",
")\n",
"class Period(Period):\n",
" @property\n",
" def normalized(self):\n",
" return self.value.normalized\n",
"\n",
"\n",
"YEAR = rule(\n",
" INT.interpretation(Year.value.custom(int)),\n",
" 'г', '.'\n",
").interpretation(Year)\n",
"\n",
"YEAR_RANGE = rule(\n",
" INT.interpretation(YearRange.start.custom(int)),\n",
" '-',\n",
" INT.interpretation(YearRange.stop.custom(int)),\n",
" 'г', '.'\n",
").interpretation(YearRange)\n",
"\n",
"CENTURY = rule(\n",
" INT.interpretation(Century.value.custom(int)),\n",
" 'век'\n",
").interpretation(Century)\n",
"\n",
"PERIOD = or_(\n",
" YEAR,\n",
" YEAR_RANGE,\n",
" CENTURY\n",
").interpretation(Period.value).interpretation(Period)\n",
"\n",
"parser = Parser(PERIOD)\n",
"lines = [\n",
" '1917-1918г.',\n",
" '21 век',\n",
" '1990г.'\n",
"]\n",
"for line in lines:\n",
" match = parser.match(line)\n",
" print(line)\n",
" print(match.fact.normalized)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 2
}