Регулярное выражение кажется очень длинным, можно ли его улучшить?

Я работаю с благотворительной организацией, которая выполняет несколько работ, более 100, менее 1000 в год. Вакансии обозначаются годом и серийным номером, состоящим из трех цифр, например, 2021 # 123.

Я пишу программу отчета на Python для извлечения заданий из нашей базы данных, и я хотел бы иметь возможность указывать диапазоны заданий, как в диалоговом окне печати можно указать диапазоны страниц, поэтому диапазон, разделенный тире 2021 # 123-45 , или список 2021 # 003,5,07,89,131, разделенных запятыми. Я хочу иметь возможность анализировать все эти представления из текстовых строк. На данный момент меня интересует только синтаксис, семантика допустимости диапазона обрабатывается позже. Несмотря на то, что у меня очень ограниченный опыт работы с регулярными выражениями, я чувствовал, что однострочный алгоритм превзойдет целую кучу python if.

Таким образом, допустимые строки, которые я хочу сопоставить, имеют форму
20лет # п ИЛИ
20y # n, q, p, r ИЛИ
20лет # нм
где y — любые две цифры, а m, n, p, q, r — от 1 до 3 цифр

Это вопрос о регулярном выражении. Единственное, на что следует обратить внимание на Python — это использование r ‘…’ для необработанной строки.

Я изо всех сил пытался написать регулярное выражение, которое фиксирует любую из этих форм, пытаясь сопоставить год # ddd, а затем ИЛИ разные окончания. Думаю, я поссорился из-за того, что единственная работа, у которой вообще нет конца. Поэтому я закончил с этим скорее сельскохозяйственным усилием, которое работает, но выглядит некрасиво. Я не добавлял комментарии только к этому посту, я знал, что если мне нужно что-то изменить в следующем месяце, они мне понадобятся.

job_patt = r'((?:20dd#d{1,3}-d{1,3})|(?:20dd#d{1,3}(?:,d{1,3})+)|(?:20dd#d{1,3}))'
# cap group  (                                                                             )
# non-caps    (?:                      ) (?:                           ) (?:              )
# OR the non-caps, in this L->R order   |                               |
# get year#      20dd#                    20dd#                         20dd#
# get 1 to 3 digits     d{1,3}                    d{1,3}                         d{1,3}
# get one instance of range    -d{1,3}
# get one or more repeats of list         ',ddd'          (?:,d{1,3})+

Что можно сделать, чтобы сократить или привести в порядок это регулярное выражение?

1 ответ
1

Это не столько вопрос Python, сколько вопрос RegEx. Тег Python практически не имеет значения.

‘+’ -vs- ‘*’

Ваше регулярное выражение включает два альтернативных варианта:

  • (?:20dd#d{1,3}(?:,d{1,3})+)
  • (?:20dd#d{1,3})

Разница в этих выражениях заключается в том, что первое включает одно или несколько повторений ,d{1,3}, где, поскольку второй не включает в себя повторов этого …

… другими словами, у вас есть 20dd#d{1,3} с последующим 0 или более повторов из ,d{1,3}.

Таким образом, мы можем заменить + модификатор с * модификатор, и пара альтернатив становится просто

(?:20dd#d{1,3}(?:,d{1,3})*)

Общий префикс

Все три (или, скорее, оба теперь, когда последние два были объединены в одно) регулярные выражения начинаются с 20dd#. Вы можете выделить это из обоих шаблонов.

20dd# (?: (?: d{1,3}-d{1,3} | d{1,3}(?:,d{1,3})* ) )

Группы захвата

Нет смысла создавать группу захвата для всего выражения регулярного выражения. Все совпадение всегда фиксируется как группа 0.

Переработанное регулярное выражение и пример

Регулярное выражение можно переписать как:

r"20dd#(?:(?:d{1,3}-d{1,3}|d{1,3}(?:,d{1,3})*))"

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

Примечание: Поскольку комментарии следуют за октторпом (#) персонаж в re.VERBOSE режим, нам нужно выйти из него (#), чтобы указать, что это действительно символ, который мы хотим сопоставить, а не начало комментария.

import re

JOB_PATTERN = re.compile(r"""
                          20dd                     # A 4-digit year
                          #                         # followed by a hash mark
                          (?:                        # followed by either ...
                             (?:d{1,3}-d{1,3})        # a range of serial #'s
                             |                          # or
                             (?:d{1,3}(?:,d{1,3})*)   # a list of serial #'s
                          )""", re.VERBOSE)


test_cases = ["junk2021#123-45bar",
              "baz2021#003,5,07,89,131foo"]

for test_case in test_cases:
    m = JOB_PATTERN.search(test_case)
    if m:
        print("Found:", m[0])

Выход:

Found: 2021#123-45
Found: 2021#003,5,07,89,131

Обновлять

Основываясь на ответе OP на комментарий RootTwo, действительно желательны именованные группы захвата. Вот обновленный пример с улучшением RootTwo:

import re

JOB_PATTERN = re.compile(
    r"""(?P<year>              # Capturedas "year" ...
           20dd                 # a 4-digit year
        )
        #                     # followed by a hash mark
        (?:                    # followed by either ...
           (?P<range>             # captured as "range" ...
              d{1,3}-d{1,3}        # a range of serial #'s
           )
           |                      # or
           (?P<list>              # captured as "list" ...
               d{1,3}               # a serial number
               (?:,d{1,3})*         # or list of comma-seperated serial #'s
           )
        )
        """, re.VERBOSE)

test_cases = ["junk2021#123-45bar",
              "baz2021#003,5,07,89,131foo"]

for test_case in test_cases:
    m = JOB_PATTERN.search(test_case)
    if m:
        if m.lastgroup == "range":
            start, end = m['range'].split('-')
            print("Year:", m['year'], " from", start, "to", end)
        else:
            serial_numbers = m['list'].split(',')
            print("Year:", m['year'], " serial #':", serial_numbers)

Выход:

Year: 2021  from 123 to 45
Year: 2021  serial #': ['003', '5', '07', '89', '131']

  • 2

    Я придумал идентичный шаблон, за исключением того, что использовал именованные группы захвата, чтобы можно было использовать Match.lastgroup() чтобы определить, соответствует ли он диапазону или списку заданий.

    — RootTwo


  • @RootTwo Это было бы полезным улучшением. OP просто фиксировал все выражение и явно использование групп без захвата для отдельных деталей, поэтому я не изменил это в своих переработках регулярных выражений.

    — А.Дж.Нойфельд

  • @RootTwo, не могли бы я попросить вас написать ответ с именованными группами захвата, пожалуйста. Тот факт, что я явно использовал не захватывающие группы, не означает, что это лучше всего, и мне не нравится то, что кажется чрезмерным (?: Синтаксис в любом случае. т, но от этого становится лучше.

    — Neil_UK


  • 1

    @Neil_UK, это буквально то же самое, что и ответ AJNeufeld, за исключением части года, которую я использовал (?P<year>20dd). в части диапазона я использовал (?P<range> вместо (?:, а в части списка я использовал (?<list> вместо первого (?:.

    — RootTwo

  • Примечание: @RootTwo, очевидно, имел в виду (?P<list>, нет (?<list>. Я добавил обновленный пример с группой захвата RootTwo и демонстрацией Match.lastgroup имущество.

    — А.Дж.Нойфельд

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *