Сборник рецептов по регулярным выражениям

Язык регулярных выражений распознает символьные образцы. Типы .NET, поддерживающие регулярные выражения, основаны на регулярных выражениях Perl 5 и обеспечивают функциональность как поиска, так и поиска/замены. Регулярные выражения используются для решения следующих задач: проверка текстового ввода, такого как пароли и телефонные номера; преобразование текстовых данных в более структурированные формы (например, извлечение данных из HTML-страницы с целью их сохранения в базе данных); замена образцов текста в документе (например, только целых слов). В посте раскрываются аспекты применения языка регулярных выражений в виде сборника рецептов.

Рецепты 1

Соответствие номеру карточки или телефонному номеру

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):

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 не станет частью результата.

Проверка сильных паролей

Следующий код проверяет, что пароль состоит минимум из шести символов и включает цифру, символ или знак пунктуации:

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 символов

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:

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 записываются с помощью приведенных выше цифр.

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:

string r = @"(?'dupe'\w+)\W\k'dupe'";

string text = "In the the beginning...";
Console.WriteLine (Regex.Replace (text, r, "${dupe}"));

Вывод:
In the beginning...

Подсчет слов

string r = @"\b(\w|[-'])+\b";

string text = "It's all mumbo-jumbo to me";
Console.WriteLine (Regex.Matches (text, r).Count); // 5

Соответствие GUID

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-разметки – особенно, когда документ может быть сформирован некорректно:

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-разметки:

string html = "<i>By default</i> quantifiers are <i>greedy</i> creatures";

Предположим, что нужно извлечь две фразы, выделенные курсивом. Если мы запустим следующий код:

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> (последняя точка, где все еще обеспечивается совпадение).

Если сделать квантификатор ленивым:

foreach (Match m in Regex.Matches (html, @"<i>.*?</i>"))
	Console.WriteLine (m.Value);

тогда он остановится в первой точке, после которой остаток строки может дать совпадение. Вот результат:
<i>By default</i>
<i>greedy</i>

Разбор текста

Статический метод Regex.Split представляет собой более мощную версию метода string.Split с регулярным выражением, обозначающим образец разделителя. В следующем примере мы разделяем строку, в которой разделителем считается любая цифра:

foreach (string s in Regex.Split ("a5b7c", @"\d"))
	Console.Write (s + " "); // a b c

Результат не содержит сами разделители. Тем не менее, включить разделители можно, поместив выражение внутрь положительного просмотра вперед. Следующий код разбивает строку в верблюжьем стиле на отдельные слова:

foreach (string s in Regex.Split ("oneTwoThree", @"(?=[A-Z])"))
	Console.Write (s + " "); // one Two Three

Получение допустимого имени файла

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 в текст документа, один из них — это использование десятичной символьной ссылки. Для этого десятичный код символа нужно поместить между &# и ;.

string htmlFragment = "© 2007";

string result = Regex.Replace (
	htmlFragment,
	@"[\u0080-\uFFFF]", // для © — \u00A9
	m => @"&#" + (int)m.Value[0] + ";");

Console.WriteLine (result); // &#169; 2007

Обратите внимание, в предлагаемом решении использован метод Replace, принимающий делегат MatchEvaluator, который вызывается для каждого совпадения.

Преобразование символов в строке запроса идентификатора URI

Идентификатор URI представляет собой особым образом сформатированную строку, которая описывает ресурс в Интернете или локальной сети, такой как веб-страница, файл или адрес электронной почты. Элементы, из которых состоит URI, удобно представить через свойства класса URI.


Рис. 1. Свойства класса Uri

Символ # в строке запроса (см. Query на рис. 1) имеет альтернативное представление %23, такая замена требуется, чтобы данный символ в адресной строке не рассматривался как служебный. Пробел представляется в виде %20.

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 из журнала веб-статистики

Это должно использоваться в сочетании с предыдущим рецептом преобразования символов в строке запроса:

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, которая помогает строить, визуализировать регулярные выражения и содержит собственную библиотеку выражений.

Использованный источник

  1. Албахари Д., Албахари Б. C# 7.0. Справочник. Полное описание языка.: Пер. с англ. – СпБ.: ООО «Альфа-книга», 2018. – С. 974-977. (См. главу 26. Регулярные выражения, п. Рецептурный справочник по регулярным выражениям.)