Швидкий витяг збірки з регулярним виразом


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

Будь-яка ідея, як швидко досягти цієї простої речі без занадто багато рядків коду?

Відповіді:


313

Навіть якщо matchesInString()метод бере Stringяк перший аргумент, він працює внутрішньо 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 в 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

Швидкий 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")

Швидкий 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")

Швидкий 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, безумовно, заохочує стиль раннього повернення [...], а не вкладений у заяви . Те ж саме справедливо і для вкладених заяв IMHO про введення / улов.
Ларс Блюмберг

try / catch - це натисна помилка Swift. try?може використовуватися, якщо вас цікавить лише результат дзвінка, а не можливе повідомлення про помилку. Так так,guard try? .. добре, але якщо ви хочете надрукувати помилку, тоді вам потрібен do-блок. Обидва способи стрімкі.
Мартін Р

3
Я додав одиничні
тести

1
Я збирався написати свій власний на основі відповіді @MartinR, поки я не побачив цього. Дякую!
Оритм

13

Якщо ви хочете витягнути підрядки з рядка, не лише позицію (а власне String, включаючи емоджи). Тоді наступне, можливо, простіше рішення.

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+")

Поверне цей масив String

["someText", "️", "pig"]

1
Цього я хотів
Кайл КІМ

1
Приємно! Для Swift 3 йому потрібно трохи налаштувати, але це чудово.
Джел

@Jelle, яке коригування воно потребує? Я використовую швидкий 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. (Див. Пропозицію про швидку еволюцію № 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наприклад, "Пошук Dory 2016", ви можете це зробити:

print ( matches(for: "^(.+)\\s(\\d{4})" , in: "Finding Dory 2016"))
// ["Finding Dory 2016", "Finding Dory", "2016"]

Ця відповідь зробила мій день. Я витратив 2 години на пошук рішення, яке може задовольнити регуляторну експресію з додатковим захопленням груп.
Ахмад

Це працює, але воно завершиться, якщо будь-який діапазон не буде знайдений. Я змінив цей код, щоб функція поверталася, [String?]і в for i in 0..<result.numberOfRangesблоці ви повинні додати тест, який додає збіг лише у випадку, якщо діапазон! = NSNotFound, Інакше він повинен додати нуль. Дивіться: stackoverflow.com/a/31892241/2805570
stef

4

Швидкий 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)!])
        }
    }
}

Будьте уважні з вищевказаним рішенням: 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
    }
}

Це дуже зручно , якщо Ви бажаєте тільки перший результат, щоб отримати кожен результат він повинен for index in 0..<matches.count {навколоlet lastRange... results.append(matchedString)}
Джеффа

стаття для 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

Ось як я це зробив, сподіваюсь, це принесе нову перспективу, як це працює на Свіфті.

У цьому прикладі нижче я отримаю будь-яку рядок між []

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)")
}

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)) }
        } ?? []
    }
}

Повертає двовимірний масив рядків:

"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 []
        }
    }
}

Для мене наявність локалізованого опису як помилки допомогло зрозуміти, що пішло не так у втечі, оскільки він відображає, який остаточний регекс стрімко намагається реалізувати.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.