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

175

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

Поэтому я ищу что-то вроде этого:

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(...)

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

mitchkman
источник

Ответы:

313

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

Начиная с 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 в a, 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"]
Мартин Р
источник
9
Вы спасли меня от безумия. Без шуток. Спасибо вам большое!
mitchkman
1
@MathijsSegers: я обновил код для Swift 1.2 / Xcode 6.3. Спасибо, что дал мне знать!
Мартин Р
1
но что, если я хочу искать строки между тегами? Мне нужен тот же результат (информация о матче), как: regex101.com/r/cU6jX8/2 . какой шаблон регулярного выражения вы бы предложили?
Питер Крейнз,
Обновление предназначено для Swift 1.2, а не для Swift 2. Код не компилируется со Swift 2.
PatrickNLT
1
Спасибо! Что если вы хотите извлечь только то, что на самом деле между () в регулярном выражении? Например, в «[0-9] {3} ([0-9] {6})» я хотел бы получить только последние 6 чисел.
p4bloch
64

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

  • Возвращает не только совпадения, но и все группы захвата для каждого совпадения (см. Примеры ниже)
  • Вместо возврата пустого массива это решение поддерживает необязательные совпадения
  • Позволяет избежать 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))
                    : ""
            }
        }
    }
}
Ларс Блумберг
источник
1
Хорошая идея о захвате групп. Но почему «охранник» быстрее, чем «делать / ловить» ??
Мартин Р
Я согласен с такими людьми, как nshipster.com/guard-and-defer, которые говорят, что Swift 2.0, безусловно, поощряет стиль раннего возврата [...], а не вложенные операторы if . То же самое верно для вложенных операторов do / catch ИМХО.
Ларс Блумберг
try / catch - это встроенная обработка ошибок в Swift. try?может использоваться, если вас интересует только исход вызова, а не сообщение о возможной ошибке. Так что да, guard try? ..хорошо, но если вы хотите напечатать ошибку, вам нужен do-block. Оба способа Swifty.
Мартин Р
3
Я добавил юнит-тесты в ваш хороший фрагмент, gist.github.com/neoneye/03cbb26778539ba5eb609d16200e4522
neoneye
1
Собирался написать свой, основываясь на ответе @MartinR, пока не увидел это. Спасибо!
Oritm
13

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

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"]
Майк Кирико
источник
1
Это то, что я хотел
Кайл КИМ
1
Ницца! Для Swift 3 нужна небольшая настройка, но это здорово.
Jelle
@Jelle, какая настройка нужна? Я использую Swift 5.1.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 []
    }
}

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

  1. Swift в Linux, кажется, требует удаления NSпрефикса для объектов Foundation, для которых нет собственного эквивалента Swift. (См. Предложение Swift Evolution № 86. )

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

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

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

Роб Мечам
источник
5

@ 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например, «В поисках Дори 2016», вы можете сделать это:

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

Swift 4 без NSString.

extension String {
    func matches(regex: String) -> [String] {
        guard let regex = try? NSRegularExpression(pattern: regex, options: [.caseInsensitive]) else { return [] }
        let matches  = regex.matches(in: self, options: [], range: NSMakeRange(0, self.count))
        return matches.map { match in
            return String(self[Range(match.range, in: self)!])
        }
    }
}
shiami
источник
Будьте осторожны с приведенным выше решением: NSMakeRange(0, self.count)это не правильно, потому что selfэто String(= UTF8), а не NSString(= UTF16). Таким образом, self.countэто не обязательно так же, как nsString.length(как используется в других решениях). Вы можете заменить расчет диапазона наNSRange(self.startIndex..., in: self)
pd95
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
    }
}
valexa
источник
Это очень удобно , если Вы желаете только первый результат, чтобы получить каждый результат он должен for index in 0..<matches.count {вокругlet lastRange... results.append(matchedString)}
Джеффа
предложение 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)) } }
CRE8IT
2

Вот как я это сделал, я надеюсь, что это даст новый взгляд на то, как это работает на 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)")
}
Dalorzo
источник
2

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

Свифт 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)
        }
    }
Хорхе Осорио
источник
2

Самый быстрый способ вернуть все совпадения и захватить группы в Swift 5

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

Возвращает 2-мерный массив строк:

"prefix12suffix fix1su".match("fix([0-9]+)su")

возвращается ...

[["fix12su", "12"], ["fix1su", "1"]]

// First element of sub-array is the match
// All subsequent elements are the capture groups
Кен Мюллер
источник
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 как ошибки помогло понять, что пошло не так с экранированием, так как оно показывает, какой последний regex swift пытается реализовать.

Vasco
источник