Partidos rápidos de expresiones regulares de extracto

175

Quiero extraer subcadenas de una cadena que coincida con un patrón regex.

Entonces estoy buscando algo como esto:

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

Entonces esto es lo que tengo:

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

El problema es que matchesInStringme ofrece una variedad de NSTextCheckingResult, donde NSTextCheckingResult.rangees de tipo NSRange.

NSRangees incompatible con Range<String.Index>, por lo que me impide usartext.substringWithRange(...)

¿Alguna idea de cómo lograr esto tan rápido sin demasiadas líneas de código?

mitchkman
fuente

Respuestas:

313

Incluso si el matchesInString()método toma a Stringcomo primer argumento, funciona internamente NSStringy el parámetro de rango debe proporcionarse utilizandoNSString longitud y no como la longitud de la cadena Swift. De lo contrario, fallará para "grupos de grafemas extendidos" como "banderas".

A partir de Swift 4 (Xcode 9), la biblioteca estándar de Swift proporciona funciones para convertir entre Range<String.Index> y 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 []
    }
}

Ejemplo:

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

Nota: El desenvolvimiento forzado Range($0.range, in: text)!es seguro porque se NSRangerefiere a una subcadena de la cadena dada text. Sin embargo, si desea evitarlo, use

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

en lugar.


(Respuesta anterior para Swift 3 y anteriores :)

Por lo tanto, debe convertir la cadena Swift dada en un NSString y luego extraer los rangos. El resultado se convertirá en una matriz de cadenas Swift automáticamente.

(El código para Swift 1.2 se puede encontrar en el historial de edición).

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

Ejemplo:

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

Ejemplo:

let string = "🇩🇪€4€9"
let matched = matches(for: "[0-9]", in: string)
print(matched)
// ["4", "9"]
Martin R
fuente
9
Me salvaste de volverme loco. No bromeo. Muchas gracias!
mitchkman
1
@MathijsSegers: He actualizado el código para Swift 1.2 / Xcode 6.3. ¡Gracias por hacérmelo saber!
Martin R
1
pero ¿y si quiero buscar cadenas entre una etiqueta? Necesito el mismo resultado (información del partido) como: regex101.com/r/cU6jX8/2 . ¿Qué patrón de expresiones regulares sugerirías?
Peter Kreinz
La actualización es para Swift 1.2, no Swift 2. El código no se compila con Swift 2.
PatrickNLT
1
¡Gracias! ¿Qué sucede si solo desea extraer lo que realmente está entre () en la expresión regular? Por ejemplo, en "[0-9] {3} ([0-9] {6})" solo me gustaría obtener los últimos 6 números.
p4bloch
64

Mi respuesta se basa en las respuestas dadas, pero hace que la coincidencia de expresiones regulares sea más sólida al agregar soporte adicional:

  • No solo devuelve coincidencias, sino que también devuelve todos los grupos de captura para cada coincidencia (ver ejemplos a continuación)
  • En lugar de devolver una matriz vacía, esta solución admite coincidencias opcionales
  • Evita do/catchno imprimir en la consola y hace uso de la guardconstrucción
  • Agrega matchingStringscomo una extensión aString

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
fuente
1
Buena idea sobre los grupos de captura. ¿Pero por qué es "guardia" más rápido que "hacer / atrapar"?
Martin R
Estoy de acuerdo con personas como nshipster.com/guard-and-defer que dicen que Swift 2.0 ciertamente parece alentar un estilo de retorno [...] temprano en lugar de declaraciones anidadas . Lo mismo es cierto para las declaraciones anidadas do / catch en mi humilde opinión.
Lars Blumberg
try / catch es el manejo de errores nativo en Swift. try?puede usarse si solo está interesado en el resultado de la llamada, no en un posible mensaje de error. Entonces sí, guard try? ..está bien, pero si desea imprimir el error, entonces necesita un do-block. Ambas formas son rápidas.
Martin R
3
He agregado pruebas de
unidad
1
Estaba a punto de escribir el mío según la respuesta de @MartinR hasta que vi esto. ¡Gracias!
Oritm
13

Si desea extraer subcadenas de una Cadena, no solo la posición (sino la Cadena real, incluidos los emojis). Entonces, lo siguiente quizás sea una solución más 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]()
    }
  }
} 

Ejemplo de uso:

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

Devolverá lo siguiente:

["👿⚽️"]

Nota usando "\ w +" puede producir un inesperado ""

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

Devolverá esta matriz de cadenas

["someText", "️", "pig"]
Mike Chirico
fuente
1
Esto es lo que quería
Kyle KIM
1
¡Agradable! Necesita un pequeño ajuste para Swift 3, pero es genial.
Jelle
@Jelle, ¿cuál es el ajuste que necesita? Estoy usando swift 5.1.3
Peter Schorn
9

Descubrí que la solución de la respuesta aceptada desafortunadamente no se compila en Swift 3 para Linux. Aquí hay una versión modificada, entonces, que sí:

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

Las principales diferencias son:

  1. Swift en Linux parece requerir que se suelte el NSprefijo en los objetos Foundation para los que no existe un equivalente nativo de Swift. (Ver la propuesta de evolución rápida # 86. )

  2. Swift en Linux también requiere especificar los optionsargumentos para la RegularExpressioninicialización y el matchesmétodo.

  3. Por alguna razón, coaccionar un Stringen un NSStringno funciona en Swift en Linux sino inicializar un nuevo NSStringcon un Stringcomo la fuente sí funciona.

Esta versión también funciona con Swift 3 en macOS / Xcode con la única excepción de que debe usar el nombre en NSRegularExpressionlugar de RegularExpression.

Rob Mecham
fuente
5

@ p4bloch si desea capturar resultados de una serie de paréntesis de captura, debe utilizar el rangeAtIndex(index)método de NSTextCheckingResult, en lugar de range. Aquí está el método de @MartinR para Swift2 desde arriba, adaptado para paréntesis de captura. En la matriz que se devuelve, el primer resultado [0]es la captura completa, y luego comienzan los grupos de captura individuales [1]. Comenté la mapoperación (por lo que es más fácil ver lo que cambié) y la reemplacé por bucles anidados.

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 ejemplo de uso podría ser, digamos que desea dividir una cadena de, title yearpor ejemplo, "Finding Dory 2016", podría hacer esto:

print ( matches(for: "^(.+)\\s(\\d{4})" , in: "Finding Dory 2016"))
// ["Finding Dory 2016", "Finding Dory", "2016"]
OliverD
fuente
Esta respuesta me alegró el día. Pasé 2 horas buscando una solución que pueda satisfacer la expresión regular con la captura adicional de grupos.
Ahmad
Esto funciona pero se bloqueará si no se encuentra ningún rango. Modifiqué este código para que la función regrese [String?]y en el for i in 0..<result.numberOfRangesbloque, debe agregar una prueba que solo agrega la coincidencia si el rango! = NSNotFound, De lo contrario, debería agregar nil. Ver: stackoverflow.com/a/31892241/2805570
stef
4

Swift 4 sin 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
fuente
Tenga cuidado con la solución anterior: NSMakeRange(0, self.count)no es correcta, porque selfes un String(= UTF8) y no un NSString(= UTF16). Entonces, self.countno es necesariamente lo mismo que nsString.length(como se usa en otras soluciones). Puede reemplazar el cálculo del rango conNSRange(self.startIndex..., in: self)
pd95
3

La mayoría de las soluciones anteriores solo dan la coincidencia completa como resultado ignorando los grupos de captura, por ejemplo: ^ \ d + \ s + (\ d +)

Para obtener las coincidencias del grupo de captura como se esperaba, necesita 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
fuente
Esto es grande si usted está queriendo sólo el primer resultado, para obtener cada resultado que necesita for index in 0..<matches.count {alrededorlet lastRange... results.append(matchedString)}
Geoff
la cláusula for debería verse así: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

Así es como lo hice, espero que traiga una nueva perspectiva de cómo funciona esto en Swift.

En este ejemplo a continuación, obtendré cualquier cadena 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
fuente
2

Esta es una solución muy simple que devuelve una matriz de cadenas con las coincidencias.

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

La forma más rápida de devolver todos los partidos y capturar grupos en 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)) }
        } ?? []
    }
}

Devuelve una matriz de cadenas de 2 dimensiones:

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

devoluciones...

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

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

Muchas gracias a Lars Blumberg por su respuesta por capturar grupos y partidos completos con Swift 4 , lo que me ayudó mucho. También hice una adición para las personas que quieren una respuesta error.localizedDescription cuando su expresión regular no es válida:

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

Para mí, tener la Descripción localizada como error me ayudó a comprender qué salió mal al escapar, ya que muestra qué final regex swift intenta implementar.

Vasco
fuente