Swift extrair correspondências de regex

175

Quero extrair substrings de uma string que corresponde a um padrão regex.

Então, eu estou procurando algo como isto:

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

Então é isso que eu tenho:

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

O problema é que isso matchesInStringme fornece uma variedade deNSTextCheckingResult onde NSTextCheckingResult.rangeé do tipo NSRange.

NSRangeé incompatível com Range<String.Index>, por isso me impede de usartext.substringWithRange(...)

Alguma idéia de como conseguir essa coisa simples rapidamente, sem muitas linhas de código?

mitchkman
fonte

Respostas:

313

Mesmo que o matchesInString()método Stringseja o primeiro argumento, ele trabalha internamente com NSString, e o parâmetro range deve ser fornecido usando o parâmetroNSString comprimento e não o comprimento da string Swift. Caso contrário, falhará em "grupos de grafemas estendidos", como "sinalizadores".

A partir do Swift 4 (Xcode 9), a biblioteca padrão do Swift fornece funções para converter entre 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 []
    }
}

Exemplo:

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

Nota: O desembrulhamento forçado Range($0.range, in: text)!é seguro porque NSRangerefere-se a uma substring da sequência especificada text. No entanto, se você quiser evitá-lo, use

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

em vez de.


(Resposta mais antiga para Swift 3 e versões anteriores :)

Portanto, você deve converter a sequência Swift fornecida em um NSString e extrair os intervalos. O resultado será convertido para uma matriz de sequências Swift automaticamente.

(O código do Swift 1.2 pode ser encontrado no histórico de edições.)

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

Exemplo:

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

Exemplo:

let string = "🇩🇪€4€9"
let matched = matches(for: "[0-9]", in: string)
print(matched)
// ["4", "9"]
Martin R
fonte
9
Você me salvou de ficar louco. Não está brincando. Muito obrigado!
Mckkman
1
@MathijsSegers: Atualizei o código do Swift 1.2 / Xcode 6.3. Obrigado por me avisar!
Martin R
1
mas e se eu quiser procurar por seqüências de caracteres entre uma tag? Preciso do mesmo resultado (informações da correspondência) como: regex101.com/r/cU6jX8/2 . qual padrão de regex você sugeriria?
Peter Kreinz
A atualização é para Swift 1.2, não Swift 2. O código não compilar com Swift 2.
PatrickNLT
1
Obrigado! E se você quiser extrair apenas o que está realmente entre () na regex? Por exemplo, em "[0-9] {3} ([0-9] {6})", eu gostaria apenas de obter os últimos 6 números.
P4bloch 23/09/2015
64

Minha resposta se baseia em respostas dadas, mas torna a correspondência de expressões regulares mais robusta adicionando suporte adicional:

  • Retorna não apenas correspondências, mas também retorna todos os grupos de captura para cada correspondência (veja os exemplos abaixo)
  • Em vez de retornar uma matriz vazia, esta solução suporta correspondências opcionais
  • Evita do/catchnão imprimir no console e faz uso da guardconstrução
  • Adiciona matchingStringscomo uma extensão aoString

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
fonte
1
Boa ideia sobre os grupos de captura. Mas por que "guarda" é mais rápido que "pegar / pegar"?
Martin R
Eu concordo com pessoas como nshipster.com/guard-and-defer que dizem que o Swift 2.0 certamente parece estar encorajando um estilo de retorno precoce, [...] em vez de declarações if aninhadas . O mesmo vale para as instruções do / catch aninhadas IMHO.
Lars Blumberg #
try / catch é o tratamento de erros nativo no Swift. try?pode ser usado se você estiver interessado apenas no resultado da chamada, e não em uma possível mensagem de erro. Então, sim, tudo guard try? ..bem, mas se você quiser imprimir o erro, precisará de um bloco de tarefas. Ambas as formas são Swifty.
Martin R
3
Eu adicionei unittests ao seu bom snippet, gist.github.com/neoneye/03cbb26778539ba5eb609d16200e4522
neoneye
1
Estava prestes a escrever o meu com base na resposta @MartinR até que vi isso. Obrigado!
Oritm
13

Se você deseja extrair substrings de uma String, não apenas a posição (mas a String real, incluindo emojis). Em seguida, talvez seja uma solução mais simples.

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

Exemplo de uso:

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

Retornará o seguinte:

["👿⚽️"]

Nota usando "\ w +" pode produzir um "" inesperado

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

Retornará essa matriz String

["someText", "️", "pig"]
Mike Chirico
fonte
1
Isto é o que eu queria #
276 Kyle KIM
1
Agradável! Ele precisa de um pequeno ajuste para o Swift 3, mas é ótimo.
Jelle
@Jelle, qual é o ajuste necessário? Estou usando o rápido 5.1.3
Peter Schorn 16/03
9

Descobri que a solução da resposta aceita infelizmente não é compilada no Swift 3 para Linux. Aqui está uma versão modificada, então, que faz:

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

As principais diferenças são:

  1. O Swift no Linux parece exigir a exclusão do NSprefixo nos objetos Foundation para os quais não há equivalente nativo do Swift. (Veja a proposta de evolução Swift nº 86 ).

  2. O Swift no Linux também requer a especificação dos optionsargumentos para a RegularExpressioninicialização e o matchesmétodo.

  3. Por alguma razão, coagir um Stringpara um NSStringnão funciona no Swift no Linux, mas inicializar um novo NSStringcom um à Stringmedida que a fonte funciona.

Esta versão também funciona com o Swift 3 no macOS / Xcode, com a única exceção de que você deve usar o nome em NSRegularExpressionvez de RegularExpression.

Rob Mecham
fonte
5

@ p4bloch se você deseja capturar resultados de uma série de parênteses de captura, precisa usar o rangeAtIndex(index)método de NSTextCheckingResult, em vez de range. Aqui está o método do @MartinR para o Swift2 de cima, adaptado para os parênteses de captura. Na matriz retornada, o primeiro resultado [0]é a captura inteira e, em seguida, começam grupos de captura individuais [1]. Comentei a mapoperação (para que seja mais fácil ver o que eu mudei) e a substitui por loops aninhados.

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

Um exemplo de caso de uso pode ser, digamos que você queira dividir uma sequência de title year por exemplo, "Procurando o Dory 2016", você pode fazer isso:

print ( matches(for: "^(.+)\\s(\\d{4})" , in: "Finding Dory 2016"))
// ["Finding Dory 2016", "Finding Dory", "2016"]
OliverD
fonte
Esta resposta fez o meu dia. Passei duas horas procurando uma solução que pudesse satisfazer a expressão regular com a captura adicional de grupos.
Ahmad
Isso funciona, mas falhará se nenhum intervalo for encontrado. Modifiquei esse código para que a função retorne [String?]e, no for i in 0..<result.numberOfRangesbloco, é necessário adicionar um teste que apenas acrescenta a correspondência se o intervalo! = NSNotFound, Caso contrário, deve acrescentar nulo. Veja: stackoverflow.com/a/31892241/2805570
stef
4

Swift 4 sem 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)!])
        }
    }
}
Miami
fonte
Cuidado com a solução acima: NSMakeRange(0, self.count)não está correto, porque selfé um String(= UTF8) e não um NSString(= UTF16). Portanto, self.countnão é necessariamente o mesmo que nsString.length(usado em outras soluções). Você pode substituir o cálculo do intervalo porNSRange(self.startIndex..., in: self)
pd95 29/06
3

A maioria das soluções acima fornece apenas a correspondência completa como resultado, ignorando os grupos de captura, por exemplo: ^ \ d + \ s + (\ d +)

Para obter as correspondências do grupo de captura conforme o esperado, você precisa de algo como (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
fonte
Isso é ótimo se você está querendo apenas o primeiro resultado, para obter cada resultado que precisa for index in 0..<matches.count {em tornolet lastRange... results.append(matchedString)}
Geoff
a cláusula for deve ficar assim: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

Foi assim que eu fiz, espero que traga uma nova perspectiva de como isso funciona no Swift.

Neste exemplo abaixo, receberei a string any 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
fonte
2

Esta é uma solução muito simples que retorna uma matriz de string com as correspondências

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
fonte
2

A maneira mais rápida de retornar todas as partidas e capturar grupos no 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)) }
        } ?? []
    }
}

Retorna uma matriz bidimensional de seqüências de caracteres:

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

retorna ...

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

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

Muito obrigado a Lars Blumberg, sua resposta pela captura de grupos e partidas completas com o Swift 4 , o que me ajudou muito. Também fiz um acréscimo para as pessoas que desejam uma resposta error.localizedDescription quando o regex deles é inválido:

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

O fato de eu ter a descrição localizada como erro ajudou a entender o que deu errado com a fuga, uma vez que é exibida qual regex final rápido tenta implementar.

Vasco
fonte