Schnelle Regex-Übereinstimmungen mit Extrakt

175

Ich möchte Teilzeichenfolgen aus einer Zeichenfolge extrahieren, die einem Regex-Muster entsprechen.

Also suche ich so etwas:

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

Das habe ich also:

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 ...
}

Das Problem ist, dass matchesInStringich eine Reihe von NSTextCheckingResult, wo NSTextCheckingResult.rangeist vom Typ liefert NSRange.

NSRangeist nicht kompatibel mit Range<String.Index>, daher verhindert es, dass ich es benutzetext.substringWithRange(...)

Haben Sie eine Idee, wie Sie diese einfache Sache schnell und ohne zu viele Codezeilen erreichen können?

Mitchellkman
quelle

Antworten:

313

Selbst wenn die matchesInString()Methode a Stringals erstes Argument verwendet, funktioniert sie intern mit NSString, und der Bereichsparameter muss unter Verwendung der NSStringLänge und nicht als Swift-Zeichenfolgenlänge angegeben werden. Andernfalls schlägt dies für "erweiterte Graphemcluster" wie "Flags" fehl.

Ab Swift 4 (Xcode 9) bietet die Swift-Standardbibliothek Funktionen zum Konvertieren zwischen Range<String.Index> und 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 []
    }
}

Beispiel:

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

Hinweis: Das erzwungene Entpacken Range($0.range, in: text)!ist sicher, da sich das NSRangeauf einen Teilstring der angegebenen Zeichenfolge bezieht text. Wenn Sie dies jedoch vermeiden möchten, verwenden Sie

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

stattdessen.


(Ältere Antwort für Swift 3 und früher :)

Sie sollten also die angegebene Swift-Zeichenfolge in eine konvertieren NSStringund dann die Bereiche extrahieren. Das Ergebnis wird automatisch in ein Swift-String-Array konvertiert.

(Der Code für Swift 1.2 befindet sich im Bearbeitungsverlauf.)

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

Beispiel:

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

Beispiel:

let string = "🇩🇪€4€9"
let matched = matches(for: "[0-9]", in: string)
print(matched)
// ["4", "9"]
Martin R.
quelle
9
Du hast mich davor bewahrt, verrückt zu werden. Kein Scherz. Ich danke dir sehr!
Mitchellkman
1
@MathijsSegers: Ich habe den Code für Swift 1.2 / Xcode 6.3 aktualisiert. Danke für die Information!
Martin R
1
aber was ist, wenn ich nach Zeichenfolgen zwischen einem Tag suchen möchte? Ich benötige das gleiche Ergebnis (Übereinstimmungsinformationen) wie: regex101.com/r/cU6jX8/2 . Welches Regex-Muster würden Sie vorschlagen?
Peter Kreinz
Das Update ist für Swift 1.2, nicht für Swift 2. Der Code wird nicht mit Swift 2 kompiliert.
PatrickNLT
1
Vielen Dank! Was ist, wenn Sie nur extrahieren möchten, was tatsächlich zwischen () in der Regex liegt? Zum Beispiel möchte ich in "[0-9] {3} ([0-9] {6})" nur die letzten 6 Zahlen erhalten.
p4bloch
64

Meine Antwort baut auf den gegebenen Antworten auf, macht aber den Regex-Abgleich durch Hinzufügen zusätzlicher Unterstützung robuster:

  • Gibt nicht nur Übereinstimmungen zurück, sondern auch alle Erfassungsgruppen für jede Übereinstimmung (siehe Beispiele unten).
  • Anstatt ein leeres Array zurückzugeben, unterstützt diese Lösung optionale Übereinstimmungen
  • Vermeidet, dass do/catchnicht auf der Konsole gedruckt wird, und verwendet das guardKonstrukt
  • Fügt matchingStringsals Erweiterung hinzuString

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))
                    : ""
            }
        }
    }
}
Lars Blumberg
quelle
1
Gute Idee zu den Capture-Gruppen. Aber warum ist "Wache" schneller als "tun / fangen"?
Martin R
Ich stimme Leuten wie nshipster.com/guard-and-defer zu, die sagen, dass Swift 2.0 sicherlich einen Stil der frühen Rückkehr [...] fördert, anstatt verschachtelte if-Aussagen . Gleiches gilt IMHO für verschachtelte do / catch-Anweisungen.
Lars Blumberg
try / catch ist die native Fehlerbehandlung in Swift. try?kann verwendet werden, wenn Sie nur am Ergebnis des Anrufs interessiert sind, nicht an einer möglichen Fehlermeldung. Also ja, guard try? ..ist in Ordnung, aber wenn Sie den Fehler drucken möchten, benötigen Sie einen Do-Block. Beide Wege sind schnell.
Martin R
3
Ich habe Unittests zu Ihrem netten Snippet hinzugefügt, gist.github.com/neoneye/03cbb26778539ba5eb609d16200e4522
neoneye
1
Ich wollte gerade meine eigene schreiben, basierend auf der @ MartinR-Antwort, bis ich das sah. Vielen Dank!
Oritm
13

Wenn Sie Teilzeichenfolgen aus einem String extrahieren möchten, nicht nur die Position (sondern den tatsächlichen String einschließlich Emojis). Dann ist das Folgende vielleicht eine einfachere Lösung.

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]()
    }
  }
} 

Anwendungsbeispiel:

"someText 👿🏅👿⚽️ pig".regex("👿⚽️")

Wird Folgendes zurückgeben:

["👿⚽️"]

Beachten Sie, dass die Verwendung von "\ w +" ein unerwartetes "" erzeugen kann.

"someText 👿🏅👿⚽️ pig".regex("\\w+")

Gibt dieses String-Array zurück

["someText", "️", "pig"]
Mike Chirico
quelle
1
Dies ist, was ich wollte
Kyle KIM
1
Nett! Für Swift 3 muss ein wenig angepasst werden, aber es ist großartig.
Jelle
@ Jelle was ist die Anpassung, die es braucht? Ich benutze Swift 5.1.3
Peter Schorn
9

Ich habe festgestellt, dass die Lösung der akzeptierten Antwort unter Swift 3 für Linux leider nicht kompiliert werden kann. Hier ist also eine modifizierte Version, die Folgendes tut:

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

Die Hauptunterschiede sind:

  1. Swift unter Linux scheint das Löschen des NSPräfixes für Foundation-Objekte zu erfordern, für die es kein Swift-natives Äquivalent gibt. (Siehe Swift Evolution-Vorschlag Nr. 86. )

  2. Für Swift unter Linux müssen außerdem die optionsArgumente sowohl für die RegularExpressionInitialisierung als auch für die matchesMethode angegeben werden.

  3. Aus irgendeinem Grund funktioniert das Erzwingen von a Stringin a unter NSStringSwift unter Linux nicht, aber das Initialisieren eines neuen NSStringmit a, Stringda die Quelle funktioniert.

Diese Version funktioniert auch mit Swift 3 unter macOS / Xcode, mit der einzigen Ausnahme, dass Sie den Namen NSRegularExpressionanstelle von verwenden müssen RegularExpression.

Rob Mecham
quelle
5

@ p4bloch Wenn Sie Ergebnisse aus einer Reihe von Erfassungsklammern erfassen möchten, müssen Sie stattdessen die rangeAtIndex(index)Methode von NSTextCheckingResultverwenden range. Hier ist die Methode von @MartinR für Swift2 von oben, angepasst für die Erfassung von Klammern. In dem zurückgegebenen Array ist das erste Ergebnis [0]die gesamte Erfassung, und dann beginnen einzelne Erfassungsgruppen [1]. Ich habe die mapOperation auskommentiert (damit ich leichter sehen kann, was ich geändert habe) und sie durch verschachtelte Schleifen ersetzt.

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

Ein Beispiel für einen Anwendungsfall könnte sein, dass Sie beispielsweise eine Zeichenfolge von title year"Finding Dory 2016" teilen möchten :

print ( matches(for: "^(.+)\\s(\\d{4})" , in: "Finding Dory 2016"))
// ["Finding Dory 2016", "Finding Dory", "2016"]
OliverD
quelle
Diese Antwort machte meinen Tag. Ich habe 2 Stunden lang nach einer Lösung gesucht, die den regulären Ausdruck durch die zusätzliche Erfassung von Gruppen befriedigt.
Ahmad
Dies funktioniert, stürzt jedoch ab, wenn kein Bereich gefunden wird. Ich habe diesen Code so geändert, dass die Funktion zurückgegeben wird. [String?]Im for i in 0..<result.numberOfRangesBlock müssen Sie einen Test hinzufügen, der die Übereinstimmung nur anfügt, wenn der Bereich! = Ist. NSNotFoundAndernfalls sollte er null anhängen. Siehe: stackoverflow.com/a/31892241/2805570
stef
4

Swift 4 ohne 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
quelle
Seien Sie vorsichtig mit der obigen Lösung: NSMakeRange(0, self.count)ist nicht korrekt, da selfes sich um ein String(= UTF8) und kein NSString(= UTF16) handelt. Das self.countist also nicht unbedingt dasselbe wie nsString.length(wie in anderen Lösungen verwendet). Sie können die Bereichsberechnung durchNSRange(self.startIndex..., in: self)
pd95
3

Die meisten der oben genannten Lösungen ergeben nur die vollständige Übereinstimmung, da die Erfassungsgruppen ignoriert werden, z. B.: ^ \ D + \ s + (\ d +)

Um die erwarteten Erfassungsgruppenübereinstimmungen zu erhalten, benötigen Sie Folgendes wie (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
quelle
Das ist großartig , wenn man nur das erste Ergebnis wollen , sind, um jedes Ergebnis zu bekommen es braucht for index in 0..<matches.count {umlet lastRange... results.append(matchedString)}
Geoff
Die for-Klausel sollte folgendermaßen aussehen: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

So habe ich es gemacht, ich hoffe, es bringt eine neue Perspektive, wie das bei Swift funktioniert.

In diesem Beispiel unten erhalte ich eine beliebige Zeichenfolge zwischen []

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
quelle
2

Dies ist eine sehr einfache Lösung, die ein Array von Zeichenfolgen mit den Übereinstimmungen zurückgibt

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)
        }
    }
Jorge Osorio
quelle
2

Der schnellste Weg, um alle Übereinstimmungen zurückzugeben und Gruppen in Swift 5 zu erfassen

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

Gibt ein zweidimensionales Array von Zeichenfolgen zurück:

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

kehrt zurück...

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

// First element of sub-array is the match
// All subsequent elements are the capture groups
Ken Mueller
quelle
0

Ein großes Dankeschön an Lars Blumberg für seine Antwort auf die Erfassung von Gruppen und vollständigen Spielen mit Swift 4 , was mir sehr geholfen hat. Ich habe es auch für die Personen hinzugefügt, die eine error.localizedDescription-Antwort wünschen, wenn ihre Regex ungültig ist:

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

Für mich hat es geholfen, die localizedDescription als Fehler zu verstehen, was beim Escape schief gelaufen ist, da sie anzeigt, welche endgültige Regex-Methode Swift zu implementieren versucht.

Vasco
quelle