{ "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", "\n", "G\n", "\n", "\n", "\n", "0\n", "\n", "R0\n", "\n", "\n", "\n", "1\n", "\n", "R1\n", "\n", "\n", "\n", "0->1\n", "\n", "\n", "\n", "\n", "\n", "3\n", "\n", "R0\n", "\n", "\n", "\n", "0->3\n", "\n", "\n", "\n", "\n", "\n", "2\n", "\n", "a\n", "\n", "\n", "\n", "1->2\n", "\n", "\n", "\n", "\n", "\n", "4\n", "\n", "R1\n", "\n", "\n", "\n", "3->4\n", "\n", "\n", "\n", "\n", "\n", "5\n", "\n", "a\n", "\n", "\n", "\n", "4->5\n", "\n", "\n", "\n", "\n", "\n", "6\n", "\n", "a\n", "\n", "\n", "\n", "4->6\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "Graph(nodes=[...], edges=[...])" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "G\n", "\n", "\n", "\n", "0\n", "\n", "R0\n", "\n", "\n", "\n", "1\n", "\n", "R1\n", "\n", "\n", "\n", "0->1\n", "\n", "\n", "\n", "\n", "\n", "4\n", "\n", "R0\n", "\n", "\n", "\n", "0->4\n", "\n", "\n", "\n", "\n", "\n", "2\n", "\n", "a\n", "\n", "\n", "\n", "1->2\n", "\n", "\n", "\n", "\n", "\n", "3\n", "\n", "a\n", "\n", "\n", "\n", "1->3\n", "\n", "\n", "\n", "\n", "\n", "5\n", "\n", "R1\n", "\n", "\n", "\n", "4->5\n", "\n", "\n", "\n", "\n", "\n", "6\n", "\n", "a\n", "\n", "\n", "\n", "5->6\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "Graph(nodes=[...], edges=[...])" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "G\n", "\n", "\n", "\n", "0\n", "\n", "R0\n", "\n", "\n", "\n", "1\n", "\n", "R1\n", "\n", "\n", "\n", "0->1\n", "\n", "\n", "\n", "\n", "\n", "3\n", "\n", "R0\n", "\n", "\n", "\n", "0->3\n", "\n", "\n", "\n", "\n", "\n", "2\n", "\n", "a\n", "\n", "\n", "\n", "1->2\n", "\n", "\n", "\n", "\n", "\n", "4\n", "\n", "R1\n", "\n", "\n", "\n", "3->4\n", "\n", "\n", "\n", "\n", "\n", "6\n", "\n", "R0\n", "\n", "\n", "\n", "3->6\n", "\n", "\n", "\n", "\n", "\n", "5\n", "\n", "a\n", "\n", "\n", "\n", "4->5\n", "\n", "\n", "\n", "\n", "\n", "7\n", "\n", "R1\n", "\n", "\n", "\n", "6->7\n", "\n", "\n", "\n", "\n", "\n", "8\n", "\n", "a\n", "\n", "\n", "\n", "7->8\n", "\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", "\n", "G\n", "\n", "\n", "\n", "0\n", "\n", "R0\n", "\n", "\n", "\n", "1\n", "\n", "R1\n", "\n", "\n", "\n", "0->1\n", "\n", "\n", "\n", "\n", "\n", "3\n", "\n", "R0\n", "\n", "\n", "\n", "0->3\n", "\n", "\n", "\n", "\n", "\n", "2\n", "\n", "a\n", "\n", "\n", "\n", "1->2\n", "\n", "\n", "\n", "\n", "\n", "4\n", "\n", "R1\n", "\n", "\n", "\n", "3->4\n", "\n", "\n", "\n", "\n", "\n", "6\n", "\n", "R0\n", "\n", "\n", "\n", "3->6\n", "\n", "\n", "\n", "\n", "\n", "5\n", "\n", "a\n", "\n", "\n", "\n", "4->5\n", "\n", "\n", "\n", "\n", "\n", "7\n", "\n", "R1\n", "\n", "\n", "\n", "6->7\n", "\n", "\n", "\n", "\n", "\n", "8\n", "\n", "a\n", "\n", "\n", "\n", "7->8\n", "\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", "\n", "G\n", "\n", "\n", "\n", "0\n", "\n", "R0\n", "\n", "\n", "\n", "1\n", "\n", "R1\n", "\n", "\n", "\n", "0->1\n", "\n", "\n", "\n", "\n", "\n", "4\n", "\n", "R0\n", "\n", "\n", "\n", "0->4\n", "\n", "\n", "\n", "\n", "\n", "2\n", "\n", "a\n", "\n", "\n", "\n", "1->2\n", "\n", "\n", "\n", "\n", "\n", "3\n", "\n", "a\n", "\n", "\n", "\n", "1->3\n", "\n", "\n", "\n", "\n", "\n", "5\n", "\n", "R1\n", "\n", "\n", "\n", "4->5\n", "\n", "\n", "\n", "\n", "\n", "6\n", "\n", "a\n", "\n", "\n", "\n", "5->6\n", "\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", "\n", "G\n", "\n", "\n", "\n", "0\n", "\n", "Or\n", "\n", "\n", "\n", "1\n", "\n", "F\n", "\n", "\n", "\n", "0->1\n", "\n", "\n", "\n", "\n", "\n", "2\n", "\n", "G\n", "\n", "\n", "\n", "0->2\n", "\n", "\n", "\n", "\n", "\n", "3\n", "\n", "F.a\n", "\n", "\n", "\n", "1->3\n", "\n", "\n", "\n", "\n", "\n", "4\n", "\n", "G.b\n", "\n", "\n", "\n", "2->4\n", "\n", "\n", "\n", "\n", "\n", "5\n", "\n", "Rule\n", "\n", "\n", "\n", "3->5\n", "\n", "\n", "\n", "\n", "\n", "6\n", "\n", "Rule\n", "\n", "\n", "\n", "4->6\n", "\n", "\n", "\n", "\n", "\n", "7\n", "\n", "Production\n", "\n", "\n", "\n", "5->7\n", "\n", "\n", "\n", "\n", "\n", "8\n", "\n", "Production\n", "\n", "\n", "\n", "6->8\n", "\n", "\n", "\n", "\n", "\n", "9\n", "\n", "'a'\n", "\n", "\n", "\n", "7->9\n", "\n", "\n", "\n", "\n", "\n", "10\n", "\n", "'b'\n", "\n", "\n", "\n", "8->10\n", "\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", "\n", "G\n", "\n", "\n", "\n", "0\n", "\n", "Proxy\n", "\n", "\n", "\n", "1\n", "\n", "Proxy.value\n", "\n", "\n", "\n", "0->1\n", "\n", "\n", "\n", "\n", "\n", "2\n", "\n", "Or\n", "\n", "\n", "\n", "1->2\n", "\n", "\n", "\n", "\n", "\n", "3\n", "\n", "F\n", "\n", "\n", "\n", "2->3\n", "\n", "\n", "\n", "\n", "\n", "4\n", "\n", "G\n", "\n", "\n", "\n", "2->4\n", "\n", "\n", "\n", "\n", "\n", "5\n", "\n", "F.a\n", "\n", "\n", "\n", "3->5\n", "\n", "\n", "\n", "\n", "\n", "6\n", "\n", "G.b\n", "\n", "\n", "\n", "4->6\n", "\n", "\n", "\n", "\n", "\n", "7\n", "\n", "Rule\n", "\n", "\n", "\n", "5->7\n", "\n", "\n", "\n", "\n", "\n", "8\n", "\n", "Rule\n", "\n", "\n", "\n", "6->8\n", "\n", "\n", "\n", "\n", "\n", "9\n", "\n", "Production\n", "\n", "\n", "\n", "7->9\n", "\n", "\n", "\n", "\n", "\n", "10\n", "\n", "Production\n", "\n", "\n", "\n", "8->10\n", "\n", "\n", "\n", "\n", "\n", "11\n", "\n", "'a'\n", "\n", "\n", "\n", "9->11\n", "\n", "\n", "\n", "\n", "\n", "12\n", "\n", "'b'\n", "\n", "\n", "\n", "10->12\n", "\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 }