Всем привет. Сегодня в блоге ЛАНИТ на Хабре мы с вами поговорим про такую важную тему, как регулярные выражения. Что это такое, для чего применяется, чем знание этого инструмента работы с данными может помочь инженеру тестирования и как регулярные выражения устроены.

Начнем с определения.
Regexp -> Regular expression -> регулярное выражение – это строчное значение, которое описывает шаблон поиска подстрок в заданной строке.
Для тех читателей, кто не прикасался к тематике языков программирования, поясним. Понятием «подстрока» обозначается любое сочетание символов, входящее в основную строку.
Пример:
Есть основная строка «С добрым утром!».
Примерами подстрок этой основной строки могут быть «бры», «м ут» и «С».
Среди основных случаев использования регулярных выражений встречаются следующие.
Поиск конкретных элементов в большом наборе текста. Например, вы можете выбрать только тот текст, который вам нужен.
Замена одних символов на другие. Можно находить и заменять любой набор символов, добавлять значения, заменять регистр с помощью текстового редактора.
Проверка ввода. Например, вы можете проверить, соответствует ли пароль таким заданным критериям, как сочетание прописных и строчных букв, наличие цифр, знаков препинания и т.д.
Координация действий. Например, вы можете обрабатывать определенные файлы в каталоге только в том случае, если они удовлетворяют заданным критериям, описанным в командной строке.
Какие выгоды нам как инженерам тестирования дает знание и умение читать регулярные выражения?
1. Улучшение практик тест-анализа.
Умение читать регулярные выражения позволит вам сопоставить описанные в них правила ввода, с правилами, описанными в документации, и выявлять потенциальные дефекты еще на стадии работы с документацией.
Пример
Для какого-либо поля в документации есть ограничение, что значение может содержать буквенные и цифровые символы. Но в описанном регулярном выражении вы видите, что в попытке сделать выражение более компактным и читаемым вместо интервалов указано предопределенное множество \w. Но в это множество кроме любых буквенных и цифровых символов входит символ нижнего подчеркивания, что противоречит документации и стоит скорректировать выражение.
2. Улучшение практик тест-дизайна.
Пример: техника “классов эквивалентности”/”эквивалентного разбиения”
Банальный пример, когда среди требований для ввода в поле разрешены символы какой-то языковой версии в верхнем и нижнем регистре. Пусть все той же кириллицы.
Да, по классике мы выделим два класса: символы верхнего и символы нижнего регистров. Но когда дело доходит до комбинирования свойств значений для оптимизации набора тестов, то нередко возникает спорный момент.
А все-таки взять в наборе значений представителей от каждого регистра или сделать значение сочетающее в себе сразу оба регистра?
Особо пытливые (как правило, начинающие специалисты) еще могут сначала сделать проверки регистров по отдельности, а потом еще и вместе в смешанном значении.
Умение понимать регулярные выражения позволит сразу понять, как скомбинировать значения.
Представим, что в регулярном выражении есть указание интервалов [А-Яа-яЁё]. Видя это, мы сразу понимаем, что оба регистра считаются валидными. Таким образом, будет оптимальнее использовать сразу смешанное значение, что в итоге даст нам один тест, а не два, за который мы с вами сразу покроем два класса эквивалентности. И только в случае выявления какого-то дефекта нужно будет проверять, кто из регистров - виновник ситуации.
Проверка валидации спецсимволов
Вот тут мы проговорим один нюанс. Дело в том, что так или иначе проверка символов в строке проходит поочередно для каждого символа. Если мы используем регулярные выражения для валидации значений, то получится, что при первом найденном несоответствии проверка остановится и значение будет считаться невалидным.
Например, у нас есть указание, что для ввода разрешены спецсимволы [@+-], а символы [!#$%^&*()_=<>?{}[\]\\|~"'/:;.,], соответственно запрещены.
Но как раз из-за того, как будет происходить валидация на основе регулярного выражения, мы не сможем в качестве негативного значения ввести в поле !#$%^&*()_=<>?{}[\]\\|~"'/:;., . Якобы из идеи «Если на нем споткнется, то все норм, и негативный тест будет успешен». Валидатор не выдает нам конкретный символ, который определяет как невалидный (в графическом интерфейсе). И может по факту быть так, что, например, из-за какой-то ошибки в выражении все символы до, например, *, не попали в это ограничение. И символ верно определил как невалидный уже только открывающую скобку. Но вы, не зная этого, можете подумать, что у меня все хорошо и спецсимволы правильно валидируются. Но не тут-то было. Поэтому при составлении негативных тестов для проверки валидации спецсимволов стоит делать отдельный тест на каждый символ, относящийся к классу невалидных.
3. Практика диагностики дефектов.
При возможности стоит узнать, какие таблицы символов использует система. В большинстве систем есть таблица символов, в которой сначала идут все строчные буквы, а затем все заглавные (например, abcdef...xyzABCD...). Однако некоторые системы чередуют строчные и прописные буквы (например, aAbBcCdD...yYzZ).
Поэтому если у вас в регулярном выражении в документации указан интервал символов [а-яё], но при этом система принимает как валидные значения и символы верхнего регистра, вы будете знать, на что указать разработчикам в баг-репорте как на область проблемы.
Ну или если в документации в описании задачи указано, что ограничение на ввод символов должно составлять 255 символов, а у вас система не принимает значение, в котором даже 233 символа. В этом случае доступ к регулярным выражениям и умение их читать смогут помочь вам в том, что вам не придется проводить много проверочных действий, чтобы опытным путем выявить, какой по факту лимит у этого поля. Взглянув, например, в ту же документацию но уже в Swagger, вы можете увидеть, что для проблемного поля ввода в спешке разработчик прописал не 255, а 225 символов. И проблема снова найдена.
4. Улучшение практик автоматизации тестирования.
Если даже вы не инженер автоматизации, но имеете навыки автоматизации тестирования, то вы можете использовать регулярные выражения для создания своих механик валидации полученных ответов от сервера при тестировании тех же API запросов. Вы можете использовать их в телах XSD и JSON-Schema файлов. И в инструментах, используемых в том числе для валидации, в том же, например, Python (модели объектов Pydantic). Имплантируя в них регулярные выражения, мы можем делать валидацию данных более детальной там, где это возможно и нужно.
Как видите, штука полезная. Давайте изучать!
Рассмотрим применение регулярных выражений на примере поиска элементов. Даже уже перечисленные ранее подстроки «бры», «м ут» и «С» можно использовать как регулярные выражения. Хоть и очень неэффективные.
Для демонстрации процесса на помощь нам придет язык программирования Python. Заранее успокою многих. Все, что будет в примерах с кодом, я буду стараться излагать языком настолько простым и близким людям, не изучавшим языки программирования, насколько это возможно.
Если же вы уже практикуетесь в программировании и тема регулярных выражений для вас нова, то рекомендую вам повторять за мной.
Ну, поехали.
Создадим переменную main_string со значением 'С добрым утром' и переменную sub_string со значением r'бры'.*
* символ r перед кавычками значения переменной sub_string означает, что это значение будет шаблоном поиска. Нашим регулярным выражением.
main_string = 'С добрым утром'
sub_string = r'бры'
Для работы с регулярными выражениями в языке Python есть встроенная библиотека re.
import re
main_string = 'С добрым утром'
sub_string = r'бры'
В ней есть несколько функций, связанных с анализом и выполнением действий с помощью регулярных выражений. Нам надо убедиться, что фрагмент «бры» встречается в строке «С добрым утром!». Для этого нам подойдет функция search библиотеки re.
Чтобы можно было ее вызвать, напишем конструкцию re.search(). Здесь точка - это так называемый оператор доступа к вложенному инструментарию.
Представьте, что re - это папка с файлами, а search - это файл в ней. И этой конструкцией вы как бы говорите компьютеру: «Из папки re дай мне файл search».
import re
main_string = 'С добрым утром'
sub_string = r'бры'
re.search()
Для того, чтобы эта функция сработала и дала нам желаемое, в нее отправим две наши переменные. Согласно документации библиотеки re, сначала нужно передать sub_string и потом main_string.
Для удобства я «положу» эту функцию в третью переменную search_result. Теперь все, что в итоге вычислит функция search(), можно будет использовать в коде по имени переменной, в которую мы ее положили.
import re
main_string = 'С добрым утром'
sub_string = r'бры'
search_result = re.search(sub_string, main_string)
Ну и остается только вывести то, что найдет нам функция search()
import re
main_string = 'С добрым утром'
sub_string = r'бры'
search_result = re.search(sub_string, main_string)
print(search_result)
Исполнив этот простой код, мы увидим, что итогом его работы стало то, что в переменной search_result теперь хранятся данные о найденных совпадениях, среди которых есть атрибут match со значением «бры». То есть, как видите, все работает, и функция нашла искомое значение в основной строке.
<re.Match object; span=(4, 7), match=’бры’>
Но, как мы уже говорили ранее, использование фиксированных последовательностей символов неэффективно. Ибо работает принцип «Шаг вправо, шаг влево - расстрел». Мы не можем выйти за пределы этого шаблона. А нам нужна гибкость. Ибо, как вы уже поняли, для нас регулярные выражения интересны именно в контексте валидации данных ввода. А правила ввода описываются не одним фиксированным значением или условием, которые надо учесть.
Изменим немного ситуацию. Представим, что нам надо, чтобы какое-то ключевое слово отслеживалось вне зависимости от регистра его первой буквы. Мало ли! Вдруг пользователь случайно прожал ввод символа с шифтом.
Далеко ходить не будем и возьмем для эксперимента все ту же фразу, что и в прошлом примере. И нам надо, чтобы слово «добрым» могло быть найдено. Вне зависимости от того, написано ли оно с большой или с маленькой буквы.
Немного изменим наш код. Теперь у нас есть переменные:
main_string_uppercase = 'С Добрым утром'
– со значением, где слово написано с заглавной буквы.
main_string_lowercase = 'С добрым утром'
- со значением, где слово написано с маленькой буквы.
А вот наш шаблон мы модифицируем так, чтобы он мог найти и слово с заглавной буквы, и с маленькой, не создавая для каждого варианта свой шаблон. Заметьте, первая буква слова заключена в квадратные скобки, и в них есть как прописная, так и строчная буквы.
sub_string = r'[Дд]обрым'
Так как теперь мы обрабатываем два значения, то и переменных с результатами обработки будет две:
search_result_uppercase = re.search(sub_string, main_string_uppercase)
search_result_lowercase = re.search(sub_string, main_string_lowercase)
Каждая из них хранит в себе итоги поиска подстроки для своей переменной по их «суффиксу» (uppercase и lowercase).
И, соответственно, у нас также есть две функции print для каждой из этих переменных.
import re
main_string_uppercase = 'С Добрым утром'
main_string_lowercase = 'С добрым утром'
sub_string = r'[Дд]обрым'
search_result_uppercase = re.search(sub_string, main_string_uppercase)
search_result_lowercase = re.search(sub_string, main_string_lowercase)
print(search_result_uppercase)
print(search_result_lowercase)
Запуск кода теперь выдаст нам две строки, и в каждой вы увидите, что было найдено совпадение с искомым словом вне зависимости от того, в каком регистре была первая буква. Несмотря на то, что шаблон поиска у нас один.
<re.Match object; span=(2, 8), match=’Добрым’>
<re.Match object; span=(2, 8), match=’добрым’>
Одно небольшое изменение, и вот уже первые признаки гибкости.
Это лишь элементарные примеры работы регулярных выражений. Сам язык описания шаблонов гораздо сложнее, и для несведущего человека, написанные на нем конструкции являются воистину кодом уровня машины «Энигма». Причиной тому служит то, что этот язык существует для того, чтобы, реализуя указанные выше назначения, можно было задавать шаблоны поиска и сравнения, опирающиеся не на сколько-то жесткие последовательности символов, как в нашем примере, а на некие правила, которые мы можем задать в выражении.
Например, гибкий шаблон для проверки состава значения в поле ввода электронной почты может выглядеть следующим образом:
(Пример ниже взят из статьи.)
r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$'
Некоторые элементы могут нам быть понятны, но, согласитесь, далеко не все.
В этой статье мы не будем разбирать шаблоны всевозможного уровня сложности. Но обязательно проведем обзор механизмов и конструкций, а также разбор некоторых примеров.
И сначала давайте, как в любом языке, начнем с изучения «алфавита». Сейчас главное - не зацикливайтесь на каждом примере. Просто ознакомьтесь, используйте эти обозначения как справочный материал.
Символы и обозначения, используемые в регулярных выражениях, можно разделить на несколько групп.
Символы для совпадений
Символ | Описание |
. | Совпадает с любым символом, кроме символа новой строки (\n). |
^ | Начало строки. Совпадает, если шаблон находится в начале строки. |
$ | Конец строки. Совпадает, если шаблон находится в конце строки. |
\b | Граница слова. Совпадает с позициями между \w и \W. |
\B | Не граница слова. |
\A | Начало строки (аналогично ^, но не зависит от флага MULTILINE). |
\Z | Конец строки (аналогично $, но перед любым символом новой строки). |
Модификаторы повторений (они же квантификаторы)
Символ | Описание |
* | 0 или более повторений предыдущего символа или группы. |
+ | 1 или более повторений предыдущего символа или группы. |
? | 0 или 1 повторение предыдущего символа или группы (делает необязательным). |
{n} | Ровно n повторений. |
{n,} | n или более повторений. |
{n,m} | От n до m повторений (включительно). |
Ленивые квантификаторы
Символ | Описание |
*? | 0 или более повторений (минимально возможное количество). |
+? | 1 или более повторений (минимально возможное количество). |
?? | 0 или 1 повторение (минимально возможное количество). |
{n,m}? | От n до m повторений (минимально возможное количество). |
Группировка и альтернатива
Символ | Описание |
( ... ) | Группа. Позволяет объединить часть выражения и сохраняет её а память во время выполнения программы. |
(?: ... ) | Негруппирующая скобка (отличается от предыдущей вариации тем, что не сохраняет найденное содержимое в память). |
(?P<name>...) | Именованная группа. Сохраняет содержимое группы под именем name. |
Классы символов
Символ | Описание |
[abc] | Любой символ из множества a, b, c. |
[^abc] | Любой символ, кроме a, b, c. |
[a-z] или [а-я] | Любая строчная буква от a до z. (или от а до я в русской раскладке). |
[A-Z] или [А-Я] | Любая заглавная буква от A до Z. (или от А до Я в русской раскладке). |
[0-9] | Любая цифра от 0 до 9. |
Здесь сделаем важное отступление. Может возникнуть вопрос. Например, зачем нам разделять интервалами верхние и нижние регистры буквенных символов? Почему нельзя написать, например, не А-Яа-я, а сразу А-я или A-z?
Все дело в том, что когда в регулярных выражениях задается диапазон, например, [A-Z] или [a-z], это означает, что мы работаем с последовательностью кодов символов в таблице Unicode (или ASCII, если используется только латиница).
И диапазон [A-z] будет включать в себя не только буквы, но и символы, которые находятся между ними в таблице Unicode. Например, [A-z] покроет такие символы, как [ \ ] ^ _
`. То же самое относится и к работе с кириллическими символами.
По этой же причине в русской раскладке отдельно указываются ё и Ё, так как в таблице Unicode они имеют свои уникальные позиции и не входят в диапазоны [А-Я] или [а-я].
Также есть дополнительные конструкции, которые позволяют сокращать регулярные выражения. Их называют классом предопределенных символов:
Символ | Описание |
\d | Любая цифра ([0-9]). |
\D | Любой нецифровой символ ([^0-9]). |
\w | Любая буква, цифра или ([a-zA-Z0-9]). |
\W | Любой символ, кроме букв, цифр и ([^a-zA-Z0-9]). |
\s | Любой пробельный символ (включая пробел, табуляцию и новую строку). |
\S | Любой символ, кроме пробельного. |
Обратные ссылки
Символ | Описание |
\n | Совпадает с содержимым n-й группы. |
(?P=name) | Совпадает с содержимым именованной группы name. |
Утверждения
Символ | Описание |
(?=...) | Положительный просмотр вперёд. Например, a(?=b) совпадает с "a", за которым следует "b". |
(?!...) | Отрицательный просмотр вперёд. Например, a(?!b) совпадает с "a", за которым НЕ следует "b". |
(?<=...) | Положительный просмотр назад. Например, (?<=a)b совпадает с "b", перед которым "a". |
(?<!...) | Отрицательный просмотр назад. Например, (?<!a)b совпадает с "b", перед которым НЕТ "a". |
Экранирование символов
Символ | Описание |
\ | Экранирует следующий символ. Например, \. совпадает с точкой .. |
Как видите, алфавит непростой. Теперь давайте рассмотрим несколько практических примеров. Чтобы понять, что не так уж и страшны регулярные выражения.
Для первой практики возьмем старую задачку, которую используют иногда на собеседованиях для проверки знаний по применению техник тест-дизайна.
Представим, что у нас имеется простой веб-интерфейс, на котором присутствует только одно поле для ввода фамилии пользователя. Для данного поля действуют следующие правила ввода.
Можно вводить:
Символы кириллицы (верхний и нижний регистр)Нельзя вводить:
* символы латиницы (верхний и нижний регистр);
* спецсимволы;
* цифровые значения.Ограничения ввода по количеству символов:
* поле не может быть пустым;
* вводимое значение по количеству символов не должно превышать 60 символов.
Давайте попробуем описать эти правила в виде регулярного выражения. Это будет несложно.
1. Чтобы сразу выставить границы проверяемой строки, используем соответствующие символы:
r’^$’
2. У нас разрешены символы кириллицы обоих регистров. Добавим их в наш шаблон, используя классы символов:
r’^[А-Яа-яЁё]$’
3. И теперь нам надо внести ограничение по количеству символов для поля. Для этого мы уже обратимся к модификаторам повторений. Они же квантификаторы:
r’^[А-Яа-яЁё]{1,60}$’.
Все. Вот такая вот получилась у нас запись. Как видите, нам не пришлось каким–то еще образом описывать правила негативного ввода. Так как при анализе значения, если оно не соответствует этому шаблону, то символы будут считаться невалидными. И хоть вариант описания тех же правил на основе указания нежелательных символов возможен, применять его будет не так эффективно и удобно.
Такое регулярное выражение будет выглядеть следующим образом:
r’^(?!.*[A-Za-z0-9!@#$%^&*()_+=<>?{}[\]\\|~`"'/:;.,-]).{1,60}$‘
Если разобрать данное выражение, получится следующее:
^ - как мы уже знаем – начало проверяемой строки.
(?!.*[A-Za-z0-9!@#$%^&*()_+=<>?{}[\]\\|~"'/:;.,-])
В этом большом фрагменте, заключив его в круглые скобки, мы использовали группировку.
?! – это комбинация, которая говорит, что мы используем «отрицательный просмотр вперед».
То есть все, что находится далее по строке (от ее начала в нашем случае), мы будем отсеивать как неправильное, если что-то будет подходить под описание указанных символов.
. – говорит, что в строке потенциально может встретиться любой символ для проверки.
* - указывает, что этот самый «любой символ» может встретиться сколько угодно раз, а может не быть вообще.
Само сочетание символов .* можно интерпретировать как "любая последовательность символов (включая пустую строку)".
[A-Za-z0-9!@#$%^&*()_+=<>?{}[\]\\|~"'/:;.,-] – последовательность символов, которые при встрече в значении мы будем отсеивать, проходясь от начала строки до ее конца. Как видим, эта часть условия получилась заметно больше и несколько сложнее в анализе с визуальной точки зрения.
Ну что ж. В шифровании попрактиковались. Теперь давайте попробуем сделать обратную операцию. Переходим к дешифровке.
Для этого сначала также возьмем пример попроще. В одном из запросов в нашем учебном приложении есть поле assigned_to. И для него назначен шаблон в виде регулярного выражения следующего вида:
^[\w.@+-]+$
Что ж, не так страшно. Пока.
С первого же взгляда мы понимаем, что ^ и $ - это символы-ограничители строки. В квадратных скобках нас встречает предопределенная последовательность символов, обозначенная как \w. Что означает, что поле может принимать любую букву, цифру или символ «_» (нижнее подчеркивание).
Далее следует символ точки. Но тут есть нюанс. В прошлом примере вы помните, что этот же символ означал любой символ. Что не совсем логично. Мы только что ограничили символы с помощью \w, а тут зачем-то даем полную свободу. Дело в том, что внутри квадратных скобок символы утрачивают свою, так сказать, власть и силу, становясь просто печатными символами. И, перефразируя классика, иногда точка - это просто точка.
Значит, пока получается, что кроме «любого символа» в значении разрешена точка, символ собачки и символы + и –.
Символ +, стоящий после закрывающей скобки, - это квантификатор, который означает, что символы, указанные в последовательности, могут встречаться в значении один или более раз.
Значением, которое подходит под такой шаблон, может быть, например, никнейм пользователя: AJIex_1988.
Вроде несложно. Замахнемся на зверя покрупнее?
Помните вот этого классного парня?:
r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$'
Что вы так испугались? Не так это и страшно. Тем более вы уже не новички. Так что вперед. Глаза боятся, руки делают.
Про базовые символы уже не говорим, с ними все ясно.
Последовательность [a-zA-Z0-9_.+-]. По ней мы понимаем, что в значении могут встречаться символы латиницы обоих регистров, цифровые символы, нижние подчеркивания, точка и символы + и -.
Символ + после первой последовательности говорит, что обозначенные в ней символы могут повторяться от одного раза и более.
Символ @ не фигурировал ни в одной группе условных обозначений синтаксиса регулярных выражений. А значит, его тут как раз стоит рассматривать, как фиксированный символ.
То есть мы уже с вами понимаем, что первая последовательность и знак + после нее показали нам, какие правила есть для части адреса, в которой пишется имя почтового ящика.
Тут все ясно, идем дальше.
Последовательность [a-zA-Z0-9-]. Создает нам очень похожее ограничение, как и в первой последовательности. За тем исключением, что в этом месте нам нельзя вводить нижние подчеркивания, точку и символ +. Что вполне логично для фрагмента, где описывается имя почтового сервиса.
Символу + после последовательности уже не уделяем внимание: знаем, что за зверь.
А вот дальше давайте разберемся. Тут интереснее. Мы знаем, что после имени почтового сервиса ставится точка и указывается имя или регионального, или специального домена (например, .com). Но почему же в нашем примере опять нет простой последовательности?
Итак, мы имеем следующую запись: (?:\.[a-zA-Z0-9-]+).
Приступаем к обследованию.
Видим скобки – явно применена группировка. В начале группы стоит сочетание символов ?: . Что указывает, что используется негруппирующая скобка, которая не сохраняет содержимое. Не нагружайте себе этим особо голову. Это понятие больше нужно для разработчиков, так как речь идет о сохранении группировки в памяти программы для возможности дальнейшего использования. Нас интересует не переиспользование этой группировки, а умение ее прочитать.
Продолжаем.
Сочетание \. это как раз экранированная точка. Без слэша она опять же будет читаться как «любой символ», так как находится вне квадратных скобок. Поэтому, чтобы лишить ее суперсил, мы используем слэш. И теперь она воспринимается как обычная печатная точка. Вот и нашелся наш разделитель имени почтового сервиса от имени домена.
И собственно все, народ. Разбор окончен. Ибо если посмотреть на последовательность после точки, то мы увидим, что она в целом копирует вторую и задает ей уже известные нам ограничения.
Видите, все не так сложно. А страху-то было.
Что ж. Мы с вами сегодня проделали большую работу. Познакомились с регулярными выражениями, с их основами синтаксиса и формирующими конструкциями и элементами. На практике убедились, что не так страшен черт, как его малюют. И наметили некоторые выгоды от их знания для нас как для инженеров тестирования. Всем успехов. Надеюсь, вам было интересно, не скучно и полезно. До новых встреч.