Регулярний вираз HOWTO

Автор:

A.M. Kuchling <amk@amk.ca>

Вступ

Регулярні вирази (так звані RE, або регулярні вирази, або шаблони регулярних виразів) — це, по суті, крихітна вузькоспеціалізована мова програмування, вбудована в Python і доступна через модуль re. Використовуючи цю маленьку мову, ви визначаєте правила для набору можливих рядків, які ви хочете зіставити; цей набір може містити англійські речення, або адреси електронної пошти, або команди TeX, або будь-що, що вам подобається. Потім ви можете поставити такі запитання, як «Чи відповідає цей рядок шаблону?» або «Чи є відповідність шаблону десь у цьому рядку?». Ви також можете використовувати RE, щоб змінити рядок або розділити його різними способами.

Шаблони регулярних виразів компілюються в серію байт-кодів, які потім виконуються механізмом відповідності, написаним мовою C. Для розширеного використання може знадобитися звернути особливу увагу на те, як механізм виконуватиме заданий RE, і записати RE у певним чином, щоб створити байт-код, який працює швидше. Оптимізація не розглядається в цьому документі, оскільки вона вимагає, щоб ви добре розуміли внутрішні механізми відповідності.

Мова регулярних виразів є відносно невеликою та обмеженою, тому не всі можливі завдання обробки рядків можна виконати за допомогою регулярних виразів. Також є завдання, які можна виконувати регулярними виразами, але вирази виявляються дуже складними. У цих випадках вам може бути краще написати код Python для виконання обробки; Хоча код Python буде повільнішим, ніж складний регулярний вираз, він також, ймовірно, буде більш зрозумілим.

Прості візерунки

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

Для детального пояснення інформатики, що лежить в основі регулярних виразів (детермінованих і недетермінованих кінцевих автоматів), ви можете звернутися до майже будь-якого підручника з написання компіляторів.

Відповідні символи

Більшість літер і символів просто збігаються. Наприклад, регулярний вираз test точно збігатиметься з рядком test. (Ви можете ввімкнути режим без урахування регістру, який дозволив би цьому RE відповідати також Test або TEST; докладніше про це пізніше.)

З цього правила є винятки; деякі символи є спеціальними metacharacters і не відповідають самі собі. Натомість вони сигналізують про те, що потрібно зіставити щось незвичайне, або впливають на інші частини RE, повторюючи їх або змінюючи їх значення. Велика частина цього документа присвячена обговоренню різних метасимволів і того, що вони роблять.

Ось повний список метасимволів; їхнє значення буде обговорено в решті цього HOWTO.

. ^ $ * + ? { } [ ] \ | ( )

Перші метасимволи, які ми розглянемо, це [ і ]. Вони використовуються для вказівки класу символів, який є набором символів, які ви хочете зіставити. Символи можна перераховувати окремо або діапазон символів можна вказати двома символами та розділити їх символом '-'. Наприклад, «[abc]» відповідатиме будь-якому із символів «a», «b» або «c»; це те саме, що [a-c], який використовує діапазон для вираження того самого набору символів. Якщо ви хочете зіставити лише малі літери, вашим RE буде [a-z].

Метасимволи (крім \) неактивні всередині класів. Наприклад, [akm$] відповідатиме будь-якому із символів 'a', 'k', 'm' або '$'; '$' зазвичай є метасимволом, але всередині класу символів він позбавлений особливої природи.

Ви можете зіставити символи, яких немає в списку в класі, complementing набір. Це вказується додаванням '^' як першого символу класу. Наприклад, [^5] відповідатиме будь-якому символу, крім '5'. Якщо каретка з’являється в іншому місці класу символів, вона не має особливого значення. Наприклад: [5^] відповідатиме або '5'', або '^'.

Мабуть, найважливішим метасимволом є зворотна коса риска, \. Як і в рядкових літералах Python, за зворотною скісною рискою можуть слідувати різні символи, щоб позначити різні спеціальні послідовності. Він також використовується для екранування всіх метасимволів, щоб ви все ще могли зіставляти їх у шаблонах; наприклад, якщо вам потрібно знайти відповідність [ або \, ви можете поставити перед ними зворотну скісну риску, щоб видалити їхнє спеціальне значення: \[ або \\.

Деякі спеціальні послідовності, що починаються з '\', представляють заздалегідь визначені набори символів, які часто є корисними, наприклад набір цифр, набір літер або набір будь-чого, що не є пробілами.

Розглянемо приклад: \w відповідає будь-якому буквено-цифровому символу. Якщо шаблон регулярного виразу виражено в байтах, це еквівалентно класу [a-zA-Z0-9_]. Якщо шаблон регулярного виразу є рядком, \w відповідатиме всім символам, позначеним як літери в базі даних Unicode, наданій модулем unicodedata. Ви можете використовувати більш обмежене визначення \w у шаблоні рядка, поставивши прапорець re.ASCII під час компіляції регулярного виразу.

Наступний список спеціальних послідовностей не є повним. Щоб отримати повний список послідовностей і розширених визначень класів для шаблонів рядків Unicode, перегляньте останню частину Синтаксису регулярного виразу у довіднику стандартної бібліотеки. Загалом, версії Unicode відповідають будь-якому символу, який знаходиться у відповідній категорії бази даних Unicode.

\d

Збігається з будь-якою десятковою цифрою; це еквівалентно класу [0-9].

\D

Відповідає будь-якому нецифровому символу; це еквівалентно класу [^0-9].

\s

Відповідає будь-якому пробілу; це еквівалентно класу [ \t\n\r\f\v].

\S

Відповідає будь-якому непробільному символу; це еквівалентно класу [^ \t\n\r\f\v].

\w

Відповідає будь-якому буквено-цифровому символу; це еквівалентно класу [a-zA-Z0-9_].

\W

Відповідає будь-якому небуквено-цифровому символу; це еквівалентно класу [^a-zA-Z0-9_].

Ці послідовності можна включити в клас символів. Наприклад, [\s,.] — це клас символів, який відповідатиме будь-якому пробілу, або ','' чи '.''.

Останнім метасимволом у цьому розділі є .. Він відповідає будь-чому, крім символу нового рядка, і є альтернативний режим (re.DOTALL), де він відповідатиме навіть новому рядку. . часто використовується, коли потрібно знайти відповідність «будь-якому символу».

Повторення речей

Здатність зіставляти різні набори символів — це перше, що можуть зробити регулярні вирази, що ще не можливо за допомогою методів, доступних для рядків. Однак, якби це була єдина додаткова можливість регулярних виразів, вони не були б великим прогресом. Інша можливість полягає в тому, що ви можете вказати, що частини RE мають повторюватися певну кількість разів.

Перший метасимвол для повторення речей, які ми розглянемо, це *. * не відповідає буквальному символу '*'; натомість він визначає, що попередній символ може бути зіставлений нуль або більше разів замість точно одного разу.

Наприклад, ca*t відповідатиме 'ct' (0 символів 'a'), 'cat' (1 'a'), 'caaat' (3 символи 'a') і так далі.

Такі повтори, як * є greedy; при повторенні RE механізм пошуку відповідності намагатиметься повторити його якомога більше разів. Якщо пізніші частини шаблону не збігаються, механізм пошуку відповідностей створить резервну копію та спробує знову з меншою кількістю повторень.

Покроковий приклад зробить це більш очевидним. Розглянемо вираз a[bcd]*b. Це відповідає літері 'a', нулю або більше літер з класу [bcd] і, нарешті, закінчується 'b'. Тепер уявіть, що зіставлення цього RE з рядком 'abcbd.

Крок

Збіг

Пояснення

1

a

a у RE відповідає.

2

abcbd

Механізм збігається з [bcd]*, просуваючись якомога далі, тобто до кінця рядка.

3

Невдача

Механізм намагається знайти відповідність b, але поточна позиція знаходиться в кінці рядка, тому це не вдається.

4

abcb

Зробіть резервну копію, щоб [bcd]* відповідав на один символ менше.

5

Невдача

Спробуйте b ще раз, але поточна позиція знаходиться на останньому символі, який є 'd'.

6

abc

Знову створіть резервну копію, щоб [bcd]* відповідало лише bc.

6

abcb

Спробуйте b знову. Цього разу символом у поточній позиції є 'b', отже, це вдалось.

Кінець RE вже досягнуто, і він відповідає 'abcb. Це демонструє, як система пошуку відповідностей спочатку йде так далеко, як тільки може, і якщо відповідності не знайдено, вона поступово створюватиме резервні копії та повторюватиме решту RE знову і знову. Він виконуватиме резервне копіювання, доки не знайде нульових збігів для [bcd]*, і якщо це згодом не вдасться, система зробить висновок, що рядок взагалі не відповідає RE.

Ще один повторюваний метасимвол – це +, який збігається один або кілька разів. Зверніть увагу на різницю між * і +; * відповідає нуль або більше разів, тому все, що повторюється, може взагалі не бути присутнім, тоді як + вимагає принаймні одного входження. Щоб використати подібний приклад, ca+t відповідатиме 'cat' (1 'a'), 'caaat' (3 'a's), але не відповідатиме 'ct.

Є ще два повторювані оператори або квантори. Знак питання, ?, збігається або один раз, або нуль разів; ви можете розглядати це як позначення чогось як необов’язкового. Наприклад, home-?brew matches either 'homebrew' або 'home-brew'.

Найскладнішим квантором є {m,n}, де m і n є десятковими цілими числами. Цей квантор означає, що має бути принаймні m повторень і не більше n. Наприклад, a/{1,3}b відповідатиме 'a/b', 'a//b', та 'a///b'. Він не збігатиметься з 'ab', який не має похилих рисок, або 'a////b', який має чотири.

Ви можете опустити m або n; у цьому випадку для відсутнього значення приймається розумне значення. Пропуск m інтерпретується як нижня межа 0, тоді як пропуск n призводить до верхньої межі нескінченності.

The simplest case {m} matches the preceding item exactly m times. For example, a/{2}b will only match 'a//b'.

Редукціоністські читачі можуть помітити, що всі три інші квантори можна виразити за допомогою цієї нотації. {0,} те саме, що ` *, {1,} еквівалентно +, and {0,1} is the same as ?. Краще використовувати *, +, або ?, коли є така можливість, просто тому, що вони коротші та легші для читання.

Використання регулярних виразів

Тепер, коли ми розглянули деякі прості регулярні вирази, як ми насправді використовуємо їх у Python? Модуль re надає інтерфейс механізму регулярних виразів, дозволяючи вам компілювати RE в об’єкти, а потім виконувати з ними збіги.

Компіляція регулярних виразів

Регулярні вирази компілюються в об’єкти шаблонів, які мають методи для різноманітних операцій, таких як пошук збігів шаблону або виконання підстановок рядків.

>>> import re
>>> p = re.compile('ab*')
>>> p
re.compile('ab*')

re.compile() також приймає необов’язковий аргумент flags, який використовується для ввімкнення різних спеціальних функцій і варіантів синтаксису. Пізніше ми розглянемо доступні параметри, а поки підійде один приклад:

>>> p = re.compile('ab*', re.IGNORECASE)

RE передається до re.compile() як рядок. RE обробляються як рядки, оскільки регулярні вирази не є частиною основної мови Python, і для їх вираження не створено спеціального синтаксису. (Існують програми, які взагалі не потребують RE, тому немає потреби роздувати специфікацію мови, включаючи їх.) Натомість модуль re є просто модулем розширення C, що входить до складу Python, як і модулі socket або zlib.

Розміщення RE в рядках робить мову Python простішою, але має один недолік, який є темою наступного розділу.

Зворотна коса чума

Як було сказано раніше, у регулярних виразах використовується символ зворотної косої риски ('\'), щоб позначити спеціальні форми або дозволити використовувати спеціальні символи без виклику їх особливого значення. Це суперечить використанню Python того самого символу для тієї ж мети в рядкових літералах.

Припустімо, ви хочете написати RE, який відповідає рядку \section, який можна знайти у файлі LaTeX. Щоб зрозуміти, що писати в коді програми, почніть із потрібного рядка, який потрібно знайти. Далі ви повинні уникнути будь-яких зворотних похилих рисок та інших метасимволів, поставивши перед ними зворотну похилу риску, що призведе до рядка \\section. Отриманий рядок, який потрібно передати в re.compile(), має бути \\section. Однак, щоб виразити це як рядковий літерал Python, обидві зворотні похилі риски потрібно екранувати знову.

Персонажі

етап

\розділ

Текстовий рядок, який потрібно знайти

\\розділ

Екранований зворотний слеш для re.compile()

"\\\\розділ"

Екрановані зворотні косі риски для рядкового літералу

Коротше кажучи, щоб відповідати буквальному зворотному слешу, потрібно написати '\\\\' як рядок RE, тому що регулярний вираз має бути \\, а кожен зворотний слеш має бути виражений як \\ всередині звичайного рядкового літералу Python. У RE, які неодноразово містять зворотні косі риски, це призводить до великої кількості повторюваних зворотних косих риск і ускладнює розуміння результуючих рядків.

Рішення полягає у використанні необробленої рядкової нотації Python для регулярних виразів; зворотні косі риски не обробляються спеціальним чином у рядковому літералі з префіксом 'r', тому r"\n" є двосимвольним рядком, що містить '\' і ' n', тоді як "\n" є односимвольним рядком, що містить новий рядок. Регулярні вирази часто записуються в коді Python з використанням цієї необробленої рядкової нотації.

Крім того, спеціальні керуючі послідовності, дійсні в регулярних виразах, але недійсні як рядкові літерали Python, тепер призводять до DeprecationWarning і згодом стануть SyntaxError, що означає, що послідовності будуть недійсними якщо не використовується необроблений рядковий запис або екранування зворотних скісних риск.

Звичайний рядок

Необроблений рядок

"ab*"

r"ab*"

"\\\\розділ"

r"\\розділ"

"\\w+\\s+\\1"

r"\w+\s+\1"

Виконання матчів

Якщо у вас є об’єкт, що представляє скомпільований регулярний вираз, що ви з ним робите? Об’єкти шаблонів мають кілька методів і атрибутів. Тут будуть розглянуті лише найважливіші з них; зверніться до документів re для отримання повного списку.

Метод/атрибут

призначення

match()

Визначте, чи відповідає RE на початку рядка.

search()

Проскануйте рядок, шукаючи будь-яке місце, де цей RE відповідає.

findall()

Знайти всі підрядки, де відповідає RE, і повернути їх у вигляді списку.

finditer()

Знайти всі підрядки, де відповідає RE, і повернути їх як iterator.

match() і search() повертають None, якщо збіг не знайдено. Якщо вони успішні, повертається екземпляр match object, який містить інформацію про збіг: де він починається і закінчується, підрядок, з яким він збігся, тощо.

You can learn about this by interactively experimenting with the re module. If you have tkinter available, you may also want to look at Tools/demo/redemo.py, a demonstration program included with the Python distribution. It allows you to enter REs and strings, and displays whether the RE matches or fails. redemo.py can be quite useful when trying to debug a complicated RE.

Цей HOWTO використовує стандартний інтерпретатор Python для своїх прикладів. Спочатку запустіть інтерпретатор Python, імпортуйте модуль re і скомпілюйте RE:

>>> import re
>>> p = re.compile('[a-z]+')
>>> p
re.compile('[a-z]+')

Тепер ви можете спробувати зіставити різні рядки з RE [a-z]+. Порожній рядок взагалі не повинен збігатися, оскільки + означає „одне або більше повторень“. match() має повернути None у цьому випадку, що призведе до того, що інтерпретатор не друкуватиме вихідні дані. Ви можете явно надрукувати результат match(), щоб це було зрозуміло.

>>> p.match("")
>>> print(p.match(""))
None

Тепер давайте спробуємо це на рядку, який має збігатися, наприклад tempo. У цьому випадку match() поверне об’єкт відповідності, тому ви повинні зберегти результат у змінній для подальшого використання.

>>> m = p.match('tempo')
>>> m
<re.Match object; span=(0, 5), match='tempo'>

Тепер ви можете запитати match object для отримання інформації про відповідний рядок. Екземпляри об’єктів зіставлення також мають кілька методів і атрибутів; найважливіші з них:

Метод/атрибут

призначення

group()

Повертає рядок, який відповідає RE

start()

Повернути вихідну позицію матчу

end()

Повернення кінцевої позиції матчу

span()

Повертає кортеж, що містить (початкову, кінцеву) позиції збігу

Спроба цих методів незабаром прояснить їх значення:

>>> m.group()
'tempo'
>>> m.start(), m.end()
(0, 5)
>>> m.span()
(0, 5)

group() повертає підрядок, який був зіставлений RE. start() і end() повертають початковий і кінцевий індекси збігу. span() повертає початковий і кінцевий індекси в одному кортежі. Оскільки метод match() лише перевіряє, чи відповідає RE на початку рядка, start() завжди дорівнюватиме нулю. Однак метод шаблонів search() сканує рядок, тому в цьому випадку збіг може не початися з нуля.

>>> print(p.match('::: message'))
None
>>> m = p.search('::: message'); print(m)
<re.Match object; span=(4, 11), match='message'>
>>> m.group()
'message'
>>> m.span()
(4, 11)

У реальних програмах найпоширенішим стилем є збереження match object у змінній, а потім перевірка, чи вона була None. Зазвичай це виглядає так:

p = re.compile( ... )
m = p.match( 'string goes here' )
if m:
    print('Match found: ', m.group())
else:
    print('No match')

Два методи шаблону повертають усі збіги шаблону. findall() повертає список відповідних рядків:

>>> p = re.compile(r'\d+')
>>> p.findall('12 drummers drumming, 11 pipers piping, 10 lords a-leaping')
['12', '11', '10']

Префікс r, який робить літерал необробленим рядковим літералом, потрібен у цьому прикладі, тому що escape-послідовності у звичайному «вареному» рядковому літералі, які не розпізнаються Python, на відміну від регулярних виразів, тепер призводять до DeprecationWarning і з часом стане SyntaxError. Дивіться Зворотна коса чума.

findall() має створити весь список, перш ніж його можна буде повернути як результат. Метод finditer() повертає послідовність екземплярів match object як iterator:

>>> iterator = p.finditer('12 drummers drumming, 11 ... 10 ...')
>>> iterator  
<callable_iterator object at 0x...>
>>> for match in iterator:
...     print(match.span())
...
(0, 2)
(22, 24)
(29, 31)

Функції рівня модуля

Вам не потрібно створювати шаблонний об’єкт і викликати його методи; модуль re також надає функції верхнього рівня під назвою match(), search(), findall(), sub() і так далі. Ці функції приймають ті самі аргументи, що й відповідний метод шаблону з рядком RE, доданим як перший аргумент, і все одно повертають None або екземпляр match object.

>>> print(re.match(r'From\s+', 'Fromage amk'))
None
>>> re.match(r'From\s+', 'From amk Thu May 14 19:12:10 1998')  
<re.Match object; span=(0, 5), match='From '>

Під капотом ці функції просто створюють для вас шаблонний об’єкт і викликають для нього відповідний метод. Вони також зберігають скомпільований об’єкт у кеш-пам’яті, тому для майбутніх викликів із використанням того самого RE не потрібно буде аналізувати шаблон знову і знову.

Ви повинні використовувати ці функції на рівні модуля, чи вам слід отримати шаблон і викликати його методи самостійно? Якщо ви отримуєте доступ до регулярного виразу в циклі, його попередня компіляція заощадить кілька викликів функцій. За межами циклів немає великої різниці завдяки внутрішньому кешу.

Прапори компіляції

Прапори компіляції дозволяють змінювати деякі аспекти роботи регулярних виразів. Прапори доступні в модулі re під двома назвами: довгою назвою, як-от IGNORECASE, і короткою однолітерною формою, як-от I. (Якщо ви знайомі з модифікаторами шаблонів Perl, однолітерні форми використовують ті самі літери; наприклад, коротка форма re.VERBOSE це re.X.) Кілька прапорів можуть вказувати їх порозрядним АБО; re.I | re.M встановлює, наприклад, прапорці I і M.

Ось таблиця доступних прапорів із більш детальним поясненням кожного з них.

Прапор

Значення

ASCII, A

Декілька символів екранування, як-от \w, \b, \s і \d, збігаються лише з символами ASCII з відповідною властивістю.

DOTALL, S

Зробіть «.» відповідним будь-якому символу, включно з новими рядками.

IGNORECASE, I

Збіги без урахування регістру.

LOCALE, L

Виконайте відповідність з урахуванням локалі.

MULTILINE, M

Багаторядкова відповідність, що впливає на ^ і $.

VERBOSE, X (для «розширеного»)

Увімкніть докладні RE, які можна організувати більш чітко та зрозуміло.

re.I
re.IGNORECASE

Виконуйте зіставлення без урахування регістру; клас символів і літеральні рядки будуть відповідати буквам, ігноруючи регістр. Наприклад, [A-Z] також відповідатиме малим регістрам. Повна відповідність Unicode також працює, якщо не використовується прапорець ASCII, щоб вимкнути збіги, відмінні від ASCII. Коли шаблони Unicode [a-z] або [A-Z] використовуються в поєднанні з прапором IGNORECASE, вони відповідатимуть 52 літерам ASCII і 4 додатковим літерам, які не належать до ASCII: „İ „ (U+0130, латинська велика літера I з крапкою вгорі), „ı“ (U+0131, латинська мала літера без крапки), „ſ“ (U+017F, латинська мала літера довге s) і „K“ (U +212A, знак Кельвіна). Spam відповідатиме 'Spam', 'spam', 'spAM' або 'ſpam' (останній збігається лише в режимі Unicode). Цей нижній регістр не враховує поточну мову; це буде, якщо ви також установите прапорець LOCALE.

re.L
re.LOCALE

Зробіть \w, \W, \b, \B і відповідність без урахування регістру залежною від поточної мови замість бази даних Unicode.

Локалі — це функція бібліотеки C, призначена для допомоги в написанні програм, які враховують мовні відмінності. Наприклад, якщо ви обробляєте закодований французький текст, ви б хотіли мати можливість писати \w+ для відповідності слів, але \w відповідає лише класу символів [A-Za- z] у шаблонах байтів; він не співпадатиме з байтами, що відповідають é або ç. Якщо ваша система налаштована належним чином і вибрано французьку мову, певні функції C повідомлять програмі, що байт, який відповідає é, також слід вважати літерою. Встановлення прапорця LOCALE під час компіляції регулярного виразу змусить отриманий скомпільований об’єкт використовувати ці функції C для \w; це повільніше, але також дозволяє \w+ відповідати французьким словам, як ви очікуєте. Використання цього прапора не рекомендується в Python 3, оскільки механізм локалізації дуже ненадійний, він обробляє лише одну «культуру» за раз і працює лише з 8-бітними локалями. Зіставлення Unicode вже ввімкнено за замовчуванням у Python 3 для шаблонів Unicode (str), і він здатний обробляти різні локалі/мови.

re.M
re.MULTILINE

(^ і $ ще не пояснено; їх буде введено в розділі Більше метасимволів.)

Зазвичай ^ збігається лише на початку рядка, а $ збігається лише в кінці рядка та безпосередньо перед символом нового рядка (якщо є) у кінці рядка. Якщо вказано цей прапорець, ^ збігається на початку рядка та на початку кожного рядка в рядку, відразу після кожного нового рядка. Подібним чином, метасимвол $ збігається або в кінці рядка, і в кінці кожного рядка (безпосередньо перед кожним новим рядком).

re.S
re.DOTALL

Робить спеціальний символ „“.““ відповідним будь-якому символу взагалі, включно з символом нового рядка; без цього прапорця „“.““ відповідатиме будь-чому крім нового рядка.

re.A
re.ASCII

Змусити \w, \W, \b, \B, \s і \S виконувати відповідність лише ASCII замість повної Зіставлення Unicode. Це має значення лише для шаблонів Unicode та ігнорується для шаблонів байтів.

re.X
re.VERBOSE

Цей прапорець дозволяє вам писати регулярні вирази, які є більш читабельними, надаючи вам більше гнучкості у тому, як ви можете їх форматувати. Якщо вказано цей прапорець, пробіли в рядку RE ігноруються, за винятком випадків, коли пробіли знаходяться в класі символів або перед ними стоїть неекранована зворотна похила риска; це дозволяє більш чітко організувати та зробити відступи RE. Цей прапор також дозволяє розміщувати коментарі в RE, які ігноруватимуться механізмом; коментарі позначаються символом '#', якого немає в класі символів або якому передує неекранований зворотний слеш.

Наприклад, ось RE, який використовує re.VERBOSE; бачите, наскільки легше читати?

charref = re.compile(r"""
 &[#]                # Start of a numeric entity reference
 (
     0[0-7]+         # Octal form
   | [0-9]+          # Decimal form
   | x[0-9a-fA-F]+   # Hexadecimal form
 )
 ;                   # Trailing semicolon
""", re.VERBOSE)

Без параметра verbose RE виглядатиме так:

charref = re.compile("&#(0[0-7]+"
                     "|[0-9]+"
                     "|x[0-9a-fA-F]+);")

У наведеному вище прикладі автоматичне об’єднання рядкових літералів Python було використано для розбиття RE на менші частини, але це все ще важче зрозуміти, ніж версію з використанням re.VERBOSE.

Більше потужності шаблону

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

Більше метасимволів

Є деякі метасимволи, які ми ще не розглянули. Більшість із них буде розглянуто в цьому розділі.

Деякі метасимволи, які залишилися для обговорення, це затвердження нульової ширини. Вони не змушують двигун просуватися по струні; натомість вони взагалі не використовують жодних символів і просто досягають успіху або зазнають невдачі. Наприклад, \b - це твердження, що поточна позиція розташована на межі слова; позиція взагалі не змінюється \b. Це означає, що твердження нульової ширини ніколи не повинні повторюватися, оскільки якщо вони збігаються один раз у заданому місці, вони, очевидно, можуть бути зіставлені нескінченну кількість разів.

|

Чергування або оператор «або». Якщо A і B є регулярними виразами, A|B відповідатиме будь-якому рядку, який відповідає A або B. | має дуже низький пріоритет, щоб він працював розумно, коли ви чергуєте багатосимвольні рядки. Crow|Servo відповідатиме 'Crow' або 'Servo', а не 'Cro', 'w' чи 'S' і 'ervo''.

Щоб відповідати літералу '|', використовуйте \| або вкладіть його в клас символів, як у [|].

^

Збіги на початку рядків. Якщо не встановлено прапорець MULTILINE, він збігатиметься лише на початку рядка. У режимі MULTILINE це також збігається відразу після кожного нового рядка в рядку.

Наприклад, якщо ви бажаєте зіставити слово From лише на початку рядка, використовуйте RE ^From.

>>> print(re.search('^From', 'From Here to Eternity'))  
<re.Match object; span=(0, 4), match='From'>
>>> print(re.search('^From', 'Reciting From Memory'))
None

Щоб відповідати літералу '^', використовуйте \^.

$

Збігається в кінці рядка, який визначається як кінець рядка або будь-яке місце, після якого йде символ нового рядка.

>>> print(re.search('}$', '{block}'))  
<re.Match object; span=(6, 7), match='}'>
>>> print(re.search('}$', '{block} '))
None
>>> print(re.search('}$', '{block}\n'))  
<re.Match object; span=(6, 7), match='}'>

Щоб відповідати літералу '$', використовуйте \$ або вкладіть його в клас символів, як у [$].

\A

Збігається лише на початку рядка. Якщо не в режимі MULTILINE, \A і ^ фактично однакові. У режимі MULTILINE вони відрізняються: \A все ще збігається лише на початку рядка, але ^ може збігатися в будь-якому місці всередині рядка, яке слідує за символом нового рядка.

\Z

Збігається лише в кінці рядка.

\b

Кордон слова. Це твердження нульової ширини, яке збігається лише на початку або в кінці слова. Слово визначається як послідовність буквено-цифрових символів, тому кінець слова позначається пробілом або неалфавітно-цифровим символом.

Наступний приклад відповідає класу лише тоді, коли це повне слово; воно не збігається, якщо міститься всередині іншого слова.

>>> p = re.compile(r'\bclass\b')
>>> print(p.search('no class at all'))
<re.Match object; span=(3, 8), match='class'>
>>> print(p.search('the declassified algorithm'))
None
>>> print(p.search('one subclass is'))
None

Використовуючи цю особливу послідовність, слід пам’ятати про дві тонкощі. По-перше, це найгірша колізія між рядковими літералами Python і послідовностями регулярних виразів. У рядкових літералах Python \b є символом повернення, значення ASCII 8. Якщо ви не використовуєте необроблені рядки, тоді Python перетворить \b на пропуск, а ваш RE не буде відповідати, як ви очікуєте. Наступний приклад виглядає так само, як наш попередній RE, але опущено 'r' перед рядком RE.

>>> p = re.compile('\bclass\b')
>>> print(p.search('no class at all'))
None
>>> print(p.search('\b' + 'class' + '\b'))
<re.Match object; span=(0, 7), match='\x08class\x08'>

По-друге, всередині класу символів, де немає користі для цього твердження, \b представляє символ зворотного простору для сумісності з рядковими літералами Python.

\B

Інше твердження нульової ширини, це протилежність \b, збігається лише тоді, коли поточна позиція не знаходиться на межі слова.

Групування

Часто вам потрібно отримати більше інформації, ніж просто відповідність RE чи ні. Регулярні вирази часто використовуються для аналізу рядків шляхом запису RE, розділеного на кілька підгруп, які відповідають різним цікавим компонентам. Наприклад, рядок заголовка RFC-822 розділено на ім’я заголовка та значення, розділені символом ':', ось так:

From: author@example.com
User-Agent: Thunderbird 1.5.0.9 (X11/20061227)
MIME-Version: 1.0
To: editor@example.com

Це можна вирішити, написавши регулярний вираз, який відповідає всьому рядку заголовка та має одну групу, яка відповідає імені заголовка, та іншу групу, яка відповідає значенню заголовка.

Групи позначені метасимволами '(' та ')'. '(' і ')' мають майже те саме значення, що й у математичних виразах; вони групують вирази, що містяться в них, і ви можете повторити вміст групи за допомогою квантора, наприклад *, +, ?, або {m,n}. Наприклад, (ab)* відповідатиме нулю або більше повторень ab.

>>> p = re.compile('(ab)*')
>>> print(p.match('ababababab').span())
(0, 10)

Групи, позначені '(', ')' також фіксують початковий і кінцевий індекс тексту, якому вони відповідають; це можна отримати, передавши аргумент до group(), start(), end() і span(). Групи нумеруються, починаючи з 0. Група 0 завжди присутня; це весь RE, тому всі методи match object мають групу 0 як аргумент за замовчуванням. Пізніше ми побачимо, як виражати групи, які не фіксують діапазон тексту, якому вони відповідають.

>>> p = re.compile('(a)b')
>>> m = p.match('ab')
>>> m.group()
'ab'
>>> m.group(0)
'ab'

Підгрупи нумеруються зліва направо, починаючи з 1 і вище. Групи можуть бути вкладеними; щоб визначити число, просто порахуйте символи відкриваючих дужок, рухаючись зліва направо.

>>> p = re.compile('(a(b)c)d')
>>> m = p.match('abcd')
>>> m.group(0)
'abcd'
>>> m.group(1)
'abc'
>>> m.group(2)
'b'

group() можна передати кілька номерів груп одночасно, і в цьому випадку він поверне кортеж, що містить відповідні значення для цих груп.

>>> m.group(2,1,2)
('b', 'abc', 'b')

Метод groups() повертає кортеж, що містить рядки для всіх підгруп, від 1 до будь-якої кількості.

>>> m.groups()
('abc', 'b')

Зворотні посилання в шаблоні дозволяють вказати, що вміст попередньої групи захоплення також має бути знайдений у поточному місці в рядку. Наприклад, \1 буде успішним, якщо точний вміст групи 1 можна знайти в поточній позиції, і не вдасться в іншому випадку. Пам’ятайте, що рядкові літерали Python також використовують зворотну скісну риску, за якою слідують числа, щоб дозволити включати довільні символи в рядок, тому обов’язково використовуйте необроблений рядок, коли включаєте зворотні посилання в RE.

Наприклад, наступний RE виявляє подвоєні слова в рядку.

>>> p = re.compile(r'\b(\w+)\s+\1\b')
>>> p.search('Paris in the the spring').group()
'the the'

Подібні зворотні посилання не часто корисні лише для пошуку в рядку — є кілька текстових форматів, які повторюють дані таким чином — але незабаром ви зрозумієте, що вони дуже корисні під час виконання замін рядків .

Неперехоплювані та іменовані групи

Розроблені RE можуть використовувати багато груп як для захоплення цікавих підрядків, так і для групування та структурування самого RE. У складних RE стає важко відслідковувати номери груп. Є дві функції, які допомагають вирішити цю проблему. Обидва вони використовують загальний синтаксис для розширень регулярних виразів, тому ми розглянемо це спочатку.

Perl 5 добре відомий своїми потужними доповненнями до стандартних регулярних виразів. Для цих нових можливостей розробники Perl не могли вибрати нові одноклавішні метасимволи або нові спеціальні послідовності, що починаються з \, не зробивши регулярні вирази Perl різко відмінними від стандартних RE. Наприклад, якби вони вибрали & як новий метасимвол, старі вирази припускали б, що & був звичайним символом і не міг би уникнути його, написавши \& або [ &].

Рішенням, обраним розробниками Perl, було використання (?...) як синтаксису розширення. ? відразу після дужок було синтаксичною помилкою, оскільки ? не було б чого повторювати, тому це не створювало жодних проблем із сумісністю. Символи відразу після ? вказують на те, яке розширення використовується, тому (?=foo) це одне (позитивне твердження попереднього перегляду), а (?:foo) це щось інше ( група без захоплення, що містить підвираз foo).

Python підтримує кілька розширень Perl і додає синтаксис розширення до синтаксису розширення Perl. Якщо першим символом після знака питання є P, ви знаєте, що це розширення, специфічне для Python.

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

Іноді вам потрібно використати групу для позначення частини регулярного виразу, але ви не зацікавлені в отриманні вмісту групи. Ви можете зробити цей факт явним, використовуючи групу без захоплення: (?:...), де ви можете замінити ... будь-яким іншим регулярним виразом.

>>> m = re.match("([abc])+", "abc")
>>> m.groups()
('c',)
>>> m = re.match("(?:[abc])+", "abc")
>>> m.groups()
()

За винятком того факту, що ви не можете отримати вміст того, що відповідає групі, група без захоплення поводиться точно так само, як група захоплення; ви можете помістити в нього що завгодно, повторити це з метасимволом повторення, таким як *, і вкладати його в інші групи (захоплюючі чи незахоплюючі). (?:...) особливо корисний під час зміни існуючого шаблону, оскільки ви можете додавати нові групи, не змінюючи спосіб нумерації всіх інших груп. Слід зазначити, що немає різниці в продуктивності пошуку між групами захоплення та групами без захоплення; жодна форма не є швидшою за іншу.

Більш важливою особливістю є іменовані групи: замість того, щоб посилатися на них номерами, на групи можна посилатися за назвою.

Синтаксис іменованої групи є одним із специфічних для Python розширень: (?P <name> ...). name — це, очевидно, назва групи. Іменовані групи поводяться так само, як групи захоплення, і додатково пов’язують назву з групою. Усі методи match object, які мають справу із захопленням груп, приймають або цілі числа, які посилаються на групу за номером, або рядки, які містять назву потрібної групи. Іменовані групи все ще мають номери, тому ви можете отримати інформацію про групу двома способами:

>>> p = re.compile(r'(?P<word>\b\w+\b)')
>>> m = p.search( '(((( Lots of punctuation )))' )
>>> m.group('word')
'Lots'
>>> m.group(1)
'Lots'

Крім того, ви можете отримати іменовані групи як словник за допомогою groupdict():

>>> m = re.match(r'(?P<first>\w+) (?P<last>\w+)', 'Jane Doe')
>>> m.groupdict()
{'first': 'Jane', 'last': 'Doe'}

Іменовані групи зручні, оскільки вони дозволяють використовувати імена, які легко запам’ятовуються, замість того, щоб запам’ятовувати числа. Ось приклад RE з модуля imaplib:

InternalDate = re.compile(r'INTERNALDATE "'
        r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-'
        r'(?P<year>[0-9][0-9][0-9][0-9])'
        r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
        r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
        r'"')

Очевидно, набагато простіше отримати m.group('zonem'), замість того, щоб пам’ятати, що потрібно отримати групу 9.

Синтаксис зворотних посилань у такому виразі, як (...)\1, посилається на номер групи. Природно, існує варіант, який використовує назву групи замість номера. Це ще одне розширення Python: (?P=name) вказує на те, що вміст групи з назвою name знову повинен бути зіставлений у поточній точці. Регулярний вираз для пошуку подвоєних слів \b(\w+)\s+\1\b також можна записати як \b(?P<word>\w+)\s+(?P=word)\b:

>>> p = re.compile(r'\b(?P<word>\w+)\s+(?P=word)\b')
>>> p.search('Paris in the the spring').group()
'the the'

Попередні твердження

Іншим твердженням нульової ширини є твердження прогнозу. Попередні твердження доступні як у позитивній, так і в негативній формі та виглядають так:

(?=...)

Позитивне прогнозне твердження. Це виконується успішно, якщо регулярний вираз, представлений тут ..., успішно збігається в поточному місці, і не вдається в іншому випадку. Але після спроби виразу, що міститься, механізм пошуку відповідностей взагалі не переходить; решта шаблону пробується там, де почалося твердження.

(?!...)

Негативне прогнозне твердження. Це протилежне до позитивного твердження; це вдається, якщо вираз, що міститься, не збігається з поточною позицією в рядку.

Щоб зробити це конкретним, давайте подивимося на випадок, де корисний погляд наперед. Розглянемо простий шаблон для відповідності назві файлу та розділення його на базове ім’я та розширення, розділені символом .. Наприклад, у news.rc news є базовою назвою, а rc є розширенням назви файлу.

Викрійка для цього досить проста:

.*[.].*$

Зауважте, що . потрібно обробляти спеціально, оскільки це метасимвол, тому він знаходиться всередині класу символів, щоб відповідати лише цьому конкретному символу. Також зверніть увагу на закінчення $; це додається, щоб переконатися, що вся решта рядка повинна бути включена в розширення. Цей регулярний вираз відповідає foo.bar і autoexec.bat, sendmail.cf і printers.conf.

Тепер подумайте про те, щоб трохи ускладнити проблему; що, якщо ви хочете зіставити назви файлів, розширення яких не є bat? Деякі неправильні спроби:

.*[.][^b].*$ Перша спроба вище намагається виключити bat, вимагаючи, щоб перший символ розширення не був b. Це неправильно, оскільки шаблон також не відповідає foo.bar.

.*[.]([^b]..|.[^a].|..[^t])$

Вираз стає складнішим, коли ви намагаєтеся виправити перше рішення, вимагаючи збігу одного з наступних випадків: перший символ розширення не є b; другий символ не є a; або третій символ не є t. Це приймає foo.bar і відхиляє autoexec.bat, але воно вимагає розширення з трьох літер і не приймає ім’я файлу з дволітерним розширенням, наприклад sendmail.cf. Ми знову ускладнимо шаблон, намагаючись його виправити.

.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$

Під час третьої спроби друга та третя літери стають необов’язковими, щоб дозволити відповідні розширення коротші за три символи, наприклад sendmail.cf.

Зараз шаблон стає дуже складним, тому його важко прочитати та зрозуміти. Гірше того, якщо проблема зміниться і ви захочете виключити і bat, і exe як розширення, шаблон стане ще більш складним і заплутаним.

Негативний прогноз прорізає всю цю плутанину:

.*[.](?!bat$)[^.]*$ Негативний перегляд означає: якщо вираз bat не збігається на цьому етапі, спробуйте решту шаблону; якщо bat$ збігається, весь шаблон буде невдалим. Кінцевий $ потрібен, щоб переконатися, що щось на зразок sample.batch, де розширення починається лише з bat, буде дозволено. [^.]* гарантує, що шаблон працює, якщо в назві файлу є кілька крапок.

Виключити інше розширення імені файлу тепер легко; просто додайте його як альтернативу всередину твердження. Наступний шаблон виключає імена файлів, які закінчуються на bat або exe:

.*[.](?!bat$|exe$)[^.]*$

Зміна рядків

До цього моменту ми просто виконували пошук у статичному рядку. Регулярні вирази також часто використовуються для зміни рядків різними способами, використовуючи такі методи шаблонів:

Метод/атрибут

призначення

split()

Розділіть рядок на список, розділивши його там, де відповідає RE

sub()

Знайдіть усі підрядки, де відповідає RE, і замініть їх іншим рядком

subn()

Робить те саме, що sub(), але повертає новий рядок і кількість замін

Розбиття рядків

Метод шаблону split() розділяє рядок на частини, де збігається RE, повертаючи список фрагментів. Він подібний до методу рядків split(), але надає набагато більшу загальність роздільників, за якими можна розділяти; string split() підтримує лише поділ за пробілами або за фіксованим рядком. Як і слід було очікувати, також існує функція re.split() на рівні модуля.

.split(string[, maxsplit=0])

Розділити рядок за збігами регулярного виразу. Якщо в RE використовуються круглі дужки, їхній вміст також повертатиметься як частина результуючого списку. Якщо maxsplit відмінний від нуля, виконується не більше ніж maxsplit.

Ви можете обмежити кількість зроблених поділів, передавши значення maxsplit. Якщо maxsplit відмінний від нуля, буде виконано щонайбільше maxsplit розбиття, а залишок рядка повертається як останній елемент списку. У наступному прикладі роздільником є будь-яка послідовність не буквено-цифрових символів.

>>> p = re.compile(r'\W+')
>>> p.split('This is a test, short and sweet, of split().')
['This', 'is', 'a', 'test', 'short', 'and', 'sweet', 'of', 'split', '']
>>> p.split('This is a test, short and sweet, of split().', 3)
['This', 'is', 'a', 'test, short and sweet, of split().']

Іноді вам не тільки цікаво, що таке текст між роздільниками, а й потрібно знати, що це за роздільник. Якщо в RE використовуються круглі дужки, їх значення також повертаються як частина списку. Порівняйте наступні виклики:

>>> p = re.compile(r'\W+')
>>> p2 = re.compile(r'(\W+)')
>>> p.split('This... is a test.')
['This', 'is', 'a', 'test', '']
>>> p2.split('This... is a test.')
['This', '... ', 'is', ' ', 'a', ' ', 'test', '.', '']

Функція рівня модуля re.split() додає RE, який буде використовуватися як перший аргумент, але в іншому вона така сама.

>>> re.split(r'[\W]+', 'Words, words, words.')
['Words', 'words', 'words', '']
>>> re.split(r'([\W]+)', 'Words, words, words.')
['Words', ', ', 'words', ', ', 'words', '.', '']
>>> re.split(r'[\W]+', 'Words, words, words.', 1)
['Words', 'words, words.']

Пошук і заміна

Ще одне поширене завдання — знайти всі збіги для шаблону та замінити їх іншим рядком. Метод sub() приймає значення заміни, яке може бути або рядком, або функцією, і рядок, який потрібно обробити.

.sub(replacement, string[, count=0])

Повертає рядок, отриманий шляхом заміни крайніх лівих неперекриваючих входжень RE в рядку на заміну replacement. Якщо шаблон не знайдено, рядок повертається без змін.

Необов’язковий аргумент count — це максимальна кількість шаблонів, які потрібно замінити; count має бути невід’ємним цілим числом. Значення за замовчуванням 0 означає заміну всіх входжень.

Ось простий приклад використання методу sub(). Він замінює назви кольорів словом колір:

>>> p = re.compile('(blue|white|red)')
>>> p.sub('colour', 'blue socks and red shoes')
'colour socks and colour shoes'
>>> p.sub('colour', 'blue socks and red shoes', count=1)
'colour socks and red shoes'

Метод subn() виконує ту саму роботу, але повертає 2-кортеж, що містить нове значення рядка та кількість виконаних замін:

>>> p = re.compile('(blue|white|red)')
>>> p.subn('colour', 'blue socks and red shoes')
('colour socks and colour shoes', 2)
>>> p.subn('colour', 'no colours at all')
('no colours at all', 0)

Порожні збіги замінюються лише тоді, коли вони не суміжні з попереднім порожнім збігом.

>>> p = re.compile('x*')
>>> p.sub('-', 'abxd')
'-a-b--d-'

Якщо replacement є рядком, будь-які вихідні символи зворотної косої риски в ньому обробляються. Тобто \n перетворюється на один символ нового рядка, \r перетворюється на повернення каретки і так далі. Невідомі вихідні коди, такі як \& залишаються в спокої. Зворотні посилання, такі як \6, замінюються підрядком, який відповідає відповідній групі в RE. Це дає змогу включати частини вихідного тексту в отриманий рядок заміни.

Цей приклад відповідає слову розділ, за яким слідує рядок, укладений у {, }, і змінює розділ на підрозділ:

>>> p = re.compile('section{ ( [^}]* ) }', re.VERBOSE)
>>> p.sub(r'subsection{\1}','section{First} section{second}')
'subsection{First} subsection{second}'

Існує також синтаксис для посилань на іменовані групи, як визначено синтаксисом (?P <name> ...). \g <name> використовуватиме підрядок, який відповідає групі з назвою name, а \g <number> використовує відповідний номер групи. \g <2>, отже, еквівалентний \2, але не є неоднозначним у рядку заміни, такому як \g <2> 0. (\20 буде інтерпретуватися як посилання на групу 20, а не як посилання на групу 2, за якою йде літеральний символ '0''.) Усі наступні заміни еквівалентні, але використовують усі три варіанти рядок заміни.

>>> p = re.compile('section{ (?P<name> [^}]* ) }', re.VERBOSE)
>>> p.sub(r'subsection{\1}','section{First}')
'subsection{First}'
>>> p.sub(r'subsection{\g<1>}','section{First}')
'subsection{First}'
>>> p.sub(r'subsection{\g<name>}','section{First}')
'subsection{First}'

заміна також може бути функцією, яка дає вам ще більше контролю. Якщо replacement є функцією, функція викликається для кожного неперекриваючого входження pattern. Під час кожного виклику функції передається аргумент match object для відповідності, і ця інформація може використовуватися для обчислення потрібного рядка заміни та повернення його.

У наступному прикладі функція заміни перетворює десяткові числа в шістнадцяткові:

>>> def hexrepl(match):
...     "Return the hex string for a decimal number"
...     value = int(match.group())
...     return hex(value)
...
>>> p = re.compile(r'\d+')
>>> p.sub(hexrepl, 'Call 65490 for printing, 49152 for user code.')
'Call 0xffd2 for printing, 0xc000 for user code.'

Під час використання функції re.sub() на рівні модуля шаблон передається як перший аргумент. Шаблон може бути наданий як об’єкт або як рядок; якщо вам потрібно вказати прапорці регулярного виразу, ви повинні або використовувати об’єкт шаблону як перший параметр, або використати вбудовані модифікатори в рядок шаблону, напр. sub("(?i)b+", "x", "bbbb BBBB") повертає 'x x''.

Загальні проблеми

Регулярні вирази є потужним інструментом для деяких програм, але в певному сенсі їх поведінка не є інтуїтивно зрозумілою, а іноді вони поводяться не так, як ви від них можете очікувати. У цьому розділі буде вказано на деякі з найпоширеніших пасток.

Використовуйте рядкові методи

Іноді використання модуля re є помилкою. Якщо ви зіставляєте фіксований рядок або одиничний клас символів і не використовуєте жодних функцій re, таких як прапор IGNORECASE, тоді повна потужність регулярних виразів може не знадобитися. Рядки мають кілька методів для виконання операцій із фіксованими рядками, і зазвичай вони набагато швидші, оскільки реалізація являє собою один маленький цикл C, оптимізований для цієї мети, замість великого, більш узагальненого механізму регулярних виразів.

Одним із прикладів може бути заміна одного фіксованого рядка іншим; наприклад, ви можете замінити слово на справа. re.sub() здається функцією для цього, але розгляньте метод replace(). Зауважте, що replace() також замінить word всередині слів, перетворивши swordfish на sdeedfish, але наївний RE word також зробив би це. (Щоб уникнути виконання заміни на частинах слів, шаблон має бути \bword\b, щоб вимагати, щоб слово мало межу слова з обох боків. Це виводить роботу за межі replace().)

Інше поширене завдання — видалення кожного входження одного символу з рядка або заміна його іншим окремим символом. Ви можете зробити це за допомогою чогось на зразок re.sub('\n', ' ', S), але translate() здатний виконувати обидва завдання та буде швидшим за будь-який регулярний вираз операція може бути.

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

Жадібний проти нежадібного

Під час повторення регулярного виразу, як у a*, результуюча дія полягає в споживанні якомога більшої частини шаблону. Цей факт часто кусає вас, коли ви намагаєтеся зіставити пару збалансованих розділювачів, таких як кутові дужки, що оточують тег HTML. Наївний шаблон для відповідності одному тегу HTML не працює через жадібний характер .*.

>>> s = '<html><head><title>Title</title>'
>>> len(s)
32
>>> print(re.match('<.*>', s).span())
(0, 32)
>>> print(re.match('<.*>', s).group())
<html><head><title>Title</title>

RE відповідає '<' in '<html>', а .* займає решту рядка. Однак у RE залишилося ще більше, і > не може збігатися в кінці рядка, тому система регулярних виразів має відстежувати символ за символом, поки не знайде відповідність >. Фінальний збіг поширюється від ' <' in '<html> '' до '>'' в ' </title> '', що не те, чого ви хочете.

У цьому випадку рішенням є використання нежадібних кванторів *?, +?, ??, або {m,n}?, які відповідають як якомога менше тексту. У наведеному вище прикладі '>' виконується відразу після перших збігів '<', і коли це не вдається, система пересуває на символ за раз, повторюючи '>' на кожному кроці. Це дає правильний результат:

>>> print(re.match('<.*?>', s).group())
<html>

(Зауважте, що синтаксичний аналіз HTML або XML за допомогою регулярних виразів є болючим. Швидкі та брудні шаблони впораються з типовими випадками, але HTML і XML мають особливі випадки, які порушують очевидний регулярний вираз; коли ви напишете регулярний вираз, обробляє всі можливі випадки, шаблони будуть дуже складними. Для таких завдань використовуйте модуль аналізатора HTML або XML.)

Використання re.VERBOSE

Наразі ви, напевно, помітили, що регулярні вирази є дуже компактним записом, але їх не дуже добре читати. RE помірної складності можуть перетворитися на довгі набори зворотних скісних риск, круглих дужок і метасимволів, що ускладнює їх читання та розуміння.

Для таких RE вказівка прапорця re.VERBOSE під час компіляції регулярного виразу може бути корисною, оскільки це дозволяє форматувати регулярний вираз більш чітко.

Прапор re.VERBOSE має кілька ефектів. Пробіли в регулярному виразі, яких не всередині класу символів, ігноруються. Це означає, що такий вираз, як собака | cat еквівалентний менш читабельному dog|cat, але [a b] все одно відповідатиме символам 'a', 'b' або пробілу. Крім того, ви також можете розмістити коментарі всередині RE; коментарі поширюються від символу # до наступного нового рядка. При використанні з рядками в потрійних лапках це дає змогу форматувати RE більш акуратно:

pat = re.compile(r"""
 \s*                 # Skip leading whitespace
 (?P<header>[^:]+)   # Header name
 \s* :               # Whitespace, and a colon
 (?P<value>.*?)      # The header's value -- *? used to
                     # lose the following trailing whitespace
 \s*$                # Trailing whitespace to end-of-line
""", re.VERBOSE)

Це набагато читабельніше, ніж:

pat = re.compile(r"\s*(?P<header>[^:]+)\s*:(?P<value>.*?)\s*$")

Зворотній зв’язок

Регулярні вирази – це складна тема. Чи допоміг вам цей документ зрозуміти їх? Чи були частини, які були незрозумілими, або проблеми, з якими ви зіткнулися, які не були розглянуті тут? Якщо так, будь ласка, надішліть пропозиції щодо покращення автору.

Найповнішою книгою про регулярні вирази майже напевно є «Опанування регулярних виразів» Джеффрі Фрідла, опублікована O’Reilly. На жаль, він зосереджений виключно на стилях регулярних виразів Perl і Java і взагалі не містить жодного матеріалу Python, тому він не буде корисним як довідник для програмування на Python. (Перше видання охоплювало видалений модуль Python regex, який вам не дуже допоможе.) Подумайте про те, щоб перевірити його у своїй бібліотеці.