Пишемо регулярні вирази, не знаючи регулярних виразів. Частина 1

  1. Частина 1.
  2. Частина 2

Деякі люди, зустрівшись із проблемою, думають: “я знаю, я використаю регулярні вирази”. Тепер у них є дві проблеми.

~Джеймі Завінський

Регулярні вирази – це мова опису тестових шаблонів. Регулярні вирази – або в народі “регулярки” – багатьма вважаються складними, незрозумілими, нечитабельними і загалом – проблемними. І варто сказати, що цілком заслужено. Їх складно написати, а більш-менш велику регулярку практично неможливо прочитати, якщо ви, звісно, людина.

В чому проблема регулярок?

Давайте нормальною мовою програмування, тобто Swift-ом, напишемо перевірку того, що рядок починається із пробілу.

let string = " hello"

if CharacterSet.whitespaces.contains(string[0]) {
    // ...
}

Це лінійний та зрозумілий шматочок коду. Тепер давайте опишемо цю ж логіку мовою регулярних виразів:

^\h

Що ми бачимо? 50 символів на Swift виражають стільки ж інформації, скільки 3 символи в регулярці. Тобто, інформація в регулярних виразах подається у приблизно 16 разів більш щільній формі, аніж в нормальній мові програмування. Людський мозок просто не створений для сприйняття настільки щільної інформації.

Більше того, Swift так чи інакше схожий на мову нормальних людей: інструкції, назви змінних та типів виглядають як нормальні слова англійською мовою, а гарно написаний код читається як речення англійською мовою. У той же час регулярні вирази більше нагадують мову R2D2, аніж людську.

4dcutv

Однак, це не єдина проблема регулярок. Давайте створимо регулярний вираз в Xcode і подивимось, який він має вигляд: regexp-xcode

Зверніть увагу, як Xcode красиво і структуровано розфарбував різні сутності в коді різними кольорами. Підсвітка синтаксису дуже сильно допомагає сприймати мову програмування як, власне, мову. Однак, Xcode абсолютно ігнорує регулярний вираз, адже для нього це звичайний рядковий літерал. Хоча для нас це не звичайний рядок, це вставки конструкції іншої мови програмування. Просто уявіть собі, якби наша улюблена IDE не ігнорувала регулярні вирази:

regexp-xcode-highlighted

Тоді регулярні вирази виглядали би набагато більш структурованими, а розбиття чогось великого і незрозумілого на маленькі й зрозумілі частинки – це і є запорука розуміння. Однак, Xcode ігнорує регулярки не лише в плані підсвітки синтаксису, а ще й у плані перевірки їх коректності. Просто уявіть собі таку картину:

regexp-xcode-highlighted-corrected

Виглядає занадто круто, щоб бути правдою, чи не так? Xcode відображає помилки в коді мовою Swift по місцю їх виникнення, що дозволяє швидко їх виправляти. Однак регулярні вирази в цьому плані залишаються в абсолютно нерівних умовах порівняно з іншими мовами програмування.

Разом з усим цим, скільки не покращуй інструменти, а регулярні вирази однаково залишатимуться проблемними. Просто за своєю природою регулярні вирази є дещо езотеричною мовою програмування. Ось наприклад, так виглядає валідація email згідно зі стандартом RFC822:

(?:(?:\r\n)?[ \t])*(?:(?:(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*|(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)*\<(?:(?:\r\n)?[ \t])*(?:@(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*)*:(?:(?:\r\n)?[ \t])*)?(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*\>(?:(?:\r\n)?[ \t])*)|(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)*:(?:(?:\r\n)?[ \t])*(?:(?:(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*|(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)*\<(?:(?:\r\n)?[ \t])*(?:@(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*)*:(?:(?:\r\n)?[ \t])*)?(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*\>(?:(?:\r\n)?[ \t])*)(?:,\s*(?:(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*|(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)*\<(?:(?:\r\n)?[ \t])*(?:@(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*)*:(?:(?:\r\n)?[ \t])*)?(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*\>(?:(?:\r\n)?[ \t])*))*)?;\s*)

Страшно, чи не так? Регулярні вирази у більшості варіацій не допускають ні форматування власного коду, ні коментарів, тому вони доволі погано масштабуються.

Пишемо мовою, яку не знаємо

Незважаючи на всі вади регулярних виразів, вони присутні у нас в iOS SDK, і вони там реалізовані не тому, що Apple нас ненавидить. Їх було створено для того, щоб робити життя розробника легшим: їхня виразність може перетворити десятки заплутаних рядків коду на пару рядків лінійного коду, та зекономити чимало часу розробникам. Тому сьогодні ми дамо їм шанс.

Але якщо регулярки такі проблемні та складні, то як же їх писати, не розбираючись у них? Як взагалі можна користуватись мовою, нічого про неї не знаючи?

parsiltang

Насправді, можна. Більше того, ми це робими доволі часто, мало не щодня. Ось, наприклад, у скількох із нас у проєктах є конфіг для CocoaPods чи fastlane? Я думаю, у багатьох. А скільки із нас знає Ruby? Думаю, що не так багато. Але ж конфіги для CocoaPods та fastlane пишуться на Ruby, чи не так? Насправді, ми дуже часто пишемо код мовами, які ми дуже слабко знаємо, і це нормально.

Для прикладу, розглянемо ось такий простий Podfile:

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '6.0'
xcodeproj 'MyProject'

pod 'ObjectiveSugar', '~> 0.5'

target :test do
    pod 'OCMock', '~> 2.0.1', :configurations => ['Debug']
end

post_install do |installer|
    installer.pods_project.targets.each do |target|
        target.build_configurations.each do |config|
            config.build_settings['GCC_ENABLE_OBJC_GC'] = 'supported'
        end
    end
end

Цей файл має більш-менш зрозумілий вигляд, навіть якщо ви не знаєте Ruby. Чому? Як правило, коли ми починаємо читати код, написаний новою, незнайомою мовою, ми швидко знаходимо знайомі конструкції:

  • source 'https://github.com/CocoaPods/Specs.git' чимось схоже на інструкцію import у Swift
  • рядок post_install do |installer| безумовно нагадує звичний нам цикл for-in.
  • config.build_settings виглядає як звертання до поля класу чи структури
  • build_settings['GCC_ENABLE_OBJC_GC'] = 'supported' - це чистої води, як присвоєння значення по ключу асоціативного масива або словника.

Так працює наш мозок: якщо ти добре знаєш якісь речі, і потрапляєш у незнайоме середовище, ти всюди починаєш шукати щось подібне до того, що ти вже знаєш. Однак, регулярні вирази не зовсім схожі на мови програмування, до яких ми звикли. Це взагалі не мова програмування. Це – мова опису текстових шаблонів, і тому її базові конструкції відрізняються від звичних для нас. Тому перш ніж писати свої перші регулярки, слід з ними ознайомитись.

Теорія

Ось як виглядають базові принципи регулярних виразів:

  • більшість текстових символів представляють самі себе
  • всілякі кракозябри на кшталт [ ] \ ^ $ . | ? * + ( ) { } є операторами, окрім крапки, бо
  • крапка (.) - то священний оператор, який позначає будь-який символ

Ось основні операції, які допоможуть нам почати писати регулярні вирази:

  • діапазон [ ], наприклад: [a-z], чи [a-zA-z]: це означає, що на певній позиції може міститись лише один символ із заданого діапазону.
  • різноманітні оператори повтонення: a?, a*, a+, a{3}. Оператор повторення ставиться в кінці певного виразу, і означає, що даний вираз має зустрітись певну кількість разів.
  • оператор групування: ( ) - звичайні круглі дужки. Вираз у круглих дужках слід розглядати, як одне ціле, зокрема, при застосуванні операторів повторення.

Цих надзвичайно глибоких теоретичних знань має виявитись достатньо для написання нескладних, але при тому цілком ефективних регулярних виразів.

Ну що, вже можна писати?

Втім, перш ніж пориватись у бій, скід згадати, що Xcode не дуже допомагатиме нам писати регулярні вирази:

regexp-xcode

Однак, це не єдиний вставний код іншою мовою, з яким нам доводиться працювати в Xcode, чи не так? Хіба в нас у коді не буває вставних рядкових літералів з кодом у вигляді json чи html?

json-html

Це не дуже заважає нам писати коректний json та html. Як? Ми просто беремо і йдемо шукати більш придатну для цього IDE. Виявляється, для написання регулярок теж існують IDE, і їх чимало. Зокрема, я спробував декілька, і підібрав для вас невеличкий список найадекватніших на мій смак рішень:

  • Програми для macOS:

  • Web IDE:

    • regex101.com - безкоштовна. Цим рішенням я користуюсь сам, і користуватимусь ним у подальшій розповіді
    • regexr.com - також непогане безкоштовне рішення

Ну що ж, давайте напишемо якусь регулярку. У мене на проєкті для того, щоб створити реліз, використовують спеціальні git-теги. Вони мають приблизно наступний вигляд:

trigger/appstore-1.2.3.4

Спершу йде слово trigger, котре означає, що даний тег повинен змусити наш CI розпочати якусь роботу. Далі йде слеш. Сакральне значення слешу полягає у тому, що в моєму git-клієнті теги можна групувати по тексту до слешу, немов папки на диску. Після слешу йде одне із двох слів: appstore чи develop. В залежності від нього визначається тип збірки релізу: для завантаження на AppStore, чи для внутрішнього тестування. Потім йде дефіс та версія збірки, котра визначається рівно чотирма числами, записаними через крапку.

Давайте спробуємо написати регулярку, що буде розпізнавати такі трігер-теги. Для цього відкриваємо regex101.com у сусідньому вікні, і перш за все пишемо такий тестовий рядок:

trigger/appstore-1.2.3.4
trigger/appstore-5.6.7.8
trigger/appstore-1.22.333.4444
trigger/develop-1.2.3.4
_trigger/appstore-1.2.3.4
trigger/appstore-1.2.3.4_

Він містить кілька різновидів трігер-тегів, правильних і неправильних, і допоможе нам написати такий регулярний вираз, який буде співпадати з правильними тегами і оминати неправильні. Хто вже впізнав у цьому TDD, тому передаю своє шанування.

Давайте з чогось починати. Оскільки більшість символів у регулярках означають самі себе, давайте тупо скопіюємо перший трігер-тег trigger/appstore-1.2.3.4 і подивимось, що буде.

simple-regex-step-1

Слеш у регулярці підсвічено червоним, а якщо піднести курсор над ним – бачимо адекватне повідомлення про помилку: слеш треба екранувати бекслешем. Вставляємо бекслеш – і оп, магія: наш регулярний вираз уже співпав із кількома тестовими виразами. І що цікаво, в IDE є цілком зрозуміле пояснення, написене справа зверху людською англійською, в якому покроково показано, чому регулярний вираз працює саме так, а не інакше. А справа посередині відображено деталізований список співпадінь. Це вже набагато більш людяно, аніж перекомпільовувати свій додаток в Xcode мільйон разів, тестуючи регулярку.

simple-regex-step-2

Тут видно, що крапочки між 1, 2, 3, та 4 у регулярному виразі також чомусь підсвітились. Ми ж пам’ятаємо, що священна крапка – то будь-який символ, а якщо ми вже забули, то варто піднести курсор на крапкою, чи почитати пояснення справа. В даному випадку нам потрібне співпадіння не з будь-яким символом, а лише з крапкою, тому спробуємо заекранувати і її бекслешем:

simple-regex-step-3

Що ж, 1.2.3.4 - це не єдина версія нашого продукту, котру ми плануємо випускати, нам треба навчитись розпізнавати будь-яку версію. Треба якось вказати, що в кінці має йти не 1.2.3.4, а число.число.число.число. Під числом мається на увазі ціле додатнє, тобто якщо розглядати текстовий запис - це просто послідовність цифр. Цифра - це один із символів 0, 1, 2, 3, 4, 5, 6, 7, 8, 9. Або, якщо коротше, якийсь символ з діапазону [0-9]. Спробуємо так і написати:

simple-regex-step-4

Ого, в нас вже є співпадіння не лише з 1.2.3.4, а ще й з 5.6.7.8. Ми робимо неабиякі успіхи! Однак, наш вираз стає трохи громіздким, а це не сприятиме його прочитанню в майбутньому. Давайте спробуємо записати поняття “цифра” без копі-пасти діапазону. Нащастя, regex101.com - це повноцінна IDE, і як і в будь-якій повноцінній IDE у ній є довідка. “Quick reference” в правому нижньому кутку. Шукаємо там “digit” - з’ясовуємо, що поняття “цифра” можна записати коротше, просто як \d.

simple-regex-step-5

Ось так значно простіше сприймається, чи не так?

simple-regex-step-6

Тепер варто навчитись вирізняти не лише одну цифру, а ціле число. Що таке ціле невід’ємне число з точки зору тексту? Це просто послідовність цифр, не менше, ніж одна, не більше, ніж нескінченність. Тому потрібно знайти доречний оператор повторення для символу \d. У довідці є розділ “Quantifiers”, що, власне, містить інформацію усі оператори повторення. Власне, там знаходимо, що + – це оператор повторення “один або більше” разів, саме те, що нам і потрібно. Ставимо його в кінці кожної цифри – і вуаля, ми тепер можемо парсити будь-яку версію:

simple-regex-step-7

Що ж, тепер пора навчитись розрізняти різні види збірок: appstore та develop. Інших видів збірок в нас нема. У довідці можемо знайти такий оператор: (a|b), який можна прочитати, як “співпадає або із a, або із b”. В нашому випадку, це матиме такий вигляд: (appstore|develop):

simple-regex-step-8

Ура! Все співпало! Хоча стоп, останні два теги явно містять якесь сміття на початку та в кінці, і ми би не хотіли, щоб наш регулярний вираз співпадав із таким сміттям. У тій же довідці нескладно знайти, що оператор ^ означає початок рядка, а $ – кінець рядка. Додавши їх у наш вираз, врешті отримуємо правильну регулярку:

simple-regex-step-9

Вона правильна саме тому, що точно співпадає з правильними тегами, і не співпадає з неправильними тегами.

Це ж блог про Swift? Де Swift, Лебовскі?

Я сподіваюсь, що регулярки зараз стали для вас трохи менш страшними. У Частині 2 ми поговоримо про те, як застосовуючи регулярки у Swift та Cocoa парсити складні текстові формати простим, зрозумілим та лінійним кодом.