Szybkie dopasowywanie wyrażeń regularnych ekstrakcji

175

Chcę wyodrębnić podciągi z ciągu pasującego do wzorca wyrażenia regularnego.

Więc szukam czegoś takiego:

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

Więc oto co mam:

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

Problem w tym, że matchesInStringdostarcza mi tablicę NSTextCheckingResultgdzie NSTextCheckingResult.rangejest typu NSRange.

NSRangejest niekompatybilny z Range<String.Index>, więc uniemożliwia mi używanietext.substringWithRange(...)

Masz pomysł, jak szybko osiągnąć tę prostą rzecz bez zbyt wielu wierszy kodu?

mitchkman
źródło

Odpowiedzi:

313

Nawet jeśli matchesInString()metoda przyjmuje a Stringjako pierwszy argument, działa wewnętrznie z NSString, a parametr range musi być podawany przy użyciu NSStringdługości, a nie jako długości łańcucha Swift. W przeciwnym razie zakończy się niepowodzeniem dla „rozszerzonych klastrów grafemów”, takich jak „flagi”.

Począwszy od Swift 4 (Xcode 9), standardowa biblioteka Swift zapewnia funkcje do konwersji między Range<String.Index> i 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 []
    }
}

Przykład:

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

Uwaga: wymuszone rozpakowanie Range($0.range, in: text)!jest bezpieczne, ponieważ NSRangeodwołuje się do podłańcucha danego ciągu text. Jeśli jednak chcesz tego uniknąć, użyj

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

zamiast.


(Starsza odpowiedź dla Swift 3 i wcześniejszych :)

Dlatego powinieneś przekonwertować podany ciąg Swift na ciąg, NSStringa następnie wyodrębnić zakresy. Wynik zostanie automatycznie przekonwertowany na tablicę ciągów Swift.

(Kod dla Swift 1.2 można znaleźć w historii edycji).

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

Przykład:

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

Przykład:

let string = "🇩🇪€4€9"
let matched = matches(for: "[0-9]", in: string)
print(matched)
// ["4", "9"]
Martin R.
źródło
9
Uratowałeś mnie od szaleństwa. Nie żartuję. Dziękuję bardzo!
mitchkman
1
@MathijsSegers: Zaktualizowałem kod dla Swift 1.2 / Xcode 6.3. Dzięki, że dałeś mi znać!
Martin R
1
ale co, jeśli chcę szukać ciągów między tagami? Potrzebuję tego samego wyniku (informacji o dopasowaniu), na przykład: regex101.com/r/cU6jX8/2 . który wzorzec regex byś zaproponował?
Peter Kreinz
Aktualizacja dotyczy Swift 1.2, a nie Swift 2. Kod nie kompiluje się ze Swift 2.
PatrickNLT
1
Dzięki! A co, jeśli chcesz tylko wyodrębnić to, co faktycznie znajduje się między () w wyrażeniu regularnym? Na przykład w „[0-9] {3} ([0-9] {6})” chciałbym uzyskać tylko 6 ostatnich liczb.
p4bloch
64

Moja odpowiedź opiera się na udzielonych odpowiedziach, ale sprawia, że ​​dopasowywanie wyrażeń regularnych jest bardziej niezawodne, dodając dodatkowe wsparcie:

  • Zwraca nie tylko dopasowania, ale także wszystkie grupy przechwytywania dla każdego dopasowania (patrz przykłady poniżej)
  • Zamiast zwracać pustą tablicę, to rozwiązanie obsługuje opcjonalne dopasowania
  • Unika do/catch, nie drukując na konsoli i używa guardkonstrukcji
  • Dodaje matchingStringsjako rozszerzenie doString

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

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

Szybki 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
źródło
1
Dobry pomysł na temat grup przechwytywania. Ale dlaczego „strażnik” jest szybszy niż „zrób / złap”?
Martin R
Zgadzam się z ludźmi takimi jak nshipster.com/guard-and-defer, którzy twierdzą, że Swift 2.0 z pewnością wydaje się zachęcać raczej do stylu wczesnego powrotu [...] niż zagnieżdżania stwierdzeń if . To samo dotyczy zagnieżdżonych instrukcji do / catch IMHO.
Lars Blumberg
try / catch to natywna obsługa błędów w języku Swift. try?można użyć, jeśli interesuje Cię tylko wynik połączenia, a nie ewentualny komunikat o błędzie. Więc tak, guard try? ..jest w porządku, ale jeśli chcesz wydrukować błąd, potrzebujesz bloku do. Oba sposoby są szybkie.
Martin R
3
Dodałem unittests do twojego fajnego fragmentu, gist.github.com/neoneye/03cbb26778539ba5eb609d16200e4522
neoneye
1
Miałem napisać własną na podstawie odpowiedzi @MartinR, dopóki tego nie zobaczyłem. Dzięki!
Oritm
13

Jeśli chcesz wyodrębnić podciągi z ciągu znaków, a nie tylko pozycję (ale rzeczywisty ciąg zawierający emotikony). Następujące może być prostszym rozwiązaniem.

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

Przykładowe zastosowanie:

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

Zwróci następujące informacje:

["👿⚽️"]

Uwaga po użyciu „\ w +” może spowodować nieoczekiwany „”

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

Zwróci tę tablicę String

["someText", "️", "pig"]
Mike Chirico
źródło
1
Tego właśnie chciałem
Kyle KIM,
1
Miły! Wymaga niewielkiej korekty dla Swift 3, ale jest świetny.
Jelle,
@Jelle, jakiej regulacji potrzebuje? Używam szybkiego 5.1.3
Peter Schorn
9

Okazało się, że rozwiązanie zaakceptowanej odpowiedzi niestety nie kompiluje się na Swift 3 dla systemu Linux. Oto zmodyfikowana wersja, która robi:

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

Główne różnice to:

  1. Wydaje się, że język Swift w systemie Linux wymaga usunięcia NSprefiksu w obiektach Foundation, dla których nie ma odpowiednika w języku Swift. (Zobacz propozycję szybkiej ewolucji nr 86 ).

  2. Swift w systemie Linux wymaga również określenia optionsargumentów zarówno dla RegularExpressioninicjalizacji, jak i matchesmetody.

  3. Z jakiegoś powodu wymuszenie a Stringna an NSStringnie działa w Swift w systemie Linux, ale inicjowanie nowego za NSStringpomocą a Stringjako źródła działa.

Ta wersja działa również ze Swift 3 w systemie macOS / Xcode z jedynym wyjątkiem, że musisz użyć nazwy NSRegularExpressionzamiast RegularExpression.

Rob Mecham
źródło
5

@ p4bloch jeśli chcesz przechwycić wyniki z serii przechwytywania nawiasów, musisz użyć rangeAtIndex(index)metody NSTextCheckingResultzamiast range. Oto metoda @MartinR dla Swift2 z góry, dostosowana do przechwytywania nawiasów. W zwracanej tablicy pierwszym wynikiem [0]jest całe przechwytywanie, a następnie poszczególne grupy przechwytywania rozpoczynają się od [1]. Skomentowałem mapoperację (aby łatwiej było zobaczyć, co zmieniłem) i zastąpiłem ją zagnieżdżonymi pętlami.

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

Przykładowym przypadkiem użycia może być, powiedzmy, że chcesz podzielić ciąg title yearnp. „Finding Dory 2016”, możesz to zrobić:

print ( matches(for: "^(.+)\\s(\\d{4})" , in: "Finding Dory 2016"))
// ["Finding Dory 2016", "Finding Dory", "2016"]
OliverD
źródło
Ta odpowiedź zrobiła mój dzień. Spędziłem 2 godziny na poszukiwaniu rozwiązania, które zadowoli regularną ekspresję z dodatkowym przechwytywaniem grup.
Ahmad,
To działa, ale ulegnie awarii, jeśli nie zostanie znaleziony żaden zakres. Zmodyfikowałem ten kod tak, że funkcja zwraca [String?]iw for i in 0..<result.numberOfRangesbloku trzeba dodać test, który dopisuje dopasowanie tylko wtedy, gdy zakres! = NSNotFound, W przeciwnym razie powinien dopisać nil. Zobacz: stackoverflow.com/a/31892241/2805570
stef
4

Swift 4 bez 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
źródło
Uważaj na powyższe rozwiązanie: NSMakeRange(0, self.count)nie jest poprawne, ponieważ selfjest String(= UTF8), a nie NSString(= UTF16). Więc self.countniekoniecznie jest to to samo, co nsString.length(używane w innych rozwiązaniach). Obliczenie zakresu można zamienić naNSRange(self.startIndex..., in: self)
pd95
3

Większość powyższych rozwiązań daje tylko pełne dopasowanie, ignorując grupy przechwytywania, np .: ^ \ d + \ s + (\ d +)

Aby dopasować grupy przechwytywania zgodnie z oczekiwaniami, potrzebujesz czegoś takiego jak (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
źródło
To jest wielki, jeśli chce tylko pierwszy wynik, aby każdy wynik musi for index in 0..<matches.count {dookołalet lastRange... results.append(matchedString)}
Geoff
klauzula for powinna wyglądać następująco: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

Tak to zrobiłem, mam nadzieję, że to przyniesie nową perspektywę, jak to działa w Swift.

W tym przykładzie poniżej otrzymam dowolny ciąg między []

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
źródło
2

To bardzo proste rozwiązanie, które zwraca tablicę łańcuchów z dopasowaniami

Szybki 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
źródło
2

Najszybszy sposób na zwrócenie wszystkich meczów i grup przechwytywania w 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)) }
        } ?? []
    }
}

Zwraca dwuwymiarową tablicę ciągów:

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

zwroty...

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

// First element of sub-array is the match
// All subsequent elements are the capture groups
Ken Mueller
źródło
0

Wielkie dzięki dla Larsa Blumberga za jego odpowiedź za przechwytywanie grup i pełnych meczy za pomocą Swift 4 , co bardzo mi pomogło. Dodałem również do tego dla osób, które chcą otrzymać odpowiedź error.localizedDescription, gdy ich wyrażenie regularne jest nieprawidłowe:

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

Dla mnie posiadanie localizedDescription jako błędu pomogło zrozumieć, co poszło nie tak z ucieczką, ponieważ wyświetla on, który ostateczny regex swift próbuje zaimplementować.

Vasco
źródło