web-dev-qa-db-fra.com

Swift extraire les correspondances regex

Je veux extraire des sous-chaînes d'une chaîne qui correspond à un motif regex.

Donc je cherche quelque chose comme ça:

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

Donc voici 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 matchesInString me fournit un tableau de NSTextCheckingResult, où NSTextCheckingResult.range est de type NSRange.

NSRange est incompatible avec Range<String.Index>, ce qui m'empêche d'utiliser text.substringWithRange(...)

Avez-vous une idée de la façon de réaliser cette chose simple dans Swift sans trop de lignes de code?

155
mitchkman

Même si la méthode matchesInString() prend un premier argument avec String, elle fonctionne en interne avec NSString, et le paramètre range doit être défini à l'aide de la longueur NSString et non comme Swift longueur de chaîne. Sinon, cela échouera pour les "grappes de graphèmes étendues" telles que les "drapeaux".

À compter de Swift 4 (Xcode 9), la bibliothèque standard Swift fournit des fonctions permettant la conversion 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éroulement forcé Range($0.range, in: text)! est sécurisé, car NSRange fait 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 une chaîne NSString, puis extraire les plages. Le résultat sera automatiquement converti en un tableau de chaînes Swift.

(Le code pour Swift 1.2 se trouve dans l'historique d'édition.)

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"]
287
Martin R

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

  • Renvoie non seulement les correspondances mais renvoie également tous les groupes de capture pour chaque correspondance (voir les exemples ci-dessous)
  • Au lieu de retourner un tableau vide, cette solution supporte les correspondances optionnelles
  • Évite do/catch en n'imprimant pas sur la console et tilise la construction guard
  • Ajoute matchingStrings en tant que 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

//: 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))
                    : ""
            }
        }
    }
}
56
Lars Blumberg

Si vous souhaitez extraire des sous-chaînes d'une chaîne, pas seulement la position (mais également la chaîne, 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("????⚽️")

Retournera 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"]
12
Mike Chirico

J'ai trouvé que la solution de réponse acceptée ne compilait malheureusement pas sur Swift 3 pour Linux. Voici une version modifiée, alors, 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 les suivantes:

  1. Swift sur Linux semble nécessiter de supprimer le préfixe NS sur les objets Foundation pour lesquels il n’existe pas d’équivalent natif de Swift. (Voir Proposition d'évolution Swift n ° 86 .)

  2. Swift sur Linux nécessite également de spécifier les arguments options pour l’initialisation RegularExpression et la méthode matches.

  3. Pour une raison quelconque, contraindre un String dans un NSString ne fonctionne pas dans Swift sous Linux, mais initialise un nouveau NSString avec un String comme la source fonctionne.

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

9
Rob Mecham

@ p4bloch Si vous souhaitez capturer les résultats d'une série de parenthèses de capture, vous devez utiliser la méthode rangeAtIndex(index) de NSTextCheckingResult au lieu de range. Voici la méthode de @MartinR pour Swift2 vue du dessus, adaptée pour les parenthèses de capture. Dans le tableau renvoyé, le premier résultat [0] correspond à la capture complète, puis les groupes de capture individuels commencent à partir de [1]. J'ai commenté l'opération map (il est donc plus facile de voir ce que j'ai changé) et je 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 []
    }
}

Voici un exemple de cas d'utilisation, par exemple, si vous souhaitez scinder une chaîne de title year, par exemple "Finding Dory 2016", procédez comme suit:

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

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

Pour obtenir les correspondances de 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
    }
}
3
valexa

C’est ce que j’ai fait. J’espère que cela apportera une nouvelle perspective à la manière dont cela fonctionne sur Swift.

Dans cet exemple ci-dessous, je vais obtenir une chaîne quelconque 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)")
}
2
Dalorzo

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

Rapide 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
Jorge Osorio

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)!])
        }
    }
}
1
shiami

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

retourne un tableau à 2 dimensions de chaînes:

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

retourne ...

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

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

Un grand merci à Lars Blumberg his réponse pour la capture de groupes et de matches complets avec Swift 4 , qui m'a beaucoup aidé. J'y ai également ajouté un complément pour les personnes qui souhaitent une réponse error.localizedDescription lorsque leur expression rationnelle 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 localisé LocalDescription comme erreur m'a permis de comprendre ce qui n'allait pas avec l'échappement, car il indique quelle expression rationnelle finale Swift essaie de mettre en œuvre.

0
Vasco