Статьи

Свифт и регулярные выражения: Свифт

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

Откройте Xcode, создайте новую игровую площадку, назовите ее RegExTut и установите Platform на OS X. Выбор платформы, iOS или OS X, не имеет значения в отношении API, который мы собираемся использовать.

Создать игровую площадку

Прежде чем мы начнем, есть еще одна вещь, о которой вам нужно знать. В Swift вам нужно использовать две обратные косые черты, \\ , для каждой обратной косой черты, которую вы используете в регулярном выражении. Причина в том, что Swift имеет строковые литералы в стиле C. Обратная косая черта обрабатывается как экранирование символа в дополнение к его роли в интерполяции строк в Swift. Другими словами, вам нужно убежать от побега персонажа. Если это звучит странно, не беспокойтесь об этом. Просто помните, чтобы использовать две обратные косые черты вместо одной.

В первом, несколько надуманном примере, мы представляем, что копаемся в строке, ища очень специфический тип адреса электронной почты. Адрес электронной почты соответствует следующим критериям:

  • первая буква первая буква имени человека
  • с последующим периодом
  • сопровождаемый фамилией человека
  • сопровождаемый символом @
  • сопровождаемый именем, представляющим университет в Соединенном Королевстве
  • затем .ac.uk , домен для академических учреждений в Соединенном Королевстве

Добавьте следующий код на игровую площадку и давайте шаг за шагом пройдемся по этому фрагменту кода.

01
02
03
04
05
06
07
08
09
10
import Cocoa
 
// (1):
let pat = «\\b([az])\\.([az]{2,})@([az]+)\\.ac\\.uk\\b»
// (2):
// (3):
let regex = try!
// (4):
let matches = regex.matchesInString(testStr, options: [], range: NSRange(location: 0, length: testStr.characters.count))

Мы определяем образец. Обратите внимание на удвоенные обратные слэши. В (нормальном) представлении регулярных выражений, например, используемом на веб-сайте RegExr , это будет ([az])\.([az]{2,})@([az]+)\.ac\.uk . Также обратите внимание на использование скобок. Они используются для определения групп захвата, с помощью которых мы можем извлечь подстроки, сопоставленные с этой частью регулярного выражения.

Вы должны быть в состоянии понять, что первая группа захвата захватывает первую букву имени пользователя, вторая — его фамилию, а третья — название университета. Обратите внимание также на использование обратной косой черты для экранирования символа точки для представления его буквального значения. В качестве альтернативы, мы могли бы поместить его в набор символов отдельно ( [.] ). В этом случае нам не нужно было бы избегать этого.

Это строка, в которой мы ищем шаблон.

Мы создаем объект NSRegularExpression , передавая шаблон без параметров. В списке параметров вы можете указать константы NSRegularExpressionOption , например:

  • CaseInsensitive : эта опция указывает, что при сопоставлении регистр не учитывается.
  • IgnoreMetacharacters : используйте эту опцию, если вы хотите выполнить буквальное совпадение, означающее, что метасимволы не имеют специального значения и соответствуют себе как обычные символы.
  • AnchorMatchLines : используйте эту опцию, если хотите, чтобы якоря ^ и $ соответствовали началу и концу строк (разделенных переносами строк) в одной строке, а не началу и концу всей строки.

Поскольку инициализатор выбрасывает, мы используем ключевое слово try . Например, если мы передаем недопустимое регулярное выражение, выдается ошибка.

Мы ищем совпадения в тестовой строке, вызывая matchesInString(_:options:range:) , передавая диапазон, чтобы указать, какая часть строки нам интересна. Этот метод также принимает список параметров. Для простоты мы не передаем никаких опций в этом примере. Я расскажу об опциях в следующем примере.

Соответствия возвращаются как массив объектов NSTextCheckingResult . Мы можем извлечь совпадения, включая группы захвата, следующим образом:

1
2
3
4
5
6
7
8
for match in matches {
    for n in 0..<match.numberOfRanges {
        let range = match.rangeAtIndex(n)
        let r = testStr.startIndex.advancedBy(range.location) ..<
            testStr.startIndex.advancedBy(range.location+range.length)
        testStr.substringWithRange(r)
    }
}

Приведенный выше фрагмент выполняет NSTextCheckingResult по каждому объекту NSTextCheckingResult в массиве. Свойство numberOfRanges для каждого совпадения в примере имеет значение 4 , по одному для всей сопоставленной подстроки, соответствующей адресу электронной почты (например, [email protected]), а остальные три соответствуют трем группам захвата. в матче («а», «хан» и «суррей» соответственно).

Метод rangeAtIndex(_:) возвращает диапазон подстрок в строке, чтобы мы могли их извлечь. Обратите внимание, что вместо использования rangeAtIndex(0) вы также можете использовать свойство range для всего соответствия.

Нажмите кнопку Показать результат на панели результатов справа. Это показывает нам «Surrey», значение testStr.substringWithRange(r) для последней итерации цикла. Щелкните правой кнопкой мыши поле результатов и выберите « История значений», чтобы отобразить историю значений.

Показать историю значений

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

Существует удобный способ выполнять операции поиска и замены, используя строку шаблона, которая имеет специальный синтаксис для представления групп захвата. Продолжая этот пример, предположим, что мы хотим заменить каждый соответствующий адрес электронной почты подстрокой формы «фамилия, инициал, университет», мы могли бы сделать следующее:

1
2
3
4
let replacedStr = regex.stringByReplacingMatchesInString(testStr,
                   options: [],
                   range: NSRange(location: 0, length: testStr.characters.count),
                   withTemplate: «($2, $1, $3)»)

Обратите внимание на синтаксис $n в шаблоне, который служит заполнителем для текста группы захвата n . Имейте в виду, что $0 представляет весь матч.

Метод matchesInString(_:options:range:) является одним из нескольких вспомогательных методов, которые полагаются на enumerateMatchesInString(_:options:range:usingBlock:) , который является наиболее гибким и общим (и поэтому сложным) методом в классе NSRegularExpression . Этот метод вызывает блок после каждого совпадения, позволяя вам выполнять любые действия, которые вы хотите.

NSMatchingOptions одно или несколько правил сопоставления, используя константы NSMatchingOptions , вы можете убедиться, что блок вызывается в других случаях. Для длительных операций вы можете указать, что блок вызывается периодически, и в какой-то момент завершить операцию. С опцией ReportCompletion вы указываете, что блок должен быть вызван по завершении.

Блок имеет параметр flags, который сообщает о любом из этих состояний, чтобы вы могли решить, какое действие предпринять. Подобно некоторым другим методам перечисления в платформе Foundation , блок также может быть завершен по вашему усмотрению. Например, если длительное совпадение не удается или вы нашли достаточно совпадений, чтобы начать обработку.

В этом сценарии мы будем искать в некотором тексте строки, похожие на даты, и проверять наличие конкретной даты. Чтобы сохранить пример управляемым, представим, что строки даты имеют следующую структуру:

  • год с двумя или четырьмя цифрами (например, 09 или 2009)
  • только с нынешнего столетия (между 2000 и 2099 годами), поэтому 1982 будет отклонен, а 16 будет автоматически интерпретирован как 2016
  • с последующим разделителем
  • за которым следует число от 1 до 12, обозначающее месяц
  • с последующим разделителем
  • заканчивая числом от 1 до 31, обозначающим день

Месяцы и даты, состоящие из одной цифры, могут дополняться начальным нулем. Допустимые разделители — это тире, точка и косая черта. Помимо вышеуказанных требований, мы не будем проверять, действительно ли дата действительна. Например, у нас все в порядке с такими датами, как 2000-04-31 (в апреле всего 30 дней) и 2009-02-29 (2009 год не високосный, то есть в феврале только 28 дней), которые не являются реальными датами. ,

Добавьте следующий код на игровую площадку и давайте шаг за шагом пройдемся по этому фрагменту кода.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// (1):
typealias PossibleDate = (year: Int, month: Int, day: Int)
// (2):
func dateSearch(text: String, _ date: PossibleDate) -> Bool {
 
    // (3):
    let datePattern = «\\b(?:20)?(\\d\\d)[-./](0?[1-9]|1[0-2])[-./](3[0-1]|[1-2][0-9]|0?[1-9])\\b»
     
    let dateRegex = try!
                                             options: [])
    // (4):
    var wasFound: Bool = false
    // (5):
    dateRegex.enumerateMatchesInString(text, options: [],
                                       range: NSRange(location: 0,
                                        length: text.characters.count)) {
        // (6):
        (match, _, stop) in
        var dateArr = [Int]()
        for n in 1…3 {
            let range = match!.rangeAtIndex(n)
            let r = text.startIndex.advancedBy(range.location) ..<
                text.startIndex.advancedBy(range.location+range.length)
            dateArr.append(Int(text.substringWithRange(r))!)
             
        }
        // (7):
        if dateArr[0] == date.year
            && dateArr[1] == date.month
            && dateArr[2] == date.day {
            // (8):
            wasFound = true
            stop.memory = true
        }
    }
     
    return wasFound
}
 
let text = » 2015/10/10,11-10-20, 13/2/2 1981-2-2 2010-13-10″
 
let date1 = PossibleDate(15, 10, 10)
let date2 = PossibleDate(13, 1, 1)
 
dateSearch(text, date1) // returns true
dateSearch(text, date2) // returns false

Дата, чье существование мы проверяем, будет в стандартизированном формате. Мы используем именованный кортеж. Мы передаем только двузначное целое число в год, то есть 16 означает 2016 год.

Наша задача состоит в том, чтобы перечислять совпадения, которые выглядят как даты, извлекать из них компоненты года, месяца и дня и проверять, соответствуют ли они дате, которую мы передали. Мы создадим функцию, которая сделает все это за нас. Функция возвращает true или false зависимости от того, была найдена дата или нет.

Шаблон даты имеет некоторые интересные особенности:

  • Обратите внимание на фрагмент (?:20)? , Если мы заменим этот фрагмент на (20)? Надеюсь, вы поймете, что это означало, что мы в порядке с «20» (представляющим тысячелетие), присутствующим в году или нет. Круглые скобки необходимы для группировки, но мы не хотим формировать группу захвата с этой парой круглых скобок, и именно для этого предназначен бит ?: .
  • Возможные разделители внутри набора символов [-./] Не должны быть экранированы, чтобы представлять их буквальные я. Вы можете думать об этом так. Тире, - , находится в начале, поэтому он не может представлять диапазон. И это не имеет смысла для периода . , для представления любого символа в наборе символов, поскольку он делает это одинаково хорошо снаружи.
  • Мы интенсивно используем вертикальную черту для чередования, чтобы представить различные цифры числа месяца и даты.

Функция будет возвращать логическую переменную notFound , указывающую, была ли найдена искомая дата или нет.

enumerateMatchesInString(_:options:range:usingBlock:) . Мы не используем ни одну из опций и передаем весь диапазон искомого текста.

Объект блока, вызываемый после каждого совпадения, имеет три параметра:

  • совпадение ( NSTextCheckingResult )
  • флаги, представляющие текущее состояние процесса сопоставления (который мы здесь игнорируем)
  • булева переменная stop , которую мы можем установить в блоке для раннего выхода

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

Мы проверяем, совпадают ли извлеченные компоненты из сопоставленной подстроки с компонентами желаемой даты. Обратите внимание, что мы приводим приведение к типу Int , которое, как мы уверены, не приведет к сбою, поскольку мы создали соответствующие группы захвата, соответствующие только цифрам.

Если совпадение найдено, мы устанавливаем notFound в true . stop.memory из блока, установив stop.memory   к true . Мы делаем это потому, что stop — это указатель на логическое значение, а Swift работает с «указанными» памятью через свойство memory.

Заметьте, что подстрока «2015/10/10» в нашем тексте соответствует возможной дате (15, 10, 10) , поэтому функция возвращает true в первом случае. Однако ни одна строка в тексте не соответствует AdditionalDate (13, 1, 1) , то есть «2013-01-01», и второй вызов функции возвращает false .

Выход

Мы рассмотрели, как работают регулярные выражения, неторопливо, но достаточно подробно, но есть еще много интересного, если вам интересно, например, утверждения lookahead и lookbehind , применение регулярных выражений к строкам Unicode, в дополнение к рассмотрению различные варианты, которые мы рассмотрели в Foundation API.

Даже если вы решите не углубляться в подробности, мы надеемся, что вы подобрали достаточно здесь, чтобы иметь возможность определить ситуации, в которых могут пригодиться регулярные выражения, а также некоторые советы о том, как разрабатывать регулярные выражения для решения проблем поиска по шаблону.