Partite regex di estrazione rapida


175

Voglio estrarre sottostringhe da una stringa che corrisponde a un modello regex.

Quindi sto cercando qualcosa del genere:

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

Quindi questo è quello che ho:

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

Il problema è che matchesInStringmi offre un array di NSTextCheckingResultdove NSTextCheckingResult.rangeè di tipo NSRange.

NSRangeè incompatibile con Range<String.Index>, quindi mi impedisce di utilizzaretext.substringWithRange(...)

Qualche idea su come realizzare questa semplice cosa in breve tempo senza troppe righe di codice?

Risposte:


313

Anche se il matchesInString()metodo accetta a Stringcome primo argomento, funziona internamente con NSStringe il parametro range deve essere dato usando la NSStringlunghezza e non come la lunghezza della stringa Swift. Altrimenti non funzionerà per "cluster grapheme estesi" come "flags".

A partire da Swift 4 (Xcode 9), la libreria standard Swift fornisce funzioni per la conversione tra Range<String.Index> e 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 []
    }
}

Esempio:

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

Nota: lo scartamento forzato Range($0.range, in: text)!è sicuro perché si NSRangeriferisce a una sottostringa della stringa specificata text. Tuttavia, se si desidera evitarlo, utilizzare

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

anziché.


(Risposta precedente per Swift 3 e precedenti :)

Quindi dovresti convertire la stringa Swift specificata in un NSStringe quindi estrarre gli intervalli. Il risultato verrà convertito automaticamente in un array di stringhe Swift.

(Il codice per Swift 1.2 è disponibile nella cronologia delle modifiche.)

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

Esempio:

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

Esempio:

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

9
Mi hai salvato dal diventare pazzo. Non sto scherzando. Grazie mille!
Mitchkman,

1
@MathijsSegers: ho aggiornato il codice per Swift 1.2 / Xcode 6.3. Grazie per avermi fatto sapere!
Martin R,

1
ma cosa succede se voglio cercare stringhe tra un tag? Ho bisogno dello stesso risultato (informazioni sulla partita) come: regex101.com/r/cU6jX8/2 . quale schema regex suggeriresti?
Peter Kreinz,

L'aggiornamento è per Swift 1.2, non Swift 2. Il codice non viene compilato con Swift 2.
PatrickNLT

1
Grazie! Cosa succede se si desidera estrarre solo ciò che è effettivamente tra () nella regex? Ad esempio, in "[0-9] {3} ([0-9] {6})" Vorrei solo ottenere gli ultimi 6 numeri.
p4bloch,

64

La mia risposta si basa sulle risposte fornite ma rende più robusta la corrispondenza regex aggiungendo ulteriore supporto:

  • Restituisce non solo le corrispondenze, ma restituisce anche tutti i gruppi di acquisizione per ogni corrispondenza (vedere gli esempi seguenti)
  • Invece di restituire un array vuoto, questa soluzione supporta corrispondenze opzionali
  • Evita do/catchdi non stampare sulla console e fa uso del guardcostrutto
  • Aggiunge matchingStringscome estensione aString

Rapido 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
Buona idea sui gruppi di acquisizione. Ma perché "proteggere" è più veloce di "fare / catturare" ??
Martin R,

Sono d'accordo con persone come nshipster.com/guard-and-defer che sostengono che Swift 2.0 sembra certamente incoraggiare uno stile di ritorno [...] anticipato piuttosto che dichiarazioni nidificate se . Lo stesso vale per le istruzioni nidificate di do / catch IMHO.
Lars Blumberg,

try / catch è la gestione degli errori nativi in ​​Swift. try?può essere utilizzato se si è interessati solo all'esito della chiamata, non a un possibile messaggio di errore. Quindi sì, guard try? ..va bene, ma se si desidera stampare l'errore, è necessario un do-block. Entrambi i modi sono Swifty.
Martin R,

3
Ho aggiunto unittest al tuo bel frammento, gist.github.com/neoneye/03cbb26778539ba5eb609d16200e4522
neoneye,

1
Stava per scrivere il mio sulla base della risposta @MartinR fino a quando non ho visto questo. Grazie!
Oritm,

13

Se vuoi estrarre sottostringhe da una stringa, non solo la posizione (ma la stringa effettiva inclusi gli emoji). Quindi, la seguente forse una soluzione più semplice.

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

Esempio di utilizzo:

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

Restituirà il seguente:

["👿⚽️"]

Nota che l'utilizzo di "\ w +" può produrre un "" imprevisto

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

Restituirà questo array di stringhe

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

1
Questo è quello che volevo
Kyle KIM,

1
Bello! Ha bisogno di un piccolo aggiustamento per Swift 3, ma è fantastico.
Jelle,

@Jelle qual è l'adeguamento di cui ha bisogno? Sto usando il rapido 5.1.3
Peter Schorn il

9

Ho scoperto che la soluzione della risposta accettata purtroppo non si compila su Swift 3 per Linux. Ecco una versione modificata, quindi, che:

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

Le principali differenze sono:

  1. Swift su Linux sembra richiedere il rilascio del NSprefisso sugli oggetti Foundation per i quali non esiste un equivalente nativo di Swift. (Vedi la proposta Swift evolution n . 86 ).

  2. Swift su Linux richiede inoltre di specificare gli optionsargomenti sia per l' RegularExpressioninizializzazione che per il matchesmetodo.

  3. Per qualche motivo, Stringinserire a in NSStringnon funziona in Swift su Linux ma inizializzare un nuovo NSStringcon a Stringcome funziona l'origine.

Questa versione funziona anche con Swift 3 su macOS / Xcode con la sola eccezione che è necessario utilizzare il nome NSRegularExpressionanziché RegularExpression.


5

@ p4bloch se si desidera acquisire risultati da una serie di parentesi di acquisizione, è necessario utilizzare il rangeAtIndex(index)metodo di NSTextCheckingResult, anziché range. Ecco il metodo di @MartinR per Swift2 dall'alto, adattato per catturare le parentesi. Nell'array che viene restituito, il primo risultato [0]è l'intera acquisizione, quindi iniziano i singoli gruppi di acquisizione [1]. Ho commentato l' mapoperazione (quindi è più facile vedere cosa ho cambiato) e l'ho sostituita con loop annidati.

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

Un esempio di utilizzo potrebbe essere, supponiamo che tu voglia dividere una stringa, title yearad esempio "Alla ricerca di Dory 2016", potresti farlo:

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

Questa risposta ha reso la mia giornata. Ho trascorso 2 ore alla ricerca di una soluzione in grado di soddisfare l'espressione regualr con l'acquisizione aggiuntiva di gruppi.
Ahmad,

Funziona ma si arresta in modo anomalo se non viene trovato alcun intervallo. Ho modificato questo codice in modo che la funzione ritorni [String?]e nel for i in 0..<result.numberOfRangesblocco, è necessario aggiungere un test che accoda la corrispondenza solo se l'intervallo! = NSNotFound, Altrimenti dovrebbe aggiungere zero. Vedi: stackoverflow.com/a/31892241/2805570
stef

4

Swift 4 senza 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)!])
        }
    }
}

Fai attenzione con la soluzione sopra: NSMakeRange(0, self.count)non è corretta, perché selfè un String(= UTF8) e non un NSString(= UTF16). Quindi self.countnon è necessariamente lo stesso di nsString.length(usato in altre soluzioni). È possibile sostituire il calcolo dell'intervallo conNSRange(self.startIndex..., in: self)
pd95

3

La maggior parte delle soluzioni di cui sopra fornisce solo la corrispondenza completa di conseguenza ignorando i gruppi di acquisizione, ad esempio: ^ \ d + \ s + (\ d +)

Per ottenere le corrispondenze del gruppo di acquisizione come previsto è necessario qualcosa come (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
    }
}

Questo è grande se siete che vogliono solo il primo risultato, per ottenere ogni risultato di cui ha bisogno for index in 0..<matches.count {in girolet lastRange... results.append(matchedString)}
Geoff

la clausola for dovrebbe apparire così: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

È così che ho fatto, spero che porti una nuova prospettiva su come funziona su Swift.

In questo esempio di seguito otterrò la stringa tra []

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

Questa è una soluzione molto semplice che restituisce una matrice di stringhe con le corrispondenze

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

2

Il modo più veloce per restituire tutte le partite e acquisire gruppi in 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)) }
        } ?? []
    }
}

Restituisce una matrice di stringhe bidimensionali:

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

ritorna...

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

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

0

Un grande ringraziamento a Lars Blumberg per la sua risposta per catturare gruppi e partite complete con Swift 4 , che mi ha aiutato molto. Ho anche fatto un'aggiunta ad esso per le persone che vogliono una risposta error.localizedDescription quando il loro regex non è valido:

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

Per me avere la LocalizedDescription come errore mi ha aiutato a capire cosa è andato storto durante la fuga, poiché mostra quali regex swift finali tentano di implementare.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.