Matchs d'expression régulière d'extrait Swift

175

Je souhaite extraire des sous-chaînes d'une chaîne correspondant à un modèle d'expression régulière.

Alors je cherche quelque chose comme ça:

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

Voici donc ce que j'ai:

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

Le problème est que cela matchesInStringme fournit un tableau de NSTextCheckingResult, où NSTextCheckingResult.rangeest de type NSRange.

NSRangeest incompatible avec Range<String.Index>, donc cela m'empêche d'utilisertext.substringWithRange(...)

Une idée comment réaliser cette chose simple en un rien de temps sans trop de lignes de code?

mitchkman
la source

Réponses:

313

Même si la matchesInString()méthode prend a Stringcomme premier argument, elle fonctionne en interne avec NSString, et le paramètre range doit être donné en utilisant la NSStringlongueur et non comme la longueur de la chaîne Swift. Sinon, il échouera pour les «clusters de graphèmes étendus» tels que les «indicateurs».

Depuis Swift 4 (Xcode 9), la bibliothèque standard Swift fournit des fonctions pour convertir entre Range<String.Index> et 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 []
    }
}

Exemple:

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

Remarque: le dépliage forcé Range($0.range, in: text)!est sûr car le NSRangefait référence à une sous-chaîne de la chaîne donnée text. Cependant, si vous voulez l'éviter, utilisez

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

au lieu.


(Ancienne réponse pour Swift 3 et versions antérieures :)

Vous devez donc convertir la chaîne Swift donnée en un NSString, puis extraire les plages. Le résultat sera automatiquement converti en un tableau de chaînes Swift.

(Le code de Swift 1.2 se trouve dans l'historique des modifications.)

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

Exemple:

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

Exemple:

let string = "🇩🇪€4€9"
let matched = matches(for: "[0-9]", in: string)
print(matched)
// ["4", "9"]
Martin R
la source
9
Vous m'avez sauvé de devenir fou. Sans blague. Merci beaucoup!
mitchkman
1
@MathijsSegers: J'ai mis à jour le code pour Swift 1.2 / Xcode 6.3. Merci de me le faire savoir!
Martin R
1
mais que faire si je veux rechercher des chaînes entre une balise? J'ai besoin du même résultat (informations de correspondance) comme: regex101.com/r/cU6jX8/2 . quel modèle de regex suggéreriez-vous?
Peter Kreinz
La mise à jour concerne Swift 1.2, pas Swift 2. Le code ne se compile pas avec Swift 2.
PatrickNLT
1
Merci! Que faire si vous voulez seulement extraire ce qui est réellement entre () dans l'expression régulière? Par exemple, dans "[0-9] {3} ([0-9] {6})", je ne voudrais obtenir que les 6 derniers chiffres.
p4bloch du
64

Ma réponse s'appuie sur les réponses données mais rend la correspondance regex plus robuste en ajoutant un support supplémentaire:

  • Renvoie non seulement des correspondances mais renvoie également tous les groupes de capture pour chaque correspondance (voir les exemples ci-dessous)
  • Au lieu de renvoyer un tableau vide, cette solution prend en charge les correspondances facultatives
  • Évite do/catchen n'imprimant pas sur la console et utilise la guardconstruction
  • S'ajoute matchingStringsen tant qu'extension àString

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
la source
1
Bonne idée des groupes de capture. Mais pourquoi "guard" est-il plus rapide que "do / catch" ??
Martin R
Je suis d'accord avec des gens comme nshipster.com/guard-and-defer qui disent que Swift 2.0 semble certainement encourager un style de retour anticipé [...] plutôt que des déclarations if imbriquées . Il en va de même pour les déclarations imbriquées do / catch IMHO.
Lars Blumberg
try / catch est la gestion native des erreurs dans Swift. try?peut être utilisé si vous n'êtes intéressé que par le résultat de l'appel, pas par un éventuel message d'erreur. Donc oui, guard try? ..c'est bien, mais si vous voulez imprimer l'erreur, vous avez besoin d'un do-block. Les deux moyens sont Swifty.
Martin R
3
J'ai ajouté unittests à votre joli extrait, gist.github.com/neoneye/03cbb26778539ba5eb609d16200e4522
neoneye
1
J'étais sur le point d'écrire le mien sur la base de la réponse @MartinR jusqu'à ce que je voie cela. Merci!
Oritm
13

Si vous souhaitez extraire des sous-chaînes d'une chaîne, pas seulement la position (mais la chaîne réelle, y compris les émojis). Ensuite, ce qui suit peut-être une solution plus simple.

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

Exemple d'utilisation:

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

Renverra ce qui suit:

["👿⚽️"]

Notez que l'utilisation de "\ w +" peut produire un "" inattendu

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

Renverra ce tableau de chaînes

["someText", "️", "pig"]
Mike Chirico
la source
1
C'est ce que je voulais
Kyle KIM
1
Agréable! Il a besoin d'un petit ajustement pour Swift 3, mais c'est génial.
Jelle
@Jelle quel est l'ajustement dont elle a besoin? J'utilise swift 5.1.3
Peter Schorn
9

J'ai trouvé que la solution de la réponse acceptée ne se compile malheureusement pas sur Swift 3 pour Linux. Voici donc une version modifiée qui fait:

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

Les principales différences sont:

  1. Swift sous Linux semble nécessiter la suppression du NSpréfixe sur les objets Foundation pour lesquels il n'y a pas d'équivalent natif Swift. (Voir la proposition d'évolution Swift n ° 86. )

  2. Swift sur Linux nécessite également de spécifier les optionsarguments pour l' RegularExpressioninitialisation et la matchesméthode.

  3. Pour une raison quelconque, forcer a Stringdans un NSStringne fonctionne pas dans Swift sur Linux mais initialiser un nouveau NSStringavec a Stringcomme source fonctionne.

Cette version fonctionne également avec Swift 3 sur macOS / Xcode à la seule exception que vous devez utiliser le nom à la NSRegularExpressionplace de RegularExpression.

Rob Mecham
la source
5

@ p4bloch si vous souhaitez capturer les résultats d'une série de parenthèses de capture, vous devez utiliser la rangeAtIndex(index)méthode de NSTextCheckingResult, au lieu de range. Voici la méthode ci-dessus de @MartinR pour Swift2, adaptée pour les parenthèses de capture. Dans le tableau renvoyé, le premier résultat [0]est la capture entière, puis les groupes de capture individuels commencent à partir de [1]. J'ai commenté l' mapopération (il est donc plus facile de voir ce que j'ai changé) et l'ai remplacée par des boucles imbriquées.

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 exemple de cas d'utilisation pourrait être, disons que vous voulez diviser une chaîne de title yearpar exemple "Finding Dory 2016", vous pouvez faire ceci:

print ( matches(for: "^(.+)\\s(\\d{4})" , in: "Finding Dory 2016"))
// ["Finding Dory 2016", "Finding Dory", "2016"]
OliverD
la source
Cette réponse a fait ma journée. J'ai passé 2 heures à chercher une solution qui puisse satisfaire l'expression régulière avec la capture supplémentaire de groupes.
Ahmad
Cela fonctionne mais il plantera si aucune plage n'est trouvée. J'ai modifié ce code pour que la fonction retourne [String?]et dans le for i in 0..<result.numberOfRangesbloc, vous devez ajouter un test qui n'ajoute la correspondance que si la plage! = NSNotFound, Sinon il devrait ajouter nil. Voir: stackoverflow.com/a/31892241/2805570
stef
4

Swift 4 sans 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
la source
Soyez prudent avec la solution ci-dessus: NSMakeRange(0, self.count)n'est pas correcte, car selfest un String(= UTF8) et non un NSString(= UTF16). Donc, le self.countn'est pas nécessairement le même que nsString.length(comme utilisé dans d'autres solutions). Vous pouvez remplacer le calcul de la plage parNSRange(self.startIndex..., in: self)
pd95 le
3

La plupart des solutions ci-dessus ne donnent que la correspondance complète en ignorant les groupes de capture, par exemple: ^ \ d + \ s + (\ d +)

Pour obtenir les correspondances du groupe de capture comme prévu, vous avez besoin de quelque chose comme (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
la source
Ceci est très bien si vous êtes désireux juste le premier résultat, pour obtenir chaque résultat dont il a besoin for index in 0..<matches.count {autourlet lastRange... results.append(matchedString)}
Geoff
la clause for devrait ressembler à ceci: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

C'est ainsi que je l'ai fait, j'espère que cela apporte une nouvelle perspective sur la façon dont cela fonctionne sur Swift.

Dans cet exemple ci-dessous, j'obtiendrai la chaîne de caractères entre []

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
la source
2

C'est une solution très simple qui retourne un tableau de chaîne avec les correspondances

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
la source
2

Le moyen le plus rapide de renvoyer tous les matchs et de capturer des groupes dans 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)) }
        } ?? []
    }
}

Renvoie un tableau bidimensionnel de chaînes:

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

Retour...

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

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

Un grand merci à Lars Blumberg pour sa réponse pour avoir capturé des groupes et des matchs complets avec Swift 4 , ce qui m'a beaucoup aidé. J'ai également fait un ajout pour les personnes qui veulent une réponse error.localizedDescription lorsque leur regex est invalide:

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

Pour moi, avoir la description localisée en tant qu'erreur m'a aidé à comprendre ce qui n'allait pas avec l'échappement, car il affiche le regex final swift tente d'implémenter.

Vasco
la source