outcoldman
outcoldman Denis Gladkikh

Регулярные выражения. Вспоминаем, пишем, тестируем.

.NET, C#, Twitter, TDD, Regex, Regular Expressions, and nUnit

Признаюсь, я фанат регулярных выражений. Всегда, когда я вижу задачу, которую можно решить при помощи RegEx, я загораюсь и бегу писать тест под новенькое Regex условие. Раньше даже специально держал установленный SharpDeveloper, так как там была удобная тулза для проверки RegEx выражений, сейчас же я немного поумнел и для каждого RegEx пишу просто отдельный тест и в нем же и тестирую. Вообще, нужно стараться находить те задачи, которые предназначены для решения их через регулярные выражения. Мне сложно помнить синтаксис регулярных выражений, точнее приходится их писать не так уж и часто, потому из головы постоянно вылетает: какой символ отвечает за начало строки и т.п. Для освежения я постоянно пользуюсь очень легкой статьей Регулярные выражения на RSDN.

Несколько примеров

Определение, что текст русский

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

const string RegexIsRussian = @"[А-Яа-я]+"

Можно, конечно, данную задачу решить проходом по всей строке и просмотром вхождения кода символа в русский диапазон. Тут же проверка очень простая – хотя бы один русский символ. Использовать этот Regex теперь очень просто в коде:

Regex.IsMatch(text, RegexIsRussian)

Regex – класс из пространства имен System.Text.RegularExpressions.

Нахождение и замена ссылки

Предыдущий пример очень прост, давайте попробуем посмотреть пример посложнее, в строке необходимо найти http ссылку. Делать это в лоб, простым разборов строки, уже будет занятие неблагодарное, а в regex пишется достаточно красиво:

const string RegexUrl = @"(https?://(www.)?([\w\-]+)(\.([\w\-]+))+([\w\\\/\.?&%=\-+]*))"

И опять же, мы теперь можем искать ссылки при помощи конструкции Regex.IsMatch, но можно и поинтереснее. Ссылки я искал не просто так, а для того чтобы их заменять в обычном тексте на anchor – html ссылки, потому, при помощи такого выражения и следующей конструкции, сделать это очень просто

Regex.Replace(text, RegexUrl, "<a href='$1'>$1</a>");

Вместо $1 будут подставляться найденные ссылки (то что в первых скобках).

Таким же способом мы можем заменить пользователей и хештеги на ссылки:

const string RegexTwitterUser = @"(@([A-Za-z0-9_]+))";
Regex.Replace(text, RegexTwitterUser, "<a href='http://twitter.com/$2'>$1</a>")
const string RegexTwitterTag = @"(#([A-Za-z0-9_]+))";
Regex.Replace(text, RegexTwitterTag, "<a href='http://twitter.com/#search?q=%23$2'>$1</a>");

Во втором примере мы уже используем как $1 так и $2, где $2 – это то что во вторых скобках, а точнее имя пользователя без #.

Вообще, примеров еще может быть огромное количество, особенно часто Regex всплывают в валидации, при помощи них и встроенных средств ASP.NET или других технологий можно запросто написать валидатор на какое либо вводимое поле – будь то email, телефон, индекс, дата или еще что-нибудь. Благо готовых решений таких стандартных Regex для валидаторов огромное количество, главное не лениться искать в поисковиках.

Тестирование Regex

Я тестирую Regex при помощи комбинаторных тестов (тестов с параметрами, значения которых подставляются при помощи источников данных). Так в nunit (сейчас использую его для своего сайта, так как MbUnit c Gallio и R#5 не смог подружить в VS2010), например, чтобы тестировать замену ссылок я написал такой тест:

public string[][] Urls
{
    get
    {        return new[]
                   {                       new[] {"Hello http://google.com bye", "Hello <a href='http://google.com'>http://google.com</a> bye"},
                       new[]
                           {                               "Hello http://mail.google.com bye",
                               "Hello <a href='http://mail.google.com'>http://mail.google.com</a> bye"
                           },                       new[]
                           {                               "Hello http://google.com/test/test.aspx",
                               "Hello <a href='http://google.com/test/test.aspx'>http://google.com/test/test.aspx</a>"
                           },                       new[]
                           {                               "Hello http://g0ogle.com/test/test.aspx?q=7&b=0",
                               "Hello <a href='http://g0ogle.com/test/test.aspx?q=7&b=0'>http://g0ogle.com/test/test.aspx?q=7&b=0</a>"
                           }
                   };
    }
}
 
[Test]public void twitter_text_to_html_convert([ValueSource("Urls")] string[] url)
{    string replace = HtmlParser.ReplaceHref(url[0]);
    Assert.AreEqual(url[1], replace);
}

Как вы видите тест на самом деле очень маленький – это всего лишь метод twitter_text_to_html_convert. А вот источник Urls я постоянно добавляю. Метод twitter_text_to_html_convert выполнится столько раз, сколько данных в источнике.

Недавно я узнал что, оказывается, у меня не учитывалось что в url может быть тире, потому я дописал свой regex и просто добавил еще одну пару в Urls:

new[]
    {        "Hello http://ninja-assassin-movie.warnerbros.com",
        "Hello <a href='http://ninja-assassin-movie.warnerbros.com'>http://ninja-assassin-movie.warnerbros.com</a>"
    },

Так я был уверен, что моя добавленная работа в regex не порушило то что было, и новый regex так же отрабатывает url с новым условием.

P.S. На свой сайт положил тулзу для быстрой проверки regex в вебе https://www.outcoldman.com/ru/tools/regex

Have feedback or questions? Looking for consultation?

My expertise: MongoDB, ElasticSearch, Splunk, and other databases. Docker, Kubernetes. Logging, Metrics. Performance, memory leaks.

Send me an email to public@denis.gladkikh.email.

The content on this site represents my own personal opinions and thoughts at the time of posting.

Content licensed under the Creative Commons CC BY 4.0.