Swift выдерживает регулярные выражения

135

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

Итак, я ищу что-то вроде этого:

func matchesForRegexInText(regex: String!, text: String!) -> [String] {
   ???
}

Итак, это то, что у меня есть:

func matchesForRegexInText(regex: String!, text: String!) -> [String] {

    var regex = NSRegularExpression(pattern: regex, 
        options: nil, error: nil)

    var results = regex.matchesInString(text, 
        options: nil, range: NSMakeRange(0, countElements(text))) 
            as Array<NSTextCheckingResult>

    /// ???

    return ...
}

Проблема заключается в том, что matchesInString предоставляет мне массив NSTextCheckingResult, где NSTextCheckingResult.range имеет тип NSRange.

NSRange несовместим с Range<String.Index>, поэтому он не позволяет мне использовать text.substringWithRange(...)

Любая идея, как добиться этой простой вещи в быстром без слишком много строк кода?

Теги:
string

9 ответов

253
Лучший ответ

Даже если метод matchesInString() принимает в качестве первого аргумента String он работает внутри с NSString, и параметр диапазона должен быть указан используя длину NSString, а не длину строки Swift. В противном случае fail для "расширенных кластеров графемы", таких как "флаги".

Как и у Swift 4 (Xcode 9), стандарт Swift библиотека предоставляет функции для преобразования между Range<String.Index> и NSRange.

func matches(for regex: String, in text: String) -> [String] {

    do {
        let regex = try NSRegularExpression(pattern: regex)
        let results = regex.matches(in: text,
                                    range: NSRange(text.startIndex..., in: text))
        return results.map {
            String(text[Range($0.range, in: text)!])
        }
    } catch let error {
        print("invalid regex: \(error.localizedDescription)")
        return []
    }
}

Пример:

let string = "€4€9"
let matched = matches(for: "[0-9]", in: string)
print(matched)
// ["4", "9"]

Примечание: принудительное разворачивание Range($0.range, in: text)! безопасно, потому что NSRange относится к подстроке заданной строки text. Однако, если вы хотите избежать этого, используйте

        return results.flatMap {
            Range($0.range, in: text).map { String(text[$0]) }
        }

вместо.


(Более старый ответ для Swift 3 и ранее:)

Итак, вы должны преобразовать заданную строку Swift в NSString, а затем извлечь диапазоны. Результат будет автоматически преобразован в строковый массив Swift.

(Код для Swift 1.2 можно найти в истории изменений.)

Swift 2 (Xcode 7.3.1):

func matchesForRegexInText(regex: String, text: String) -> [String] {

    do {
        let regex = try NSRegularExpression(pattern: regex, options: [])
        let nsString = text as NSString
        let results = regex.matchesInString(text,
                                            options: [], range: NSMakeRange(0, nsString.length))
        return results.map { nsString.substringWithRange($0.range)}
    } catch let error as NSError {
        print("invalid regex: \(error.localizedDescription)")
        return []
    }
}

Пример:

let string = "€4€9"
let matches = matchesForRegexInText("[0-9]", text: string)
print(matches)
// ["4", "9"]

Swift 3 (Xcode 8)

func matches(for regex: String, in text: String) -> [String] {

    do {
        let regex = try NSRegularExpression(pattern: regex)
        let nsString = text as NSString
        let results = regex.matches(in: text, range: NSRange(location: 0, length: nsString.length))
        return results.map { nsString.substring(with: $0.range)}
    } catch let error {
        print("invalid regex: \(error.localizedDescription)")
        return []
    }
}

Пример:

let string = "€4€9"
let matched = matches(for: "[0-9]", in: string)
print(matched)
// ["4", "9"]
  • 6
    Вы спасли меня от безумия. Без шуток. Спасибо вам большое!
  • 1
    @MathijsSegers: я обновил код для Swift 1.2 / Xcode 6.3. Спасибо, что дали мне знать!
Показать ещё 17 комментариев
44

Мой ответ основывается на ответах на эти вопросы, но делает регулярное сопоставление более надежным, добавляя дополнительную поддержку:

  • Возвращает не только совпадения, но также возвращает все группы захвата для каждого совпадения (см. Примеры ниже)
  • Вместо того, чтобы возвращать пустой массив, это решение поддерживает дополнительные совпадения
  • Avoids do/catch, не печатая на консоль, и использует конструкцию guard
  • Добавляет matchingStrings как расширение к String

Swift 4.2

//: Playground - noun: a place where people can play

import Foundation

extension String {
    func matchingStrings(regex: String) -> [[String]] {
        guard let regex = try? NSRegularExpression(pattern: regex, options: []) else { return [] }
        let nsString = self as NSString
        let results  = regex.matches(in: self, options: [], range: NSMakeRange(0, nsString.length))
        return results.map { result in
            (0..<result.numberOfRanges).map {
                result.range(at: $0).location != NSNotFound
                    ? nsString.substring(with: result.range(at: $0))
                    : ""
            }
        }
    }
}

"prefix12 aaa3 prefix45".matchingStrings(regex: "fix([0-9])([0-9])")
// Prints: [["fix12", "1", "2"], ["fix45", "4", "5"]]

"prefix12".matchingStrings(regex: "(?:prefix)?([0-9]+)")
// Prints: [["prefix12", "12"]]

"12".matchingStrings(regex: "(?:prefix)?([0-9]+)")
// Prints: [["12", "12"]], other answers return an empty array here

// Safely accessing the capture of the first match (if any):
let number = "prefix12suffix".matchingStrings(regex: "fix([0-9]+)su").first?[1]
// Prints: Optional("12")

Swift 3

//: Playground - noun: a place where people can play

import Foundation

extension String {
    func matchingStrings(regex: String) -> [[String]] {
        guard let regex = try? NSRegularExpression(pattern: regex, options: []) else { return [] }
        let nsString = self as NSString
        let results  = regex.matches(in: self, options: [], range: NSMakeRange(0, nsString.length))
        return results.map { result in
            (0..<result.numberOfRanges).map {
                result.rangeAt($0).location != NSNotFound
                    ? nsString.substring(with: result.rangeAt($0))
                    : ""
            }
        }
    }
}

"prefix12 aaa3 prefix45".matchingStrings(regex: "fix([0-9])([0-9])")
// Prints: [["fix12", "1", "2"], ["fix45", "4", "5"]]

"prefix12".matchingStrings(regex: "(?:prefix)?([0-9]+)")
// Prints: [["prefix12", "12"]]

"12".matchingStrings(regex: "(?:prefix)?([0-9]+)")
// Prints: [["12", "12"]], other answers return an empty array here

// Safely accessing the capture of the first match (if any):
let number = "prefix12suffix".matchingStrings(regex: "fix([0-9]+)su").first?[1]
// Prints: Optional("12")

Swift 2

extension String {
    func matchingStrings(regex: String) -> [[String]] {
        guard let regex = try? NSRegularExpression(pattern: regex, options: []) else { return [] }
        let nsString = self as NSString
        let results  = regex.matchesInString(self, options: [], range: NSMakeRange(0, nsString.length))
        return results.map { result in
            (0..<result.numberOfRanges).map {
                result.rangeAtIndex($0).location != NSNotFound
                    ? nsString.substringWithRange(result.rangeAtIndex($0))
                    : ""
            }
        }
    }
}
  • 0
    Хорошая идея о захвате групп. Но почему «охранник» более быстрый, чем «делать / ловить» ??
  • 0
    Я согласен с такими людьми, как nshipster.com/guard-and-defer, которые говорят, что Swift 2.0, безусловно, поощряет стиль раннего возврата [...], а не вложенных операторов if . То же самое верно для вложенных операторов do / catch ИМХО.
Показать ещё 5 комментариев
10

Если вы хотите извлечь подстроки из строки, а не только позиции (но фактическая строка, включая emojis). Тогда возможно более простое решение.

extension String {
  func regex (pattern: String) -> [String] {
    do {
      let regex = try NSRegularExpression(pattern: pattern, options: NSRegularExpressionOptions(rawValue: 0))
      let nsstr = self as NSString
      let all = NSRange(location: 0, length: nsstr.length)
      var matches : [String] = [String]()
      regex.enumerateMatchesInString(self, options: NSMatchingOptions(rawValue: 0), range: all) {
        (result : NSTextCheckingResult?, _, _) in
        if let r = result {
          let result = nsstr.substringWithRange(r.range) as String
          matches.append(result)
        }
      }
      return matches
    } catch {
      return [String]()
    }
  }
} 

Пример использования:

"someText ️ pig".regex("️")

Вернет следующее:

["️"]

Примечание с использованием "\ w +" может привести к неожиданному "

"someText ️ pig".regex("\\w+")

Вернет этот массив строк

["someText", "️", "pig"]
  • 0
    Это то что я хотел
  • 1
    Ницца! Для Swift 3 нужна небольшая настройка, но это здорово.
9

Я обнаружил, что принятое решение для решения, к сожалению, не компилируется на Swift 3 для Linux. Вот тогда модифицированная версия, которая делает:

import Foundation

func matches(for regex: String, in text: String) -> [String] {
    do {
        let regex = try RegularExpression(pattern: regex, options: [])
        let nsString = NSString(string: text)
        let results = regex.matches(in: text, options: [], range: NSRange(location: 0, length: nsString.length))
        return results.map { nsString.substring(with: $0.range) }
    } catch let error {
        print("invalid regex: \(error.localizedDescription)")
        return []
    }
}

Основные отличия:

  • Swift в Linux, по-видимому, требует сбросить префикс NS на объектах Foundation, для которого нет родственного эквивалента Swift. (См. Предложение по быстрой эволюции # 86.)

  • Swift в Linux также требует указания аргументов options для инициализации RegularExpression и matches.

  • По какой-то причине принуждение a String к NSString не работает в Swift в Linux, но инициализирует новый NSString с String по мере того, как источник работает.

Эта версия также работает с Swift 3 на macOS/Xcode с единственным исключением, что вы должны использовать имя NSRegularExpression вместо RegularExpression.

4

@p4bloch, если вы хотите захватить результаты из серии скобок, тогда вам нужно использовать rangeAtIndex(index) метод NSTextCheckingResult вместо range. Здесь метод @MartinR для Swift2 сверху, адаптированный для круглых скобок. В возвращаемом массиве первый результат [0] - это весь захват, а затем отдельные группы захвата начинаются с [1]. Я прокомментировал операцию map (чтобы было легче увидеть, что я изменил) и заменил ее вложенными циклами.

func matches(for regex: String!, in text: String!) -> [String] {

    do {
        let regex = try NSRegularExpression(pattern: regex, options: [])
        let nsString = text as NSString
        let results = regex.matchesInString(text, options: [], range: NSMakeRange(0, nsString.length))
        var match = [String]()
        for result in results {
            for i in 0..<result.numberOfRanges {
                match.append(nsString.substringWithRange( result.rangeAtIndex(i) ))
            }
        }
        return match
        //return results.map { nsString.substringWithRange( $0.range )} //rangeAtIndex(0)
    } catch let error as NSError {
        print("invalid regex: \(error.localizedDescription)")
        return []
    }
}

Пример использования может быть, скажем, вы хотите разделить строку title year, например "Finding Dory 2016", вы можете сделать это:

print ( matches(for: "^(.+)\\s(\\d{4})" , in: "Finding Dory 2016"))
// ["Finding Dory 2016", "Finding Dory", "2016"]
  • 0
    Этот ответ сделал мой день. Я потратил 2 часа на поиски решения, которое могло бы удовлетворить обычное выражение с дополнительным захватом групп.
  • 0
    Это работает, но он потерпит крах, если какой-либо диапазон не найден. Я изменил этот код так, чтобы функция возвращала [String?] в блоке for i in 0..<result.numberOfRanges вы должны добавить тест, который добавляет совпадение только в том случае, если диапазон! = NSNotFound , в противном случае он должен добавить ноль. См .: stackoverflow.com/a/31892241/2805570
3

Большинство вышеперечисленных решений дают полное совпадение, в результате игнорируя группы захвата, например: ^\d +\s + (\ d +)

Чтобы получить соответствие группе захвата, вам нужно что-то вроде (Swift4):

public extension String {
    public func capturedGroups(withRegex pattern: String) -> [String] {
        var results = [String]()

        var regex: NSRegularExpression
        do {
            regex = try NSRegularExpression(pattern: pattern, options: [])
        } catch {
            return results
        }
        let matches = regex.matches(in: self, options: [], range: NSRange(location:0, length: self.count))

        guard let match = matches.first else { return results }

        let lastRangeIndex = match.numberOfRanges - 1
        guard lastRangeIndex >= 1 else { return results }

        for i in 1...lastRangeIndex {
            let capturedGroupIndex = match.range(at: i)
            let matchedString = (self as NSString).substring(with: capturedGroupIndex)
            results.append(matchedString)
        }

        return results
    }
}
  • 0
    Это замечательно, если вы хотите получить только первый результат, чтобы получить каждый необходимый результат for index in 0..<matches.count { вокруг let lastRange... results.append(matchedString)}
  • 0
    предложение for должно выглядеть следующим образом: for i in 1...lastRangeIndex { let capturedGroupIndex = match.range(at: i) if capturedGroupIndex.location != NSNotFound { let matchedString = (self as NSString).substring(with: capturedGroupIndex) results.append(matchedString.trimmingCharacters(in: .whitespaces)) } }
2

Это очень простое решение, которое возвращает массив строк со спичками

Swift 3.

internal func stringsMatching(regularExpressionPattern: String, options: NSRegularExpression.Options = []) -> [String] {
        guard let regex = try? NSRegularExpression(pattern: regularExpressionPattern, options: options) else {
            return []
        }

        let nsString = self as NSString
        let results = regex.matches(in: self, options: [], range: NSMakeRange(0, nsString.length))

        return results.map {
            nsString.substring(with: $0.range)
        }
    }
1

Вот как я это сделал, я надеюсь, что это привнесет новую перспективу, как это работает на Swift.

В этом примере ниже я получу любую строку между []

var sample = "this is an [hello] amazing [world]"

var regex = NSRegularExpression(pattern: "\\[.+?\\]"
, options: NSRegularExpressionOptions.CaseInsensitive 
, error: nil)

var matches = regex?.matchesInString(sample, options: nil
, range: NSMakeRange(0, countElements(sample))) as Array<NSTextCheckingResult>

for match in matches {
   let r = (sample as NSString).substringWithRange(match.range)//cast to NSString is required to match range format.
    println("found= \(r)")
}
0

Огромное спасибо Ларсу Блумбергу за ответ на сбор групп и полные матчи с Swift 4, которые очень помогли мне. Я также добавил к нему для людей, которые действительно хотят error.localizedDescription, когда их регулярное выражение недействительно:

extension String {
    func matchingStrings(regex: String) -> [[String]] {
        do {
            let regex = try NSRegularExpression(pattern: regex)
            let nsString = self as NSString
            let results  = regex.matches(in: self, options: [], range: NSMakeRange(0, nsString.length))
            return results.map { result in
                (0..<result.numberOfRanges).map {
                    result.range(at: $0).location != NSNotFound
                        ? nsString.substring(with: result.range(at: $0))
                        : ""
                }
            }
        } catch let error {
            print("invalid regex: \(error.localizedDescription)")
            return []
        }
    }
}

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню