Работа с регулярными выражениями в iOS 10
Всем привет! В этой статье мы разберем как работать с NSRegularExpression и NSDataDetector,
всех неравнодушных приглашают под кат.

Регулярное выражение — строка или последовательность символов, которые задают шаблон. С помощью которого можно делать очень гибкие поисковые выборки в тексте.
Допустим у нас имеется некий текст в строке и наша задача найти в нем все перечисленные email-адреса. Самый лучший инструмент для этой задачи это регулярные выражения.
Пишем Р.В. (Регулярное Выражение) для проверки email. Для начала нужно описать базовые условия всех возможных комбинаций видов эл.адресов. В самом минимальном варианте, он может иметь следующий вид: a@b.io
Начинаем составлять РВ поэтапно:
@"([a-z0-9])"
— говорит, что имя ящика может содержать любую букву и любую цифру
@"([a-z0-9]){1,64}"
— говорит, что группа этих символов может быть длиной от 1 до 64-х символов
@"([a-z0-9!#$%&'*+-/=?^_`{|}~]){1,64}"
— говорит, что можно указать еще и эти символы
@"([a-z0-9!#$%&'*+-/=?^_`{|}~]){1,64}@"
— говорит, что собака на данном месте обязательна
@"([a-z0-9!#$%&'*+-/=?^_`{|}~]){1,64}@([a-z0-9!#$%&'*+-/=?^_`{|}~]){1,64}"
— Добавляем все те же символы, только после @
@"([a-z0-9!#$%&'*+-/=?^_`{|}~]){1,64}@([a-z0-9!#$%&'*+-/=?^_`{|}~]){1,64}\\."
— говорит, что символ точка на этом месте обязательна
@"([a-z0-9!#$%&'*+-/=?^_`{|}~]){1,64}@([a-z0-9!#$%&'*+-/=?^_`{|}~]){1,64}\\.([a-z0-9]){2,64}"
— говорит, что имя домена может содержать буквы и цифры общей суммы символов от 2 до 64-х символов

Только что мы составили полноценное регулярное выражение.
Общее количество всех найденных совпадений
Возвращает range первого совпадения
Получаем массив всех найденных совпадений
Получаем первое совпадение из общего количества
Проходим итератором блоком по каждому совпадению
Нахождение нашего паттерна в тексте и вставляем на его место слово Sieg!!!
NSDataDetector — это подкласс NSRegularExpression, сделанный для удобного поиска (ссылок, номеров телефонов, даты и т.д.). То есть фактический этот класс у себя под капотом содержит универсальные регулярные выражения для поиска вышеперечисленного.
Также NSDataDetector может вызывать все методы NSRegularExpression, также искать firstMatch/matches всего текста и т.д.
Создание экземпляра NSDataDectector
Количество найденных совпадений в тексте
Все совпадения в одном массиве (он содержит объекты NSTextCheckingResult)
Проходим блоком по всем совпадениям
Краткая документация свойствам
Всем кто дочитал до конца и поставил палец вверх, буду очень признателен.
Проект можно найти по этой ссылке: здесь
Всем привет! В этой статье мы разберем как работать с NSRegularExpression и NSDataDetector,
всех неравнодушных приглашают под кат.

Регулярное выражение — строка или последовательность символов, которые задают шаблон. С помощью которого можно делать очень гибкие поисковые выборки в тексте.
Допустим у нас имеется некий текст в строке и наша задача найти в нем все перечисленные email-адреса. Самый лучший инструмент для этой задачи это регулярные выражения.
Пишем Р.В. (Регулярное Выражение) для проверки email. Для начала нужно описать базовые условия всех возможных комбинаций видов эл.адресов. В самом минимальном варианте, он может иметь следующий вид: a@b.io
- от 1 символа на имя ящика — до 64-х символов
- от 1 зарезервированных символ «собака»
- от 1 символа имени ресурса — до 64-х символов
- от 2 символа домена — до 64-х символов
Начинаем составлять РВ поэтапно:
@"([a-z0-9])"
— говорит, что имя ящика может содержать любую букву и любую цифру
@"([a-z0-9]){1,64}"
— говорит, что группа этих символов может быть длиной от 1 до 64-х символов
@"([a-z0-9!#$%&'*+-/=?^_`{|}~]){1,64}"
— говорит, что можно указать еще и эти символы
@"([a-z0-9!#$%&'*+-/=?^_`{|}~]){1,64}@"
— говорит, что собака на данном месте обязательна
@"([a-z0-9!#$%&'*+-/=?^_`{|}~]){1,64}@([a-z0-9!#$%&'*+-/=?^_`{|}~]){1,64}"
— Добавляем все те же символы, только после @
@"([a-z0-9!#$%&'*+-/=?^_`{|}~]){1,64}@([a-z0-9!#$%&'*+-/=?^_`{|}~]){1,64}\\."
— говорит, что символ точка на этом месте обязательна
@"([a-z0-9!#$%&'*+-/=?^_`{|}~]){1,64}@([a-z0-9!#$%&'*+-/=?^_`{|}~]){1,64}\\.([a-z0-9]){2,64}"
— говорит, что имя домена может содержать буквы и цифры общей суммы символов от 2 до 64-х символов

Только что мы составили полноценное регулярное выражение.
Неполный справочник зарезервированных символов для РВ:
Символ | Значение |
---|---|
^ | начало проверяемой строки |
$ | конец проверяемой строки |
. | любой символ |
| | логическое ИЛИ |
? | предшествующий символ или группа символов является необязательными |
+ | один или несколько экземпляров предшествующего элемента |
* | любое количество экземпляров элемента (в том числе и нулевое |
\d | цифровой символ |
\D | не циф. символ |
\s | пробельный символ |
\S | не пробельный символ |
\w | соответствует любой букве или цифре; эквивалент [a-zA-Z0-9_] |
\W | наоборот эквивалент [^a-zA-Z0-9_], все символы кроме этих |
Квантификаторы | |
{n} | ровно n раз |
{m,n} | включительно от M до N |
{m,} | не менее m раз |
{,n} | не более n раз |
() | создание группы |
[] | в таких скобках говорим, «любой символ из этих, но только один |
Создание NSRegularExpression
self.mainText = @"There’s also +39(081)-552-1488 a “Bookmark” button that allows the user to highlight any date, time or location in the text. For simplicity’s sake, www.ok.ru you won’t
+39(081)552 2080 not cover every possible format of date, time and 2693485 location strings that can appear in your text. You’ll implement the https://github.com bookmarking functionality +249-54-85 at the very end +39 333 3333333 of the tutorial. vk.com Your first step to getting the search
functionality working is to turn standard strings representing regular expressions into http://app.com NSRegularExpression objects.";
NSString* pattern = @"\\b(in)|(or)\\b"; // Хотим искать слова "in" или "or"
NSRegularExpressionOptions regexOptions = NSRegularExpressionCaseInsensitive; // Поиск вне зависимости от регистра
NSError* error = NULL;
// Само создание регулярного выражения
NSRegularExpression* regex = [NSRegularExpression regularExpressionWithPattern:pattern
options:regexOptions
error:&error];
if (error){
NSLog(@"Ошибка при создании Regular Expression"); // Если в pattern были внесены корректные данные, тогда это сообщение не появиться
}
Справедливости ради должен отметить, что при создании паттерна, в строке мы должны писать непросто \w,(или любую другую букву или символ который зарезервирован)
а писать backslash два раза что бы экранировать NSString. \\w
Манипуляции с экземпляром NSRegularExpression
Общее количество всех найденных совпадений
NSUInteger numberOfMatches = [regex numberOfMatchesInString:_mainText
options:0
range:NSMakeRange(0, [_mainText length])];
Возвращает range первого совпадения
NSRange rangeOfFirstMatch = [regex rangeOfFirstMatchInString:_mainText
options:0
range:NSMakeRange(0, [_mainText length])];
if (!NSEqualRanges(rangeOfFirstMatch, NSMakeRange(NSNotFound, 0))) {
NSString *substringForFirstMatch = [_mainText substringWithRange:rangeOfFirstMatch];
}
Получаем массив всех найденных совпадений
NSArray *matches = [regex matchesInString:_mainText
options:0
range:NSMakeRange(0, [_mainText length])];
for (NSTextCheckingResult *match in matches) {
//=== Через проперти resultType, можно проверить
// к какому типу относиться найденый матч
if (match.resultType == NSTextCheckingTypeQuote) NSLog(@"Цитата!");
NSRange matchRange = [match range];
NSRange firstHalfRange = [match rangeAtIndex:1];
NSRange secondHalfRange = [match rangeAtIndex:2];
}
Получаем первое совпадение из общего количества
NSTextCheckingResult *match = [regex firstMatchInString:_mainText
options:0
range:NSMakeRange(0, [_mainText length])];
if (match) {
NSRange matchRange = [match range];
NSRange firstHalfRange = [match rangeAtIndex:1];
NSRange secondHalfRange = [match rangeAtIndex:2];
}
Проходим итератором блоком по каждому совпадению
__block NSUInteger count = 0;
[regex enumerateMatchesInString:_mainText options:0 range:NSMakeRange(0, [_mainText length]) usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop){
NSRange matchRange = [match range];
NSRange firstHalfRange = [match rangeAtIndex:1];
NSRange secondHalfRange = [match rangeAtIndex:2];
if (++count >= 100) *stop = YES;
}];
Нахождение нашего паттерна в тексте и вставляем на его место слово Sieg!!!
NSString *modifiedString = [regex stringByReplacingMatchesInString:_mainText
options:0
range:NSMakeRange(0, [_mainText length])
withTemplate:@"Sieg!!!"];
Обзор класса NSDataDetector
NSDataDetector — это подкласс NSRegularExpression, сделанный для удобного поиска (ссылок, номеров телефонов, даты и т.д.). То есть фактический этот класс у себя под капотом содержит универсальные регулярные выражения для поиска вышеперечисленного.
Также NSDataDetector может вызывать все методы NSRegularExpression, также искать firstMatch/matches всего текста и т.д.
Манипуляции с экземпляром NSDataDetector
Создание экземпляра NSDataDectector
NSError* error1 = nil;
NSDataDetector* detector=[NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink |
NSTextCheckingTypePhoneNumber
error:&error];
/* Типы данных, того чего можно искать
NSTextCheckingTypeOrthography
NSTextCheckingTypeSpelling
NSTextCheckingTypeGrammar
NSTextCheckingTypeDate
NSTextCheckingTypeAddress
NSTextCheckingTypeLink
NSTextCheckingTypeQuote
NSTextCheckingTypeDash
NSTextCheckingTypeReplacement
NSTextCheckingTypeCorrection
NSTextCheckingTypeRegularExpression
NSTextCheckingTypePhoneNumber
NSTextCheckingTypeTransitInformation
*/
Количество найденных совпадений в тексте
NSUInteger numberOfMatchesFromDetect = [detector numberOfMatchesInString:_mainText
options:0
range:NSMakeRange(0,[_mainText length])];
Все совпадения в одном массиве (он содержит объекты NSTextCheckingResult)
NSArray* matchesFromDetect =[detector matchesInString:_mainText
options:0
range:NSMakeRange(0,[_mainText length])];
// ----- Проходим по каждому совпадению и смотрим что там
for (NSTextCheckingResult* match in matchesFromDetect)
{
NSLog(@"------------");
NSRange matchRange = [match range];
if ([match resultType] == NSTextCheckingTypeLink)
{
NSURL* url = [match URL];
NSLog(@"url = %@",url.absoluteString);
} else if ([match resultType] == NSTextCheckingTypePhoneNumber)
{
NSString *phoneNumber = [match phoneNumber];
NSLog(@"phone = %@",phoneNumber);
}
}
Проходим блоком по всем совпадениям
__block NSUInteger countForDectect = 0;
[detector enumerateMatchesInString:_mainText
options:0
range:NSMakeRange(0, [_mainText length])
usingBlock:^(NSTextCheckingResult * _Nullable result, NSMatchingFlags flags, BOOL * _Nonnull stop) {
NSRange matchRange = result.range;
NSLog(@"In Enumerate = %d, \tRange = %@\t \tText = %@",countForDectect, NSStringFromRange(matchRange), [_mainText substringWithRange:matchRange]);
countForDectect++;
}];
Краткая документация свойствам
Проперти экземпляра NSRegularExpression | Трактовка |
---|---|
var pattern: String | Возвращает паттерн регулярного выражения. |
var options: NSRegularExpression.Options | Возвращает параметры поиска например: ищем без учета регистра NSRegularExpressionCaseInsensitive. |
var numberOfCaptureGroups: Int | Возвращает количество групп из паттерна регулярного выражения, например: @»^(ab|bc)(amg|img)$" — тут две группы |
Всем кто дочитал до конца и поставил палец вверх, буду очень признателен.
Проект можно найти по этой ссылке: здесь
Поделиться с друзьями
Комментарии (9)
Ivanq
22.02.2017 23:39Как тьюториал все понятно и интересно описано, но вот тему для валидации не ту выбрали.
Только что мы составили полноценное регулярное выражение.
Какое-то… маленькое оно. А где проверка на кавычки?
"foo"@bar.com
И, да, в кавычках можно использовать еще специальные символы.
abc"@"def@foobar.com
И их может быть несколько (кавычек)
PS Ну и вообще RFC822 следовать надо )hack_developer
22.02.2017 23:42Спасибо за отзыв, исправил РВ
([\w~}|{.`^?\-=+/*'&%$#!]){1,64}\@([\w~}|{.`^?\-=+/*'&%$#!]){1,64}\.([a-z0-9]){2,64}
Теперь кавычки не проходят
Ivanq
23.02.2017 10:51Так они должны проходить!
hack_developer
23.02.2017 11:31Делал проверку исходя из этих условий, к сожалению кавычки там не упоминаются, поэтому не брал их в расчет.
Androzd_1992
22.02.2017 23:43А как же русские буквы, ведь существуют домены.рф,.бел и другие
hack_developer
22.02.2017 23:45Хорошие замечание! Честно говоря, только сейчас от вас узнал, что есть такие.
vladbarcelo
Почитайте RFC 3696 и ужаснитесь следующим валидным е-мейлам:
Алсо, не смог потестить Ваш монструозный регексп на regex101, не подскажете где тестировали?
hack_developer
Спасибо за отзыв!

Тестировал в сервисе http://www.regexpal.com/
P.S.
Немного поменял выражение, там были подводные камни при перечисление этих символов! # $ % & ' * + — / =? ^ _ `. { | } ~
Перед знаком минус должен стоять backslash.
Вот исправленное
([\w~}|{.`^?\-=+/*'&%$#!]){1,64}\@([\w~}|{.`^?\-=+/*'&%$#!]){1,64}\.([a-z0-9]){2,64}
+ Выражение могло не запуститься в онлайн редакторе, потому что в нем точка была экранирована два раза \\.
(первый раз для РВ, второй для NSString)
vladbarcelo
Нет кавычек, а они в качестве адреса валидны. Для полноценной работы нужна поддержка русских доменов.рф,.дети, etc. Ещё домен может ВНЕЗАПНО быть в punycode — mail@xn--d1acufc.xn--p1ai (что вашим регекспом не покрывается).
Алсо, ещё несколько валидных адресов:
И парочка неправильных, которые Ваш регексп кушает на отлично: