🍎 Swift

Expressões regulares em Swift: um guia completo

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты — библиотекарь, и у тебя есть огромное хранилище с миллионами книг. Однажды приходит читатель и говорит: «Мне нужна книга, где на 42-й странице есть слово "дракон", но только если оно написано с большой буквы и стоит в конце предложения». Если бы ты искал такую книгу вручную, перелистывая каждую страницу, ты бы потратил недели. Но у тебя есть волшебная лупа — регулярное выражение (или регекс, от английского regular expression).

Ты просто настраиваешь эту лупу: «Покажи мне все места, где есть "Дракон." в конце предложения». Лупа мгновенно подсвечивает все совпадения. В программировании регулярные выражения работают точно так же — это язык для поиска и замены текста по шаблону.

💡 Важное замечание: Регулярные выражения — это не фишка языка Swift. Это отдельный язык, который встроен почти во все современные языки программирования. Научившись один раз, ты сможешь использовать его везде: в Python, JavaScript, Java, даже в текстовых редакторах вроде VS Code.

Зачем это нужно в реальной жизни? Вот несколько примеров:

  • Валидация форм: Проверить, что пользователь ввёл правильный email (например, ivan@mail.ru) или номер телефона.
  • Парсинг данных: Извлечь все ссылки из веб-страницы или все цены из прайс-листа.
  • Поиск и замена: В огромном файле заменить все даты из формата «12.05.2024» на «2024-05-12».
  • Логирование: Найти в логах сервера все ошибки, которые произошли в определённый час.

В Swift регулярные выражения появились относительно недавно (с iOS 16 / macOS 13), но они стали намного удобнее и безопаснее, чем старые способы.

Теория: как это работает изнутри

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

Допустим, у нас есть шаблон: cat. Автомат делает следующее:

  1. Берёт первый символ текста. Если это c — переходит к следующему шагу. Если нет — идёт дальше.
  2. Берёт второй символ. Если это a — продолжает. Если нет — возвращается в начало.
  3. Берёт третий символ. Если это t — объявляет: «Найдено совпадение!».

Но что, если шаблон сложнее? Например: c.t (где точка означает «любой символ»). Тогда автомат ищет c, потом любой символ, потом t. Так он найдёт и cat, и cot, и c8t.

В основе регулярных выражений лежат три кита:

  • Литералы — обычные символы, которые ищут сами себя (буквы, цифры).
  • Метасимволы — специальные символы с особым значением (точка, звёздочка, плюс, скобки).
  • Квантификаторы — указывают, сколько раз должен повторяться символ или группа.

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

⚠️ Важно: Регулярные выражения бывают «жадными» (greedy) и «ленивыми» (lazy). Жадный квантификатор (*, +) захватывает максимально возможную строку. Ленивый (*?, +?) — минимально возможную. Это частая причина ошибок у новичков.

Синтаксис в Swift: все формы и варианты

В Swift есть три способа работы с регулярными выражениями. Мы рассмотрим каждый, но рекомендую сразу учиться использовать Regex literal — это самый современный и удобный способ.

Способ 1: Regex literal (рекомендуемый)

Появился в Swift 5.7. Ты просто пишешь выражение между двумя слешами: /pattern/. Swift сам проверяет корректность регекса на этапе компиляции.

// Создаём регулярное выражение для поиска email
let emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}/
// Swift сам проверит, что регекс написан правильно, ещё до запуска программы!

Способ 2: Класс NSRegularExpression (старый способ)

Работает во всех версиях iOS. Требует больше кода, но даёт больше контроля (например, можно указать опции поиска).

import Foundation

let pattern = "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
// Обрати внимание: в строке нужно экранировать обратные слеши!

do {
    let regex = try NSRegularExpression(pattern: pattern)
    // ... дальше работаем с regex
} catch {
    print("Ошибка в регексе: (error)")
}

Способ 3: Методы строк с .regularExpression

Можно использовать опцию .regularExpression в методах range(of:) и replacingOccurrences(of:).

let text = "Мой email: ivan@mail.ru"
if let range = text.range(of: "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", 
                          options: .regularExpression) {
    print("Найден email: (text[range])")
}

Таблица основных метасимволов

Символ Значение Пример
. Любой символ (кроме новой строки) c.t найдёт cat, cot, c8t
d Любая цифра (0-9) d{3} найдёт 123, 456
w Любая буква, цифра или подчёркивание w+ найдёт слово
s Любой пробельный символ (пробел, табуляция) s+ найдёт группу пробелов
^ Начало строки ^Привет — строка начинается с "Привет"
$ Конец строки конец$ — строка заканчивается на "конец"
* 0 или более повторений ab*c найдёт ac, abc, abbc
+ 1 или более повторений ab+c найдёт abc, abbc, но не ac
? 0 или 1 повторение colou?r найдёт color и colour
{n,m} От n до m повторений d{2,4} найдёт 12, 123, 1234
[abc] Один из символов в скобках [cbr]at найдёт cat, bat, rat
(...) Группировка и захват (d{2})-(d{2}) захватит две группы цифр
| Логическое ИЛИ кот|пёс найдёт "кот" или "пёс"

Примеры кода — от простого к сложному

Пример 1: Поиск простого слова

import Foundation

let text = "Сегодня я купил кота и собаку."
// Ищем слово "кот" в тексте
let regex = /кот/

if let match = text.firstMatch(of: regex) {
    // firstMatch возвращает первое совпадение
    print("Найдено: (match.0)") // Выведет: Найдено: кот
} else {
    print("Совпадений нет")
}

Пример 2: Поиск всех совпадений

import Foundation

let text = "Кот спит. Кот ест. Кот гуляет."
// Ищем все вхождения слова "Кот"
let regex = /Кот/

// Используем matches, чтобы получить все совпадения
let matches = text.matches(of: regex)
print("Найдено (matches.count) совпадений") // Выведет: Найдено 3 совпадений

for match in matches {
    print("Совпадение: (match.0)")
}

Пример 3: Поиск цифр в тексте

import Foundation

let text = "У меня 3 яблока и 15 груш."
// d+ означает "одна или более цифр"
let regex = /d+/

let matches = text.matches(of: regex)
for match in matches {
    print("Найдено число: (match.0)")
}
// Выведет:
// Найдено число: 3
// Найдено число: 15

Пример 4: Валидация email-адреса

import Foundation

// Проверяем, является ли строка корректным email
func isValidEmail(_ email: String) -> Bool {
    // ^ - начало строки, $ - конец строки
    // Это гарантирует, что мы проверяем ВСЮ строку, а не ищем email внутри
    let emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/
    return email.wholeMatch(of: emailRegex) != nil
}

print(isValidEmail("ivan@mail.ru"))     // true
print(isValidEmail("ivan@mail"))        // false (нет домена)
print(isValidEmail("ivan@.ru"))         // false (нет имени)
print(isValidEmail("ivan@@mail.ru"))    // false (две собачки)

Пример 5: Извлечение данных из строки (группы захвата)

import Foundation

let text = "Мой номер: +7 (999) 123-45-67"
// Разбиваем номер на части с помощью групп (скобки)
let phoneRegex = /+(d)s((d{3}))s(d{3})-(d{2})-(d{2})/

if let match = text.firstMatch(of: phoneRegex) {
    print("Код страны: (match.1)")     // 7
    print("Код оператора: (match.2)")  // 999
    print("Основной номер: (match.3)") // 123
    print("Остальные цифры: (match.4) (match.5)") // 45 67
}

Пример 6: Замена текста по шаблону

import Foundation

var text = "Сегодня 12.05.2024, а завтра 13.05.2024"
// Ищем даты в формате ДД.ММ.ГГГГ и заменяем на ГГГГ-ММ-ДД
let dateRegex = /(d{2}).(d{2}).(d{4})/
// В замене используем $1, $2, $3 для обращения к группам
let newText = text.replacing(dateRegex, with: "$3-$2-$1")
print(newText) // Выведет: Сегодня 2024-05-12, а завтра 2024-05-13

Пример 7: Сложный поиск с условиями

import Foundation

let text = "apple, banana, apricot, cherry, avocado"
// Ищем слова, которые начинаются на 'a' и заканчиваются на 'e'
//  - граница слова (не даёт захватить часть слова)
let regex = /aw*e/

let matches = text.matches(of: regex)
for match in matches {
    print("Найдено: (match.0)")
}
// Выведет:
// Найдено: apple
// Найдено: apricot (нет, оно заканчивается на t)
// Найдено: avocado (нет, заканчивается на o)
// Правильный ответ: только apple

Пример 8: Использование опций (регистронезависимый поиск)

import Foundation

let text = "Кот, кот, КОТ, кОт"
// Добавляем опцию .caseInsensitive, чтобы игнорировать регистр
let regex = /кот/.ignoresCase()

let matches = text.matches(of: regex)
print("Найдено (matches.count) совпадений") // Выведет: Найдено 4 совпадений

Сравнение с Python: чем похоже и чем отличается

Если ты уже знаком с Python, тебе будет проще. Но есть важные отличия:

Характеристика Swift (Regex literal) Python (модуль re)
Создание регекса /pattern/ — проверка на этапе компиляции re.compile(r"pattern") — проверка во время выполнения
Экранирование Не нужно экранировать внутри // Нужен raw-строки (r"...") или двойное экранирование
Поиск первого совпадения text.firstMatch(of: regex) re.search(pattern, text)
Поиск всех совпадений text.matches(of: regex) re.findall(pattern, text)
Замена text.replacing(regex, with: "$1") re.sub(pattern, r"1", text)
Группы захвата Доступ через match.1, match.2 Доступ через match.group(1)
Именованные группы /(?<name>pattern)/ (?P<name>pattern)
Производительность Выше (компилируется один раз) Зависит от использования compile
💡 Совет: Если ты переходишь с Python на Swift, главное отличие — в Swift регексы проверяются на этапе компиляции. Это значит, что если ты ошибёшься в синтаксисе, программа даже не запустится. В Python ошибка всплывёт только когда выполнение дойдёт до этой строки.

Типичные ошибки и подводные камни

Ошибка 1: Забыл экранировать точку

// ❌ Неправильно: точка означает "любой символ"
let regex = /mail.ru/  // Найдёт "mailXru", "mailYru" и т.д.

// ✅ Правильно: экранируем точку
let regex = /mail.ru/  // Найдёт только "mail.ru"

Ошибка 2: Жадность квантификаторов

let text = "<h1>Заголовок</h1> <p>Текст</p>"
// ❌ Жадный поиск захватит всё от первого < до последнего >
let greedyRegex = /<.*>/
// Найдёт: <h1>Заголовок</h1> <p>Текст</p>

// ✅ Ленивый поиск остановится на первом >
let lazyRegex = /<.*?>/
// Найдёт: <h1>, </h1>, <p>, </p>

Ошибка 3: Неправильное использование якорей

let text = "Привет, мир! Приветствие"
// ❌ Ищем "Привет" в любом месте
let regex = /Привет/
// Найдёт оба вхождения

// ✅ Ищем только в начале строки
let regexStart = /^Привет/
// Найдёт только первое "Привет"

Ошибка 4: Проблемы с Unicode

let text = "café"
// ❌ w не захватывает букву "é"
let regex = /w+/
// Найдёт только "caf"

// ✅ Используем p{L} для любой буквы Unicode
let unicodeRegex = /p{L}+/
// Найдёт "café"

Ошибка 5: Забыл про опцию .ignoresCase()

let text = "Swift, swift, SWIFT"
// ❌ По умолчанию поиск регистрозависимый
let regex = /swift/
// Найдёт только "swift"

// ✅ Добавляем опцию
let caseInsensitiveRegex = /swift/.ignoresCase()
// Найдёт все три варианта

Лучшие практики и советы профессионалов

  1. Начинай с простого. Не пытайся написать идеальный регекс с первого раза. Начни с простого шаблона и постепенно усложняй его.
  2. Используй онлайн-тестеры. Перед тем как вставлять регекс в код, проверь его на сайтах вроде regex101.com или regexr.com. Они подсвечивают совпадения и объясняют, что делает каждый символ.
  3. Давай регексам имена. Вместо того чтобы писать регекс прямо в коде, вынеси его в константу с понятным именем:
// ❌ Плохо: непонятно, что ищем
let result = text.range(of: "^[\w.-]+@[\w.-]+\.\w{2,}$", options: .regularExpression)

// ✅ Хорошо: понятно из названия
let emailPattern = /^[w.-]+@[w.-]+.w{2,}$/
let result = text.wholeMatch(of: emailPattern)
  1. Не изобретай велосипед. Для стандартных задач (email, телефон, URL) используй готовые проверенные шаблоны. Они есть в документации Apple и на Stack Overflow.
  2. Тестируй на граничных случаях. Проверь, как твой регекс работает с пустой строкой, с очень длинной строкой, со специальными символами.
  3. Используй именованные группы для сложных шаблонов. Это делает код самодокументируемым:
let dateRegex = /(?<day>d{2}).(?<month>d{2}).(?<year>d{4})/
if let match = text.firstMatch(of: dateRegex) {
    print("День: (match.day), Месяц: (match.month), Год: (match.year)")
}
  1. Помни про производительность. Если ты используешь один и тот же регекс много раз, создай его один раз и переиспользуй. В Swift Regex literal автоматически компилируется один раз.
  2. Избегай катастрофического возврата (catastrophic backtracking). Это когда регекс пытается проверить миллионы комбинаций и программа зависает. Пример опасного шаблона: (a+)+b на строке из 20 символов "a". Лучше переписать как a+b.

Вопросы для самопроверки

  1. Вопрос: Что такое регулярное выражение простыми словами?

    Ответ: Это язык для поиска и замены текста по шаблону. Представь, что это умная поисковая строка, которая понимает не просто слова, а целые правила: «найди все слова из 5 букв, которые начинаются на "а" и заканчиваются на "я"».

  2. Вопрос: Чем отличается * от + в регулярных выражениях?

    Ответ: * означает «0 или более повторений» (может вообще не быть), а + — «1 или более повторений» (должен быть хотя бы один символ). Например, ab*c найдёт ac, abc, abbc, а ab+c найдёт abc, abbc, но не ac.

  3. Вопрос: Как в Swift создать регулярное выражение, которое будет искать независимо от регистра?

    Ответ: Нужно добавить опцию .ignoresCase() к регексу. Например: /swift/.ignoresCase() найдёт Swift, SWIFT, swift и любые другие комбинации регистра.

  4. Вопрос: Что такое «жадный» и «ленивый» поиск?

    Ответ: Жадный квантификатор (*, +) захватывает максимально возможную строку. Ленивый (*?, +?) — минимально возможную. Например, для текста <p>текст</p> шаблон <.*> найдёт всю строку, а <.*?> найдёт только <p>.

  5. Вопрос: Как в Swift извлечь часть совпадения с помощью групп захвата?

    Ответ: Нужно использовать круглые скобки в шаблоне, а затем обращаться к группам через match.1, match.2 и т.д. Например, для шаблона /(d{2})-(d{2})/ и строки "12-34" — match.1 вернёт "12", а match.2 вернёт "34".

  6. Вопрос: Какая главная ошибка новичков при работе с точкой в регексе?

    Ответ: Забывают, что точка означает «любой символ». Если нужно найти именно точку как символ, её нужно экранировать: .. Например, чтобы найти mail.ru, нужно писать /mail.ru/, иначе найдётся и mailXru.

  7. Вопрос: Чем отличается firstMatch(of:) от wholeMatch(of:)?

    Ответ: firstMatch(of:) ищет первое совпадение в любом месте строки. wholeMatch(of:) проверяет, соответствует ли вся строка целиком шаблону. Для валидации email нужно использовать wholeMatch с якорями ^ и $.

  8. Вопрос: Как заменить все даты в формате ДД.ММ.ГГГГ на формат ГГГГ-ММ-ДД?

    Ответ: Использовать метод replacing(_:with:) с группами захвата. Шаблон: /(d{2}).(d{2}).(d{4})/, замена: "$3-$2-$1". Где $1 — день, $2 — месяц, $3 — год.

  9. Вопрос: Что такое катастрофический возврат и как его избежать?

    Ответ: Это ситуация, когда регекс пытается проверить экспоненциально много комбинаций и программа зависает. Возникает при вложенных квантификаторах, например (a+)+b. Избегать: не использовать вложенные квантификаторы, переписывать шаблоны проще.

  10. Вопрос: В чём преимущество Regex literal перед NSRegularExpression?

    Ответ: Regex literal проверяется на этапе компиляции (ошибки в регексе не дадут запустить программу), не требует экранирования обратных слешей, имеет более чистый синтаксис и лучше интегрирован с системой типов Swift.

Итог и что изучить дальше

Мы прошли долгий путь от простого понимания «что такое регулярные выражения» до написания сложных шаблонов с группами захвата и заменой текста. Ты узнал:

  • Регулярные выражения — это язык для поиска и замены текста по шаблону.
  • В Swift есть три способа работы: Regex literal (рекомендуемый), NSRegularExpression и методы строк с опцией .regularExpression.
  • Основные метасимволы: ., d, w, s, ^, $, *, +, ?, {n,m}, [...], (...), |.
  • Как использовать группы захвата для извлечения частей текста.
  • Как избегать типичных ошибок: жадность, забытое экранирование, проблемы с Unicode.

Что изучить дальше:

  1. Именованные группы захвата — делают код ещё понятнее: /(?<имя>шаблон)/.
  2. Опции регексов — .dotMatchesLineSeparators(), .anchorsMatchLines() и другие.
  3. Regex Builder — новый способ создавать регексы в Swift через DSL (предметно-ориентированный язык). Позволяет писать сложные шаблоны без риска ошибиться в синтаксисе.
  4. Unicode в регексах — работа с разными алфавитами, эмодзи, специальными символами.
  5. Производительность рег