Сборник рецептов по регулярным выражениям
Язык регулярных выражений распознает символьные образцы. Типы .NET, поддерживающие регулярные выражения, основаны на регулярных выражениях Perl 5 и обеспечивают функциональность как поиска, так и поиска/замены. Регулярные выражения используются для решения следующих задач: проверка текстового ввода, такого как пароли и телефонные номера; преобразование текстовых данных в более структурированные формы (например, извлечение данных из HTML-страницы с целью их сохранения в базе данных); замена образцов текста в документе (например, только целых слов). В посте раскрываются аспекты применения языка регулярных выражений в виде сборника рецептов.
Рецепты 1
Соответствие номеру карточки или телефонному номеру
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
string ssNum = @"\b\d{3}-\d{2}-\d{4}\b";
Console.WriteLine (Regex.IsMatch ("123-45-6789", ssNum)); // True
string phone = @"(?x)
\b\d\s?
( \d{3}\s? | \(\d{3}\)\s? )
\d{3}[-\s]?
\d{2}[-\s]?
\d{2}\b";
Console.WriteLine (Regex.IsMatch ("8(123)456-78-90", phone)); // True
Console.WriteLine(Regex.IsMatch("81234567890", phone)); // True
Console.WriteLine(Regex.IsMatch("8 (123) 456-78-90", phone)); // True
Console.WriteLine(Regex.IsMatch("8 123 456-78-90", phone)); // True
Console.WriteLine(Regex.IsMatch("8 123 456 78 90", phone)); // True
Извлечение пар “имя = значение”
Обратите внимание на использование в начале директивы (?m)
:
1
2
3
4
5
6
7
8
9
string r = @"(?m)^\s*(?'name'\w+)\s*=\s*(?'value'.+)\s*(?=\r?$)";
string text =
@"id = 3
secure = true
timeout = 30";
foreach (Match m in Regex.Matches (text, r))
Console.WriteLine (m.Groups["name"] + " is " + m.Groups["value"]);
Вывод:
id is 3
secure is true
timeout is 30
Если указать RegexOptions.Multiline
или включить в выражение конструкцию (?m)
, то:
- символ
^
соответствует началу всей строки или строки текста (сразу после\n
); - символ
$
соответствует концу всей строки или строки текста (непосредственно перед\n
).
С использованием символа $
в многострочном (Multiline) режиме связана одна загвоздка: новая строка в Windows почти всегда обозначается с помощью комбинации \r\n
, а не просто \n
. Это значит, что в случае $
обычно придется искать совпадение также и с \r
, применяя положительный просмотр вперед: (?=\r?$)
. Положительный просмотр вперед гарантирует, что \r
не станет частью результата.
Проверка сильных паролей
Следующий код проверяет, что пароль состоит минимум из шести символов и включает цифру, символ или знак пунктуации:
1
2
3
4
5
6
7
8
9
string r =
@"(?x)" + // Ignore spaces within regex expression, for readability
@"^" + // Anchor at start of string
@"(?=.* ( \d | \p{P} | \p{S} ))" + // String must contain a digit or punctuation char or symbol
@".{6,}"; // String must be at least 6 characters in length
Console.WriteLine (Regex.IsMatch ("abc12", r)); // False
Console.WriteLine (Regex.IsMatch ("abcdef", r)); // False
Console.WriteLine (Regex.IsMatch ("ab88yz", r)); // True
Строки текста, содержащие, по крайней мере, 80 символов
1
2
3
4
5
6
7
8
string r = @"(?m)^.{80,}(?=\r?$)";
string fifty = new string ('x', 50);
string eighty = new string ('x', 80);
string text = eighty + "\r\n" + fifty + "\r\n" + eighty;
Console.WriteLine (Regex.Matches (text, r).Count); // 2
Разбор даты/времени
Показанное ниже выражение поддерживает разнообразные числовые форматы дат и работает независимо от того, где указан год – в начале или в конце. Директива (?х)
улучшает читабельность, разрешая применение пробельных символов; директива (?i)
отключает чувствительность к регистру символов (для необязательного указателя АМ/РМ). Затем к компонентам совпадения можно обращаться через коллекцию Groups
:
1
2
3
4
5
6
7
8
9
string r = @"(?x)(?i)
(\d{1,4}) [./-]
(\d{1,2}) [./-]
(\d{1,4}) [\sT] (\d+):(\d+):(\d+) \s? (A\.?M\.?|P\.?M\.?)?";
string text = "01/02/2008 5:20:50 PM";
foreach (Group g in Regex.Match (text, r).Groups)
Console.Write (g.Value + " ");
Вывод:
01/02/2008 5:20:50 PM 01 02 2008 5 20 50 PM
(Разумеется, выражение не проверяет корректность даты/времени.)
Соответствие римским числам
В римской системе счисления используются следующие знаки:
I = 1; V = 5; X = 10; L = 50; C = 100; D = 500; M = 1000.
Все целые числа от 1 до 3999 записываются с помощью приведенных выше цифр.
1
2
3
4
5
6
7
8
string r =
@"(?i)\bm*" +
@"(d?c{0,3}|c[dm])" +
@"(l?x{0,3}|x[lc])" +
@"(v?i{0,3}|i[vx])" +
@"\b";
Console.WriteLine (Regex.IsMatch ("MCMLXXXIV", r)); // True
Удаление повторяющихся слов
Здесь мы захватываем именованную группу dupe:
1
2
3
4
string r = @"(?'dupe'\w+)\W\k'dupe'";
string text = "In the the beginning...";
Console.WriteLine (Regex.Replace (text, r, "${dupe}"));
Вывод:
In the beginning...
Подсчет слов
1
2
3
4
string r = @"\b(\w|[-'])+\b";
string text = "It's all mumbo-jumbo to me";
Console.WriteLine (Regex.Matches (text, r).Count); // 5
Соответствие GUID
1
2
3
4
5
6
7
8
9
10
11
string r =
@"(?i)\b" +
@"[0-9a-f]{8}\-" +
@"[0-9a-f]{4}\-" +
@"[0-9a-f]{4}\-" +
@"[0-9a-f]{4}\-" +
@"[0-9a-f]{12}" +
@"\b";
string text = "Its key is {3F2504E0-4F89-11D3-9A0C-0305E82C3301}.";
Console.WriteLine (Regex.Match (text, r).Index); // 12
GUID (Globally Unique Identifier) – статистически уникальный 128-битный идентификатор. Его главная особенность – уникальность, которая позволяет создавать расширяемые сервисы и приложения без опасения конфликтов, вызванных совпадением идентификаторов.
Разбор дескриптора XML/HTML
Класс Regex
удобен при разборе фрагментов HTML-разметки – особенно, когда документ может быть сформирован некорректно:
1
2
3
4
5
6
7
8
9
10
11
string r =
@"<(?'tag'\w+?).*>" + // match first tag, and name it 'tag'
@"(?'text'.*?)" + // match text content, name it 'text'
@"</\k'tag'>"; // match last tag, denoted by 'tag'
string text = "<h1>hello</h1>";
Match m = Regex.Match (text, r);
Console.WriteLine (m.Groups ["tag"].Value); // h1
Console.WriteLine (m.Groups ["text"].Value); // hello
Обратите внимание на наличие символа ?
после квантификатора *
. Дело в том, что по умолчанию квантификаторы являются жадными как противоположность ленивым квантификаторам. Жадный квантификатор повторяется настолько много раз, сколько может, прежде чем продолжить. Ленивый квантификаторы повторяется настолько мало раз, сколько может, прежде чем продолжить. Для того чтобы сделать любой квантификатор ленивым, его необходимо снабдить суффиксом в виде символа ?
.
Чтобы проиллюстрировать разницу, рассмотрим следующий фрагмент HTML-разметки:
1
string html = "<i>By default</i> quantifiers are <i>greedy</i> creatures";
Предположим, что нужно извлечь две фразы, выделенные курсивом. Если мы запустим следующий код:
1
2
foreach (Match m in Regex.Matches (html, @"<i>.*</i>"))
Console.WriteLine (m.Value);
то результатом будет не два, а одно совпадение:
<i>By default</i> quantifiers are <i>greedy</i>
Проблема в том, что квантификатор *
жадным образом повторяется настолько много раз, сколько может, перед обнаружением соответствия </i>
. Таким образом, он поглощает первое вхождение </i>
, останавливаясь только на финальном вхождении </i>
(последняя точка, где все еще обеспечивается совпадение).
Если сделать квантификатор ленивым:
1
2
foreach (Match m in Regex.Matches (html, @"<i>.*?</i>"))
Console.WriteLine (m.Value);
тогда он остановится в первой точке, после которой остаток строки может дать совпадение. Вот результат:
<i>By default</i>
<i>greedy</i>
Разбор текста
Статический метод Regex.Split
представляет собой более мощную версию метода string.Split
с регулярным выражением, обозначающим образец разделителя. В следующем примере мы разделяем строку, в которой разделителем считается любая цифра:
1
2
foreach (string s in Regex.Split ("a5b7c", @"\d"))
Console.Write (s + " "); // a b c
Результат не содержит сами разделители. Тем не менее, включить разделители можно, поместив выражение внутрь положительного просмотра вперед. Следующий код разбивает строку в верблюжьем стиле на отдельные слова:
1
2
foreach (string s in Regex.Split ("oneTwoThree", @"(?=[A-Z])"))
Console.Write (s + " "); // one Two Three
Получение допустимого имени файла
1
2
3
4
5
6
7
string input = "My \"good\" <recipes>.txt";
char[] invalidChars = System.IO.Path.GetInvalidPathChars();
string invalidString = Regex.Escape (new string (invalidChars));
string valid = Regex.Replace (input, "[" + invalidString + "]", "");
Console.WriteLine (valid); // My good recipes.txt
Чтобы применить метасимвол литерально, его требуется предварить обратной косой чертой. Метод Escape
класса Regex
преобразуют строку, содержащую метасимволы регулярных выражений, путем замены их отмененными эквивалентами. В данном рецепте метод Escape
представлен из академических соображений, на самом деле он избыточен, так как метасимволы, находящиеся внутри набора (в квадратных скобках), интерпретируются литеральным образом.
Представление символов Unicode в HTML
В HTML есть несколько способов включить символ Unicode в текст документа, один из них — это использование десятичной символьной ссылки. Для этого десятичный код символа нужно поместить между &#
и ;
.
1
2
3
4
5
6
7
8
string htmlFragment = "© 2007";
string result = Regex.Replace (
htmlFragment,
@"[\u0080-\uFFFF]", // для © — \u00A9
m => @"&#" + (int)m.Value[0] + ";");
Console.WriteLine (result); // © 2007
Обратите внимание, в предлагаемом решении использован метод Replace
, принимающий делегат MatchEvaluator
, который вызывается для каждого совпадения.
Преобразование символов в строке запроса идентификатора URI
Идентификатор URI представляет собой особым образом сформатированную строку, которая описывает ресурс в Интернете или локальной сети, такой как веб-страница, файл или адрес электронной почты. Элементы, из которых состоит URI, удобно представить через свойства класса URI.
Символ #
в строке запроса (см. Query
на рис.) имеет альтернативное представление %23
, такая замена требуется, чтобы данный символ в адресной строке не рассматривался как служебный. Пробел представляется в виде %20
.
1
2
3
4
5
6
7
8
9
10
string sample = "C%23%20programming%20language";
string result = Regex.Replace (
sample,
@"%[0-9a-f][0-9a-f]",
m => ((char) Convert.ToByte (m.Value.Substring (1), 16)).ToString(),
RegexOptions.IgnoreCase
);
Console.WriteLine (result); // C# programming language
Разбор поисковых терминов Google из журнала веб-статистики
Это должно использоваться в сочетании с предыдущим рецептом преобразования символов в строке запроса:
1
2
3
4
5
6
7
8
string sample = "http://www.google.com/search?hl=en&q=greedy+quantifiers+regex&btnG=Search";
Match m = Regex.Match (sample, @"(?<=google\..+search\?.*q=).+?(?=(&|$))");
string[] keywords = m.Value.Split (new[] { '+' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var keyword in keywords)
Console.Write(keyword + " "); // greedy quantifiers regex
Более подробные сведения о регулярных выражениях можно найти на веб-сайте regular-expressions.info, который содержит удобный онлайновый справочник с множеством примеров. Также доступна интерактивная утилита под названием Expresso, которая помогает строить, визуализировать регулярные выражения и содержит собственную библиотеку выражений.
Использованный источник
Албахари Д., Албахари Б. C# 7.0. Справочник. Полное описание языка.: Пер. с англ. – СпБ.: ООО «Альфа-книга», 2018. – С. 974-977. (См. главу 26. Регулярные выражения, п. Рецептурный справочник по регулярным выражениям.) ↩︎