Я работаю с благотворительной организацией, которая выполняет несколько работ, более 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 ответ
Это не столько вопрос 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']

Я придумал идентичный шаблон, за исключением того, что использовал именованные группы захвата, чтобы можно было использовать
Match.lastgroup()чтобы определить, соответствует ли он диапазону или списку заданий.— RootTwo
@RootTwo Это было бы полезным улучшением. OP просто фиксировал все выражение и явно использование групп без захвата для отдельных деталей, поэтому я не изменил это в своих переработках регулярных выражений.
— А.Дж.Нойфельд
@RootTwo, не могли бы я попросить вас написать ответ с именованными группами захвата, пожалуйста. Тот факт, что я явно использовал не захватывающие группы, не означает, что это лучше всего, и мне не нравится то, что кажется чрезмерным (?: Синтаксис в любом случае. т, но от этого становится лучше.
— Neil_UK
@Neil_UK, это буквально то же самое, что и ответ AJNeufeld, за исключением части года, которую я использовал
(?P<year>20dd). в части диапазона я использовал(?P<range>вместо(?:, а в части списка я использовал(?<list>вместо первого(?:.— RootTwo
Примечание: @RootTwo, очевидно, имел в виду
(?P<list>, нет(?<list>. Я добавил обновленный пример с группой захвата RootTwo и демонстрациейMatch.lastgroupимущество.— А.Дж.Нойфельд