SRE : Упражнение

Регулярные выражения : Упражнения



Разберём несколько примеров регулярных выражений.

IP-адрес

IP-адрес имеет вид 192.168.30.90, каждое число от 0 до 255 разделённые точкой. В качестве отдельно взятого символа числа можно использовать один из трёх вариантов: диапазон [0-9], метасимвол \d или символьный класс posix [:digit:]. Мне нравится самая краткая запись, в данном случае метасимвол \d.
Просто заменяем каждый символ искомого текста подстановочными метасимволами. Точка является метасимволом, поэтому экранируем её обратным слешем. Итак:

\d\d\d\.\d\d\d\.\d\d\.\d\d

Учитывая, что числа могут быть однозначными, двузначными и трёхзначными, то необходимо использовать повторители. Метасимвол "+" использовать нельзя, поскольку он может захватить числа четырёхзначные и более. Остаётся точно указать диапазон количества повторяемых чисел, в данном случае от 1 до 3-х.

\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}

Глядя на результат, видно что часть шаблона имеет повторяющийся участки текста, точнее \d{1,3}\. повторяется 3 раза и завершается числом без точки \d{1,3}. Возьмём повторяющуюся часть в группу и к группе применим трёхкратный повторитель {3}.

(?:\d{1,3}\.){3}\d{1,3}

Вставим в регулярное выражение пробелы, разделив на блоки, чтобы легче прочитать и добавим флаг (?x) для игнорирования пробелов в регулярном выражении. Конструкция (?:...) исключает возвращение группы в результат, то есть только целиком всю строку.

(?x)   (?:   \d{1,3}\.   )   {3}   \d{1,3}

Готовый пример

$sText = 'IP = 192.168.30.90'
$aRes = StringRegExp($sText, '(?:\d{1,3}\.){3}\d{1,3}', 1)
If Not @error Then MsgBox(0, 'Сообщение', $aRes[0])

Но данный вариант не совершенный, он позволяет захватывать числа 999. Чтобы сделать регулярное выражение более избирательным, необходимо усложнить его. Ниже указанный пример лишён этих недостатков.

(?:(?:2(?:[0-4]\d|5[0-5])|1?\d{1,2})\.){3}(?:(?:2(?:[0-4]\d|5[0-5])|1?\d{1,2}))

Чтобы разобраться попробуйте между группами вставить пробелы и вы увидите, что он похож на свой упрощённый вариант. В нём так же есть трёхкратный повторитель {3} и последняя часть похожа на первую половину, только без точки.

Попробуем разобрать только усложнённую часть регулярного выражения

(?:2   (?:   [0-4]\d   |   5[0-5]   )     |     1?\d{1,2}   )

Левая часть группы начинающаяся с цифры 2 и заканчивающаяся вложенной группой с выбором либо двузначным числом от 00 до 49, определяемое шаблоном [0-4]\d, либо от 50 до 55, определяемое шаблоном 5[0-5] и правая часть группы 1?\d{1,2} в которой перед любым числом от 0 до 99, определяемое шаблоном \d{1,2} может находится число 1, определяемое шаблоном 1?. Левая часть группы разрешает числа от 200 до 255, а правая часть группы разрешает числа от 0 до 199. В итоге данный шаблон разрешает числа только в диапазоне от 0 до 255.




Путь к файлу

Попробуем с помощью регулярного выражения разложить путь на составляющие. Выделим 3 основные составляющие пути C:\Folder\Text.txt, это путь к папке в которой находится файл, имя файла и расширение. Дополнительно элементы пути, которые также иногда необходимо получить: диск, имя папки, в которой находится файл. Особенности: расширение и имя папки, в которой находится файл могут отсутствовать в пути.
Упрощённый вариант:

(.*)\\(.*)\.(.*)

Передаваемая строка по условию является только путь, а не путь внутри текста, поэтому начало строки совпадает с началом пути, а конец строки с концом пути.
Первая группа (.*) захватывает начало пути и является жадным захватом. Метасимвол точка "." определяет любой символ, а метасимвол звёздочка "*" повторяет предыдущий символ 0 и более раз, поэтому конструкция ".*" определяет строку из любых символов (если быть точным, кроме переноса строки, но и его можно включить в захват флагом (?s)). Но так как по условию после этого набора символов следует "\", то захват символов остановится перед последним встретившимся на пути захвата символом, если быть точным, то перед последним символом, который позволит совпасть оставшейся части шаблона. В отличии от жадного захвате есть не жадный (.*?), который остановит захват перед первым символом "\", если быть точным, то перед первым символом, который позволит совпасть оставшейся части шаблона, а также есть сверхжадный захват, который захватит все символы определяемые выражением не переживая, что шаблон может вовсе не совпасть, обычно такой набор не определяют символом "." точка, который захватит всё и вся. Вторая группа захватывает имя и аналогично первой группе является жадным захватом, но в отличии от неё конец имени является точка, после которой выполняется захват третьей группы - расширение файла.
Недостаток этого регулярного выражения в том, что оно требует наличие расширения файла и позволяет недопустимые символы для пути. Хотя это не обязательно недостаток, в некоторых случаях предоставляемый путь может оказаться правильным и обязательно с расширением, а мы в данном случае проверяем не валидность пути, а выполняем разделение на составляющие, а это разные задачи. Поэтому в рамках текущей задачи будем считать, что задача решена.

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

^(.*\\)([^\\]+?)(\.[^.]+)?$

Приведём его к легко читаемому виду из трёх групп

(?x)   ^   (.*\\)   ([^\\]+?)   (\.[^.]+)?   $

В данном регулярном выражении добавились символы начала и конца строки, ^ и $ соответственно. Это принуждает шаблон искать такое решение, при котором вся строка будет соответствовать шаблону, а не её часть. Символы "\" и "." теперь находятся внутри своих групп. Шаблон имени файла [^\\]+? сильно изменился. Теперь он не допускает символа "\" внутри имени, ведь жадность предыдущей группы не исключает такой возможности. Также к имени применён повторитель "+", который принуждает иметь в имени хотя бы один символ и этим исключает комбинацию "\.ext" расширение без имени, создать такой файл невозможно. Шаблон расширения (\.[^.]+)? читается как "точка, за которой следует любой символ кроме точки, с числом повтора 1 или более раз". К группе шаблона расширения применён квантификатор "?", который означает что расширение может отсутствовать.
Недостаток этого регулярного выражения в том, что в случае отсутствия расширения возвращается 2 группы, а при наличии расширения 3 группы. Если бы возвращалось всегда три группы, то не требуется проверки размера массива, чтобы объединить элементы массива обратно в строку.

Вышеуказанное регулярное выражение работает с относительными путями, то есть успешно захватит элементы из такого пути "\Text.txt". Если такое поведение не устраивает, то в качестве начала строки жёстко задаём формат буквы диска:

(?i)^([a-z]:.*\\)([^\\]+?)(\.[^.]+)?$

Здесь флаг (?i) отключает зависимость от регистра, поэтому в качестве буквы диска достаточно указать диапазон в нижнем (или верхнем) регистре [a-z], в противном случае потребовалось бы [A-Za-z]. Диапазон [a-z] определяет букву диска от "a" до "z", после которого двоеточие.
Существуют символы, которые запрещено использовать в качестве имени файлов \/:*?"<>|. Если ввод пути осуществляется пользователем вручную в поле ввода, то можно вместо конструкции .+ использовать [^\/:*?"<>|]+ этим заранее определить валидность символов в имени файла.

#include <Array.au3>
$sText = 'C:\Folder\Text.txt'
$aRes = StringRegExp($sText, '^(.*\\)([^\\]*?)(\.[^.]+)?$', 3)
_ArrayDisplay($aRes, 'aRes')