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.