Рубрики
Без рубрики

Адвое Код 2020: День 04 Использование грамматиков PEG в Python

Продолжая свое преследование за необычные решения для появления кода, для этого я рассматриваю данные, как если бы это … Помечено с AdhentOfCode, Python.

Продолжая свое преследование за необычные решения к появлению кода, для этого я рассматриваю данные, как будто это был язык программирования, и написать целый парсер на основе PEG, чтобы «выполнить его».

Все упомянутые в этом посте: Разборная экспрессия грамматики (PEG), разборка, расщепление струн, больше регеляций

Ссылка на вызов на Advent of Code 2020 сайта 2020

Представленные являются еще одним вызовом анализа/проверки. Мы давали некоторые «паспортные» данные, которые выглядят так:

ecl:gry pid:860033327 eyr:2020 hcl:#fffffd
byr:1937 iyr:2017 cid:147 hgt:183cm

Которые являются парами клавишных значений, разделенные пустыми линиями, и задача для проверки, чтобы убедиться, что каждый паспорт имеет правильный набор значений в них.

Правила, как указано, просто что все поля, кроме CID должен быть присутствует

В отличие от ранее, данные входят в набор записей, которые каждый промежуток более одной линии, разделенный пустыми линиями. Так что мы не можем использовать Readleines () Как в день 2 решения. Вместо этого мы можем прочитать все данные одновременно, а затем разделеть двойную новую линию:

data = open("input.txt").read().split("\n\n")

Вывод

['eyr:2029 byr:1931 hcl:z cid:128\necl:amb hgt:150cm iyr:2015 pid:148714704',
 'byr:2013 hgt:70cm pid:76982670 ecl:#4f9a1c\nhcl:9e724b eyr:1981 iyr:2027',
 'pid:261384974 iyr:2015\nhgt:172cm eyr:2020\nbyr:2001 hcl:#59c2d9 ecl:amb cid:163',
...

Теперь у нас есть массив, который содержит одну запись паспорта для проверки за индекс. На протяжении всего этого поста, если вы увидите меня использовать Вход Это просто одна из этих записей, которые я вытащил для тестирования (например, вход [0] )

Разбор с использованием разделения ()

Мы действительно просто имеем дело с кучей пространства (или новой строки) сюда, поэтому мы можем просто идти вперед и Сплит () их. Сплит () Автоматически расщепляется строки пробелом, поэтому обрабатывает новинки, а также:

entry.split()

Вывод:

['eyr:2024',
 'hcl:#b6652a',
 'cid:340',
 'byr:1929',
 'ecl:oth',
 'iyr:2014',
 'pid:186640193',
 'hgt:193in']

Теперь мы можем сломать каждый из них в пары ключа-значений, расщепляясь на : А также используя понимание списка, чтобы применить его ко всем элементам:

[item.split(":") for item in entry.split()]

Вывод

[['eyr', '2024'],
 ['hcl', '#b6652a'],
 ['cid', '340'],
 ['byr', '1929'],
 ['ecl', 'oth'],
 ['iyr', '2014'],
 ['pid', '186640193'],
 ['hgt', '193in']]

Питона Dict () Функция удобно принимает список списков, как это, и превращает его в словарь для нас:

parsed = dict(item.split(":") for item in entry.split())

Вывод

{'eyr': '2024',
 'hcl': '#b6652a',
 'cid': '340',
 'byr': '1929',
 'ecl': 'oth',
 'iyr': '2014',
 'pid': '186640193',
 'hgt': '193in'}

Полезная вещь, имеющая словарь, подобный это, мы можем легко получить только ключи в этом словаре:

parsed.keys()

Вывод

dict_keys(['eyr', 'hcl', 'cid', 'byr', 'ecl', 'iyr', 'pid', 'hgt'])

Проверка

Теперь, когда у нас есть способ анализировать записи, наше правило проверки для этой проблемы – это просто все поля, кроме CID должен быть присутствует.

Напомним решение о теории набора из дня 01. В любое время мы хотим обнаружить, если один список имеет некоторое отношение к другому списку (игнорирующий порядок списка), мы можем использовать теорию набора и мощные установленные функции Python.

Таким образом, мы можем создать наш набор обязательных полей:

required = {'eyr', 'hcl', 'byr', 'ecl', 'iyr', 'pid', 'hgt'}

И тогда мы можем использовать набор Разница () Чтобы выяснить, какие поля в нашей паспортной записи отсутствуют:

missing = required.difference(parsed.keys())

Если это приводит к пустому набору, это означает, что у нас есть все клавиши, которые нам нужны для этой записи паспорта, и поэтому она действительна. Если этот набор имеет что-нибудь в этом, это означает, что он недействителен.

Таким образом, мы можем настроить простое понимание списка для обработки этих различий для каждой записи и считать их. Так что весь код выглядит так:

data = open("input.txt").read().split("\n\n")
required = {'eyr', 'hcl', 'byr', 'ecl', 'iyr', 'pid', 'hgt'}

valids = 0
for entry in data:
    items = dict(item.split(":") for item in entry.split())
    valid = not required.difference(items.keys())
    if valid:
        valids += 1

print("Total valid", valids)

Но, конечно, это может быть в гольф несколько до одной строки кода, если вы хотите быть дико нечитаемым. Это не полезно сделать это, но это круто, что Python может достичь этой компактной линии кода.

print("Total valid", sum(not {'eyr', 'hcl', 'byr', 'ecl', 'iyr', 'pid', 'hgt'}.difference([item.split(":")[0] for item in entry.split()]) for entry in open("input.txt").read().split("\n\n")))

Часть 2 проблемы расширяет это, чтобы добавить совершенно новый набор правил проверки для каждой из частей. Теперь это где начинается веселье

Поскольку у нас есть анализатор, который способен собирать каждый ключ: Value Pair, мы можем просто написать новые правила, чтобы убедиться, что каждое значение имеет правильную функцию. Но где бы повеселиться в этом? Давайте возьмем совершенно другое направление.

Мы смотрели на использование State Machines и Regex для разборки обязанностей в день 02, где мы использовали штатные машины или регулярное выражение для потребления конкретных моделей данных. Это весело и все, но то, что мы здесь делаем, немного отличается – чтение различных токенов из входных данных, и проверка ли эти токены действительны или не основаны на более широком наборе правил «синтаксиса». Звучит много как программирование правильно?

Так почему бы нам не подходить к этому, как будто эти паспортные поля были языком программирования. Вместо расщепления струн и обработки каждой части строки, как мы обработаем данные, что, если мы относимся к тем полям, таким как ключевые слова и значения в языке программирования.

Угадайте, что такое анализ языков программирования – «решенная проблема». Введите PEG, разборную грамматику экспрессии, которые используются для описания языков программирования (и анализа)! Я буду использовать библиотеку Python Parsimonious , очень легкий и быстрый PEG-анализатор. Я использовал Parsimonious Перед тем, как сделать крошечный язык сценариев для игрового диалогового двигателя и наслаждаться его использованием.

Привязывать

Если вам нравится Regex, вы будете любить PEG. PEG использует сопоставление шаблонов, но в дополнение к этому, он может сделать рекурсивное сопоставление вложенного рисунка. Это немного похоже на регулярное регельс внутри Regex внутри Regex.

Давайте посмотрим на этот формат/правило проверки:

HGT (высота) – число, за которым следует см или в:

  • Если CM, число должно быть не менее 150 и не более 193.
  • Если в, число должно быть не менее 59 и не более 76.

Поэтому мы ожидаем, что ценность будет выглядеть что-то подобное: HGT: 190см или HGT: 70IN Отказ Как это выглядит в PEG?

from parsimonious.grammar import Grammar
grammar = Grammar(r"""
    HGT   = "hgt:" (HGTCM / HGTIN)
    HGTCM = NUMB "cm"
    HGTIN = NUMB "in"
    NUMB  = ~r"[0-9]{2,4}"
""")

Вот. Что мы здесь сделали, определяют HGT узел как персонажи HGT: сопровождаемый либо HGTCM Узел или HGTIN узел. Тогда мы определили HGTCM Узел как Онемение затем «см» и мы определили Онемение С помощью REGEX говорят, что он должен быть от 2 до 4 цифр (причина, по которой это 2-4 цифры состоит в том, чтобы иметь возможность повторно использовать онемение в течение многих лет спустя)

В реальном использовании мы получаем что-то подобное:

print(grammar.parse("hgt:123cm"))

Вывод


    
    
        
            
            

Тадаа, мы анализировали значение высоты, как будто это был язык программирования. Сгенерированный результат представляет собой синтаксическое дерево, которое содержит все компоненты, которые мы вытащили, включая важные значения. Тем не менее, это не вся история, в то время как Regex и PEG позволяет нам делать такие вещи, как обеспечить все правильное количество символов, то есть правильный синтаксис, то, что он не может сделать, это убедитесь, что значение между определенным диапазон. Нам придется делегировать, что для некоторого более позднего кода Python.

Если мы дали ему плохое значение, которое не соответствовало нашей грамматике (это не хватает в или см часть

print(grammar.parse("hgt:123"))

Вывод

ParseError: Rule  didn't match at '' (line 1, column 8).

Хорошо, вот все правила синтаксиса для различных клавиш и значений (Это не готовый PEG):

grammar = Grammar(r"""
    BYR   = "byr:" NUMB
    IYR   = "iyr:" NUMB
    EYR   = "eyr:" NUMB
    HGT   = "hgt:" (HGTCM / HGTIN)
    HCL   = "hcl:" ~r"#[0-9a-f]{6}"
    ECL   = "ecl:" ("amb" / "blu" / "brn" / "gry" / "grn" / "hzl" / "oth") 
    PID   = "pid:" ~r"[0-9]{9}"
    CID   = "cid:" ~r"[0-9a-zA-Z]*"

    HGTCM = NUMB "cm"
    HGTIN = NUMB "in"

    NUMB  = ~r"[0-9]{2,4}"
""")

Как видно, любой год определяется как 4 цифры, любая проверка их значений будет выполнена позже; в то время как ECL имеет список из 7 возможных значений. У нас на самом деле здесь есть два варианта: либо имейте 7 возможных значений как часть PEG, и в этом случае ошибка разбора будет подниматься при неправильном значении; Или у них есть в качестве части проверки позже, в этом случае PEG будет проанализировать штраф, и код будет поднять ошибку позже. В реальном использовании, которого можно идти, зависит от того, где вы хотите, чтобы ошибка произошла.

Тем не менее, PEG не закончен. ПЭГ должен соответствовать всей строке. До сих пор мы определили только разные ключи: значения. Но мы ожидаем, что несколько клавиш: значения отображаются в записи, поэтому нам нужен общий узел выражения.

grammar = Grammar(r"""
    EXPR  = ITEM+
    ITEM  = (BYR / IYR / EYR / HGT / HCL / ECL / PID / CID) WHITE?
    WHITE =  ~r"[\s]+"

    BYR   = "byr:" NUMB
    IYR   = "iyr:" NUMB
    EYR   = "eyr:" NUMB
    HGT   = "hgt:" (HGTCM / HGTIN)
    HCL   = "hcl:" ~r"#[0-9a-f]{6}"
    ECL   = "ecl:" ("amb" / "blu" / "brn" / "gry" / "grn" / "hzl" / "oth") 
    PID   = "pid:" ~r"[0-9]{9}"
    CID   = "cid:" ~r"[0-9a-zA-Z]*"

    HGTCM = NUMB "cm"
    HGTIN = NUMB "in"

    NUMB  = ~r"[0-9]{2,4}"
""")

Extra Parent Expr добавляется в верхних состояниях, что эта анализируемая запись должна состоять как минимум на один из предметов; И каждый элемент является одним из семи записей, за которыми следуют дополнительное пробелование. Вот и все! Теперь у нас есть привязка для анализа наших паспортов. На этом этапе мы уже можем выбросить любые записи паспорта, которые имеют недействительный синтаксис, но нам нужно еще несколько шагов для проверки фактических данных, таких как номера года, поэтому нам нужно будет обратиться к пересечению генерируемого синтаксического дерева.

Успешное синтаксическое дерево выглядит так:


    
        
            
                
                
        
            
    
        
            
                
                
                    
                        
                        
        
            
    
        
            
                
                
        
            
    
        
            
                
                
                    
        
            
    
        
            
                
                
        
            
    
        
            
                
                
        
            
    
        
            
                
                
        

Там много узлов, но встроенные в это формальная структура нашего «языка программирования», которые являются паспортными записями, и мы можем просто цикровать эти узлы, чтобы найти значения, которые нам нужно проверить, и мы гарантированы, что Эти узлы появляются в порядке, который мы определили в нашем PEG.

Проверка

Чтобы подтвердить наше недавно созданное синтаксическое дерево, нам нужно рекурсивно путешествовать по дереву, чтобы посетить каждый узел, а затем сделать что-то со своим значением. К счастью, Parsimonious уже поставляется с деревом Traversal Classic называется Nodevisitor Что выходит на глубину – сначала, дайвинг глубоко в узле, а затем оценивает каждый узел с помощью функции, которую вы предоставляете.

В качестве примера вот посетитель узла, который проверяет только, что рождение года составляет от 1920 до 2002 года:

class PassportVisitor(NodeVisitor):
    def visit_BYR(self, node, visited_children):
        assert 1920 <= visited_children[1] <= 2002

    def visit_YEAR(self, node, visited_children):
        return int(node.text)

    def generic_visit(self, node, visited_children):
        return visited_children or node

Несколько вещей происходит здесь. generic_visit () Метод – это требование, так как это функция, которая работает, когда она не найдет метод соответствия, специфичным для узла. Я дал это visit_year () Метод, который автоматически будет вызван в год узлы, которые мы определили ранее. Работа этого метода состоит в том, чтобы преобразовать 4-значный год из строки в целое число, чтобы вышестоящие узлы не должны делать это преобразование. Мы абсолютно уверены, что строка будет содержать 4 цифры, потому что если она не сделала, PEG не сможет разобрать. Вот как посетитель узла работает вместе с привязностью, чтобы убедиться, что как формат, так и значения являются правильными.

Дальше, visit_byr () Теперь обрабатывает полный BYR: ключ и значение. На данный момент дети узлы уже были обработаны, включая Год Узел, который вернул номер. Поэтому мы можем получить доступ к результату Год Узел внутри посетил_children [1] С BYR Блок имеет два детей (внутри ребенка 0 – это буквальный «BYR:», как определено в PEG.

С момента единственной работы нашего узла состоит в том, чтобы проверять ошибки, нам даже не нужно возвращать какие-либо значения из этого узла BYR, так как нам не нужно его использовать. Нам нужно только поднять исключения, когда валидация не удалась. Мы можем использовать Python’s утверждать Ключевое слово для этого (но повысить исключение также был бы хорошо).

Теперь полный посетитель узла со всеми другими правилами, реализованными:

class PassportVisitor(NodeVisitor):
    def visit_BYR(self, node, visited_children):
        assert 1920 <= visited_children[1] <= 2002

    def visit_IYR(self, node, visited_children):
        assert 2010 <= visited_children[1] <= 2020

    def visit_EYR(self, node, visited_children):
        assert 2020 <= visited_children[1] <= 2030

    def visit_HGTCM(self, node, visited_children):
        assert 150 <= visited_children[0] <= 193

    def visit_HGTIN(self, node, visited_children):
        assert 59 <= visited_children[0] <= 76

    def visit_NUMB(self, node, visited_children):
        return int(node.text)

    def generic_visit(self, node, visited_children):
        return visited_children or node

Мы почти там! Этот посетитель узла применяет оставшиеся численные проверки, которые не покрываются PEG. Однако единственное, что это не делает, это сказать нам, видели ли мы все необходимые ключи, поскольку он проверяет только каждый ключ индивидуально. Мы можем добавить, что, используя то же самое Установить Понимание, сделанное ранее, нам просто нужно, чтобы посетитель Узел пункт возвращается, что такое его вложенный ключ, чтобы мы могли иметь посетитель Expr Node, проверяя все наши ключевые ключи для нас.

Последний код выглядит так:

from parsimonious.grammar import Grammar, NodeVisitor
from parsimonious.exceptions import ParseError, IncompleteParseError, VisitationError

grammar = Grammar(r"""
    EXPR  = ITEM+
    ITEM  = (BYR / IYR / EYR / HGT / HCL / ECL / PID / CID) WHITE?
    WHITE =  ~r"[\s]+"

    BYR   = "byr:" NUMB
    IYR   = "iyr:" NUMB
    EYR   = "eyr:" NUMB
    HGT   = "hgt:" (HGTCM / HGTIN)
    HCL   = "hcl:" ~r"#[0-9a-f]{6}"
    ECL   = "ecl:" ("amb" / "blu" / "brn" / "gry" / "grn" / "hzl" / "oth") 
    PID   = "pid:" ~r"[0-9]{9}"
    CID   = "cid:" ~r"[0-9a-zA-Z]*"

    HGTCM = NUMB "cm"
    HGTIN = NUMB "in"

    NUMB  = ~r"[0-9]{2,4}"
""")

class PassportVisitor(NodeVisitor):
    def visit_EXPR(self, node, visited_children):
        assert not {"BYR", "IYR", "EYR", "HGT", "HCL", "ECL", "PID"}.difference(visited_children)

    def visit_ITEM(self, node, visited_children):
        return node.children[0].children[0].expr_name

    def visit_BYR(self, node, visited_children):
        assert 1920 <= visited_children[1] <= 2002

    def visit_IYR(self, node, visited_children):
        assert 2010 <= visited_children[1] <= 2020

    def visit_EYR(self, node, visited_children):
        assert 2020 <= visited_children[1] <= 2030

    def visit_HGTCM(self, node, visited_children):
        assert 150 <= visited_children[0] <= 193

    def visit_HGTIN(self, node, visited_children):
        assert 59 <= visited_children[0] <= 76

    def visit_NUMB(self, node, visited_children):
        return int(node.text)

    def generic_visit(self, node, visited_children):
        return visited_children or node

pv = PassportVisitor()
pv.grammar = grammar

data = open("input.txt").read().split("\n\n")
valid = 0
for entry in data:
    try:
        pv.parse(entry)
    except (ParseError, VisitationError, IncompleteParseError):
        continue
    else:
        valid += 1
print("Valid:", valid)

В конце здесь мы зацикливаемся по записям, и ловить один из нескольких различных типов ошибок, анализатор может бросить, когда есть исключение (включая нашу собственную Assert )

У 64 строк кода это не самые уплотнительные анализаторы возможны, но, надеюсь, это демонстрирует некоторые идеи о том, как вы можете приблизиться к числу таревников для задач, таких как это без необходимости повторно придумывать синтаксический разбор.

Далее!

Оригинал: “https://dev.to/meseta/advent-of-code-day-04-using-peg-grammars-35b3”