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

Advent of Code 2020: День 02 (b) Использование Regex по имени группы

Хорошо, вот регельс, который вы хотели за день 2 появления Кода 2020 Все упомянутые в этом посте: Rege … Tagged с AdhentOfCode, Python.

Advent of Code 2020 (26 части серии)

Хорошо, вот регельс, который вы хотели за День 2 пришествия Кода 2020

Все упомянутые в этом посте: Regex, цепочка оператора, названные группы, XOR

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

Напоминание о формате:

  • Номер (низкий диапазон)
  • тире
  • Номер (высокий ассортимент)
  • пространство
  • письмо (рассматриваемое письмо)
  • двоеточие
  • пространство
  • строка для проверки

Regeex имеет … Возможно, заслуженная репутация для того, чтобы быть непроницаемыми для тех, кто ее изучает, но я думаю, что много связано с тем, насколько компактным является его нотация. Я думаю, что Regex намного легче писать, чем читать, поэтому, надеюсь, если бы я объяснил процесс, было бы немного проще.

Позвольте мне сломать шаблон один шаг за раз:

Номер Поэтому нам нужно соответствовать одному или нескольким цифрам подряд. В большинстве ароматизаторов Regex у нас есть класс персонажа \ D который соответствует любой цифре от 0 до 9. Мы можем изменить это \ D с количеством. Там символ короткого квантификатора + что означает «один или несколько». Так просто, \ D + означает «один или несколько цифр подряд». Если мы не использовали этот сокровенный персонаж, мы также сможем указать точное количество символов, которое мы ожидаем, поэтому \ d {1} будет означать «ровно одну цифру», а \ D {1,2} будет означать «между одной или двумя цифрами». Мы будем придерживаться \ D + на данный момент.

тире Regex в основном являются кучу групп и литералов. Если мы хотим подобрать тире, мы просто пишем dash - Отказ Пока это не внутри ничего другого, это будет рассматриваться как буквальный характер - Отказ

Еще один номер Так же, как и раньше, мы можем использовать \ D + здесь. До сих пор наше Regex выглядит так: \ D + - \ D + что будет соответствовать таким вещам, как 23-99 или 1-15 Отказ

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

письмо Нам нужно сопоставить ровно одну букву. В Regex мы можем сделать группы персонажей. что-то вроде [ABC] будет соответствовать одному символу, который является либо А , B или c . Вы также можете сделать диапазоны. Итак, [A-Z] будет соответствовать любой строчной букве между А и z Отказ Вы также можете объединить диапазоны, так что [A-ZA-Z] означает любую более низкую или заглавную букву. Но, тоже есть сокращение и так же, как \ D Выше снятно для [0-9] Есть сокращение \ W Что соответствует любому письму или подчеркиванию (простой способ запоминания, так это то, что часто символы допустимые для имени переменных в языках программирования). Поскольку нам нужна только один из них, никаких количественных квантов не требуется.

толстая кишка и пространство Опять же, мы можем использовать литерал : здесь.

Много букв * Мы можем использовать этот старый + квантификатор с нашими \ W класс, чтобы соответствовать нескольким буквам. Наше регулярное выражение теперь выглядит так:

\d+-\d+ \w: \w+

Доказательство того, что он работает, мы можем использовать Regex101 проверить.

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

(\d+)-(\d+) (\w): (\w+)

Надеюсь, для тех, кто не знаком с регулярным выражением, это не выглядит слишком запугивающим. Если вы облегчаетесь, то позвольте мне сделать вас менее комфортно, показывая вам, почему Regeex имеет плохую репутацию, что она делает.

Напомним, что я сказал \ D был сокращенным для класса [0-9]; и + был коротким квантификатором? Ну, давайте скажем, я не использовал какую-либо сокращение и переписал вышеупомянутый Longhand:

([0-9]{1,})-([0-9]{1,}) ([a-zA-Z_]): ([a-zA-Z_]{1,})

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

Реализация его

Библиотека Regex Python только что называется Re и предполагая, что у нас есть на наших Вход Значение раньше, используя его работает так:

match = re.match('(\d+)-(\d+) (\w): (\w+)', entry)
print(match.groups())

Выход

('9', '12', 't', 'ttfjvvtgxtctrntnhtt')

Как видите, где потребовалось многочисленные строки кода для реализации состояния для анализа текста, Regex достиг его в паре строк.

На данный момент мы можем воспользоваться распаковкой Python, чтобы придерживаться ценностей, которые Группа () Возвращает переменные:

match = re.match('(\d+)-(\d+) (\w): (\w+)', entry)
low_str, high_str, letter, password = match.groups()

Теперь мы можем, наконец, подтвердить пароль. Правила говорят, что есть просто некоторое количество Письмо внутри пароль это между низкий и Высокий Отказ Python уже имеет строковый метод под названием count () Это возвращает, сколько экземпляров подстроки в строке. Итак, Password.Count (буква) это число, которое должно быть между низкий и Высокий быть действительным.

Сравнение цепочки

Python имеет цепочку сравнения, что может удивить некоторых людей. Это означает, что мы можем проверить, если счет между двумя значениями, просто делая:

if low <= password.count(letter) <= high:

В большом количестве языков это не работает или дает странные ценности. Например, если выражение было 4> 3> 2 Некоторые языки оценивают бы 4> 3 Во-первых, что возвращает «правда», а затем перейти к оценке Правда> 3 который обычно возвращает ложь.

Python однако, делает то, что вы это говорите, вышеуказанный код действительно проверит, что Password.Count (буква) между Низкий и Высокий включительно.

Это означает … Если мы действительно хотели кодовой гольф наше решение, мы могли бы выжать все, что мы видели, на три линии:

import re
data = [re.match('(\d+)-(\d+) (\w): (\w+)', line).groups() for line in open("input.txt").readlines()]
print("Valid passwords", sum(int(low) <= password.count(letter) <= int(high) for low, high, letter, password in data))

Nigh-на нечитаемо, но это очень компактно.

Хотя это нормально для простых упражнений, как это, это далеко не приемлемо в реальном мире.

Нет, как и действительно реализовать это

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

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

pattern = re.compile("(\d+)-(\d+) (\w): (\w+)")

Это означает позже, я могу просто сделать:

match = pattern.match(entry)

Второе, что я собираюсь сделать, это использовать «именованные группы», уникальную особенность Regex Python, где мы можем дать имена группу, а Python выкроет словарь вместо кортежа. Это в теории затрудняет ошибку, потому что вы перепутали порядок кортежа.

pattern = re.compile('(?P\d+)-(?P\d+) (?P\w): (?P\w+)', entry)
print(pattern.match(entry).groupdict())

Выход

{'low': '9', 'high': '12', 'letter': 't', 'password': 'ttfjvvtgxtctrntnhtt'}

Это делает регулярное выражение дольше и менее читаемо, но делает возвратную ценность, обрабатывающую немного более надежную.

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

class Password:
    pattern = re.compile("(?P\d+)-(?P\d+) (?P\w): (?P\w+)", entry)

    def __init__(self, string: str): 
        extracted = Password.pattern.match(string)
        groups = extracted.groupsdict()
        self.low = int(groups["low"])
        self.high = int(groups["high"])
        self.letter = groups["letter"]
        self.password = groups["password"]

    def is_valid(self) -> bool:
        return self.low <= self.password.count(self.letter) <= self.high

Обратите внимание на использование переменной класса для рисунка. Есть несколько плюсов и минусов того, что я не буду идти прямо сейчас. Также обратите внимание на подсказку типа Python. Это полезно иметь

Обработка исключений

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

В нашем случае есть одно большое исключение для обработки, которое заключается в том, если Regex не совпадает. Нам нужно немного обновить наш код:

class PasswordGeneralError(exception):
    def __init__(self, message):
        self.message = message
        super().__init__(self.message)

class PasswordFormatError(PasswordGeneralError):
    pass

class Password:
    pattern = re.compile("(?P\d+)-(?P\d+) (?P\w): (?P\w+)", entry)

    def __init__(self, string: str):
        extracted = Password.pattern.match(string)
        if not extracted:
          raise PasswordFormatError("Password line does not match pattern")
        groups = extracted.groupsdict()
        self.low = int(groups["low"])
        self.high = int(groups["high"])
        self.letter = groups["letter"]
        self.password = groups["password"]

    def is_valid(self) -> bool:
        return self.low <= self.password.count(self.letter) <= self.high

Код сейчас, в случае отсутствия совпадения ( Extract () ничего не возвращается), поднимет PasswordFormatorRor Исключение, которое можно поймать в других местах.

Теперь мы можем запустить проверку паролем в одну запись:

Password(entry).is_valid()

Это вернется правдой, если Вход был действительным паролем и ложным в противном случае.

Обработка файлов

Пока Пароль Класс может иметь дело с одним паролем, нам нужно больше кода, чтобы потреблять все входные файлы. Для этого мы можем написать класс Anher, здесь показанный с обработкой исключения, когда мы хотим поймать, когда проверить Поднимает исключение и сообщите пользователю полезную информацию, такую как какая строка входного файла была проблема.

class PasswordFileParseError(PasswordGeneralError):
    def __init__(self, message, line):
        self.message = f"{message} on line {line}"
        super().__init__(self.message)

class PasswordChecker:
    def __init__(self, file):
        self.file = file

    def count_valid(self) -> int:
        total = 0
        with open(self.file) as file:
            for idx, line in enumerate(file):
            try:
                valid = self.validate(line)
            except PasswordFormatError as err:
                raise PaswordFileParseError("Could not validate", idx) from err
            total += int(valid)
        return total

    @staticmethod
    def validate(line) -> bool:
        return Password(line).is_valid()

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

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

Окончательное использование теперь просто:

checker = PasswordChecker("input.txt")
print("Number valid", checker.count_valid()

В части 2 изменится правила проверки пароля. Где до того, как мы ожидаем, что письмо появится в пароле между низкий и Высокий времена; Теперь числа являются позициями символов (1-индексированные), и ожидается, что буква будет отображаться только в одном из двух позиций символов.

Так как у нас есть наши классы написаны, только необходимо изменить IS_Valid () функция. Это может быть сделано одним из нескольких способов, в зависимости от обстоятельства, вы можете просто редактировать код. Вы можете создать детский класс Пароль и переопределить IS_Valid () метод. Вы можете обезьянить-патч исправить (если в чрезвычайной ситуации).

Какой бы способ это произойдет, код, который был ранее:

def is_valid(self) -> bool:
    return self.low <= self.password.count(self.letter) <= self.high

сейчас:

def is_valid(self) -> bool:
    return (self.password[self.low-1] == self.letter) != (self.password[self.high-1] == self.letter)

Здесь мы используем в основном булевую операцию XOR, чтобы убедиться, что у нас есть либо одно из соответствующих позиций, но не обоих.

Далее!

Advent of Code 2020 (26 части серии)

Оригинал: “https://dev.to/meseta/advent-of-code-day-02-b-290a”