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. Автомат делает следующее:
- Берёт первый символ текста. Если это
c— переходит к следующему шагу. Если нет — идёт дальше. - Берёт второй символ. Если это
a— продолжает. Если нет — возвращается в начало. - Берёт третий символ. Если это
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()
// Найдёт все три варианта
Лучшие практики и советы профессионалов
- Начинай с простого. Не пытайся написать идеальный регекс с первого раза. Начни с простого шаблона и постепенно усложняй его.
- Используй онлайн-тестеры. Перед тем как вставлять регекс в код, проверь его на сайтах вроде regex101.com или regexr.com. Они подсвечивают совпадения и объясняют, что делает каждый символ.
- Давай регексам имена. Вместо того чтобы писать регекс прямо в коде, вынеси его в константу с понятным именем:
// ❌ Плохо: непонятно, что ищем
let result = text.range(of: "^[\w.-]+@[\w.-]+\.\w{2,}$", options: .regularExpression)
// ✅ Хорошо: понятно из названия
let emailPattern = /^[w.-]+@[w.-]+.w{2,}$/
let result = text.wholeMatch(of: emailPattern)
- Не изобретай велосипед. Для стандартных задач (email, телефон, URL) используй готовые проверенные шаблоны. Они есть в документации Apple и на Stack Overflow.
- Тестируй на граничных случаях. Проверь, как твой регекс работает с пустой строкой, с очень длинной строкой, со специальными символами.
- Используй именованные группы для сложных шаблонов. Это делает код самодокументируемым:
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)")
}
- Помни про производительность. Если ты используешь один и тот же регекс много раз, создай его один раз и переиспользуй. В Swift Regex literal автоматически компилируется один раз.
- Избегай катастрофического возврата (catastrophic backtracking). Это когда регекс пытается проверить миллионы комбинаций и программа зависает. Пример опасного шаблона:
(a+)+bна строке из 20 символов "a". Лучше переписать какa+b.
Вопросы для самопроверки
-
Вопрос: Что такое регулярное выражение простыми словами?
Ответ: Это язык для поиска и замены текста по шаблону. Представь, что это умная поисковая строка, которая понимает не просто слова, а целые правила: «найди все слова из 5 букв, которые начинаются на "а" и заканчиваются на "я"».
-
Вопрос: Чем отличается
*от+в регулярных выражениях?Ответ:
*означает «0 или более повторений» (может вообще не быть), а+— «1 или более повторений» (должен быть хотя бы один символ). Например,ab*cнайдётac,abc,abbc, аab+cнайдётabc,abbc, но неac. -
Вопрос: Как в Swift создать регулярное выражение, которое будет искать независимо от регистра?
Ответ: Нужно добавить опцию
.ignoresCase()к регексу. Например:/swift/.ignoresCase()найдётSwift,SWIFT,swiftи любые другие комбинации регистра. -
Вопрос: Что такое «жадный» и «ленивый» поиск?
Ответ: Жадный квантификатор (
*,+) захватывает максимально возможную строку. Ленивый (*?,+?) — минимально возможную. Например, для текста<p>текст</p>шаблон<.*>найдёт всю строку, а<.*?>найдёт только<p>. -
Вопрос: Как в Swift извлечь часть совпадения с помощью групп захвата?
Ответ: Нужно использовать круглые скобки в шаблоне, а затем обращаться к группам через
match.1,match.2и т.д. Например, для шаблона/(d{2})-(d{2})/и строки"12-34"—match.1вернёт"12", аmatch.2вернёт"34". -
Вопрос: Какая главная ошибка новичков при работе с точкой в регексе?
Ответ: Забывают, что точка означает «любой символ». Если нужно найти именно точку как символ, её нужно экранировать:
.. Например, чтобы найтиmail.ru, нужно писать/mail.ru/, иначе найдётся иmailXru. -
Вопрос: Чем отличается
firstMatch(of:)отwholeMatch(of:)?Ответ:
firstMatch(of:)ищет первое совпадение в любом месте строки.wholeMatch(of:)проверяет, соответствует ли вся строка целиком шаблону. Для валидации email нужно использоватьwholeMatchс якорями^и$. -
Вопрос: Как заменить все даты в формате ДД.ММ.ГГГГ на формат ГГГГ-ММ-ДД?
Ответ: Использовать метод
replacing(_:with:)с группами захвата. Шаблон:/(d{2}).(d{2}).(d{4})/, замена:"$3-$2-$1". Где$1— день,$2— месяц,$3— год. -
Вопрос: Что такое катастрофический возврат и как его избежать?
Ответ: Это ситуация, когда регекс пытается проверить экспоненциально много комбинаций и программа зависает. Возникает при вложенных квантификаторах, например
(a+)+b. Избегать: не использовать вложенные квантификаторы, переписывать шаблоны проще. -
Вопрос: В чём преимущество Regex literal перед NSRegularExpression?
Ответ: Regex literal проверяется на этапе компиляции (ошибки в регексе не дадут запустить программу), не требует экранирования обратных слешей, имеет более чистый синтаксис и лучше интегрирован с системой типов Swift.
Итог и что изучить дальше
Мы прошли долгий путь от простого понимания «что такое регулярные выражения» до написания сложных шаблонов с группами захвата и заменой текста. Ты узнал:
- Регулярные выражения — это язык для поиска и замены текста по шаблону.
- В Swift есть три способа работы: Regex literal (рекомендуемый), NSRegularExpression и методы строк с опцией .regularExpression.
- Основные метасимволы:
.,d,w,s,^,$,*,+,?,{n,m},[...],(...),|. - Как использовать группы захвата для извлечения частей текста.
- Как избегать типичных ошибок: жадность, забытое экранирование, проблемы с Unicode.
Что изучить дальше:
- Именованные группы захвата — делают код ещё понятнее:
/(?<имя>шаблон)/. - Опции регексов —
.dotMatchesLineSeparators(),.anchorsMatchLines()и другие. - Regex Builder — новый способ создавать регексы в Swift через DSL (предметно-ориентированный язык). Позволяет писать сложные шаблоны без риска ошибиться в синтаксисе.
- Unicode в регексах — работа с разными алфавитами, эмодзи, специальными символами.
- Производительность рег