Particionado en Apache Spark
Los RDD operan con datos no como una sola masa de datos, sino que administran y operan los datos en particiones repartidas por todo el…
A veces necesitamos encontrar patrones en strings en Scala. Un caso muy común sería, por ejemplo, verificar si un String contiene una expresión regular.
Una posible solución para este problema sería crear un objeto Regex invocando el método .r en un String y luego usar ese patrón con la función findFirstIn cuando estemos buscando una sola coincidencia y findAllIn cuando deseemos todas las coincidencias.
Las expresiones regulares son una herramienta fundamental en cualquier lenguaje de programación, y Scala no es la excepción. En Scala, la clase scala.util.matching.Regex proporciona un conjunto completo de métodos para buscar, extraer y reemplazar texto basándose en patrones. A diferencia de otros lenguajes como Java, donde trabajar con regex requiere instanciar objetos Pattern y Matcher de forma explícita, Scala simplifica enormemente el proceso gracias al método .r disponible directamente en cualquier String.
A continuación veamos cómo trabajar con estos dos enfoques.
Lo primero que vamos a realizar es crear la expresión regular la cual deseamos buscar en el String. Para este ejemplo vamos a crear una expresión que busque una secuencia de uno o más caracteres numéricos.
val numPattern = "[0-9]+".r
Usar el método .r en un String es la forma más fácil de crear un objeto Regex. Otro posible enfoque sería importar la clase Regex, crear una instancia de Regex y luego usar la instancia de la misma forma.
import scala.util.matching.Regex
val numPattern = new Regex("[0-9]+")
Lo siguiente que vamos a realizar es crear un String de muestra sobre el cual podamos buscar coincidencias con el patrón creado anteriormente.
val address = "Calle 02 de Abril 350"
A continuación utilizamos el método findFirstIn para encontrar la primera coincidencia dentro del String.
val result = numPattern.findFirstIn(address)
La salida generada por esta línea de código es la siguiente.
result: Option[String] = Some(02)
Notemos como este método retorna un Option[String]. Una forma sencilla de pensar en Option es que contiene cero o un valor. Para el caso de la función findFirstIn, si la búsqueda es exitosa devuelve el string «02» dentro de Some, es decir, Some(02). Sin embargo, si falla en encontrar el patrón en el string retorna None.
Cuando buscamos múltiples coincidencias debemos usar la función findAllIn.
val result = numPattern.findAllIn(address)
La salida generada por esta línea de código es la siguiente.
result: scala.util.matching.Regex.MatchIterator = <iterator>
Como se puede observar findAllIn retorna un iterador el cual nos permite iterar sobre el resultado.
result.foreach(println)
El resultado de esta última línea de código es el siguiente.
02
350
Si findAllIn no encuentra ningún resultado este retorna un iterador vacío. Si por ejemplo, necesitamos el resultado como vector podríamos agregar la función toVector después de la función findAllIn.
val result = numPattern.findAllIn(address).toVector
Obteniendo como resultado:
result: Vector[String] = Vector(02, 350)
Si no existen coincidencias esta última línea devuelve como resultado un vector vacío. Otros métodos como toList, toSeq y toArray también están disponibles de la misma forma que acabamos de demostrar.
En ocasiones no solo necesitamos encontrar un patrón, sino también extraer partes específicas de la coincidencia. Para esto podemos usar findFirstMatchIn, que en lugar de retornar un Option[String] retorna un Option[Match]. El objeto Match nos permite acceder a los grupos de captura definidos en la expresión regular.
Supongamos que tenemos un String con fechas en formato dd-mm-yyyy y queremos extraer el día, el mes y el año por separado.
val datePattern = """(\d{2})-(\d{2})-(\d{4})""".r
val text = "La fecha de entrega es 15-03-2024 y la fecha límite es 30-06-2024"
Utilizamos findFirstMatchIn para obtener la primera coincidencia como un objeto Match.
val matchResult = datePattern.findFirstMatchIn(text)
Ahora podemos acceder a cada grupo de captura usando el método group con el número de grupo correspondiente.
matchResult match {
case Some(m) =>
println(s"Día: ${m.group(1)}")
println(s"Mes: ${m.group(2)}")
println(s"Año: ${m.group(3)}")
case None =>
println("No se encontró ninguna fecha")
}
La salida de este código es la siguiente.
Día: 15
Mes: 03
Año: 2024
Esta funcionalidad es especialmente útil cuando trabajamos con datos semi-estructurados donde necesitamos parsear y extraer componentes individuales de un texto.
Además de buscar patrones, la clase Regex en Scala nos permite reemplazar coincidencias dentro de un String. Los dos métodos principales para esto son replaceFirstIn y replaceAllIn.
El método replaceFirstIn reemplaza únicamente la primera coincidencia encontrada.
val censored = numPattern.replaceFirstIn(address, "XX")
El resultado sería:
censored: String = Calle XX de Abril 350
Por otro lado, replaceAllIn reemplaza todas las coincidencias del patrón en el String.
val allCensored = numPattern.replaceAllIn(address, "XX")
Obteniendo como resultado:
allCensored: String = Calle XX de Abril XX
También es posible usar replaceAllIn con una función que reciba cada coincidencia y retorne el texto de reemplazo. Esto nos da mayor control sobre la transformación.
val doubled = numPattern.replaceAllIn(address, m => (m.matched.toInt * 2).toString)
En este caso, cada número encontrado se multiplica por 2.
doubled: String = Calle 4 de Abril 700
A continuación se presenta una tabla resumen con los métodos más utilizados de la clase Regex en Scala.
| Método | Retorna | Descripción |
|---|---|---|
findFirstIn |
Option[String] |
Primera coincidencia como String |
findAllIn |
MatchIterator |
Todas las coincidencias como iterador |
findFirstMatchIn |
Option[Match] |
Primera coincidencia con acceso a grupos de captura |
findAllMatchIn |
Iterator[Match] |
Todas las coincidencias con acceso a grupos de captura |
replaceFirstIn |
String |
Reemplaza la primera coincidencia |
replaceAllIn |
String |
Reemplaza todas las coincidencias |
matches |
Boolean |
Verifica si el String completo coincide con el patrón |
Al trabajar con expresiones regulares en Scala es fácil cometer ciertos errores, especialmente si vienes de otros lenguajes. Veamos los más frecuentes.
No manejar el Option de findFirstIn. Dado que findFirstIn retorna un Option[String], intentar usar el resultado directamente como String provocará un error de compilación. Siempre debemos usar pattern matching, getOrElse o foreach para manejar el resultado de forma segura.
// Incorrecto - no compila
val result: String = numPattern.findFirstIn(address)
// Correcto - usando getOrElse
val result: String = numPattern.findFirstIn(address).getOrElse("No encontrado")
Olvidar escapar caracteres especiales. Los caracteres como ., (, ), [, ], +, * y ? tienen significado especial en las expresiones regulares. Si necesitamos buscarlos literalmente, debemos escaparlos con \\ o usar Regex.quote.
// Buscar un punto literal
val dotPattern = "\\." .r
// Alternativa usando Regex.quote
val dotPattern2 = Regex.quote(".").r
Reutilizar un iterador agotado. El MatchIterator retornado por findAllIn solo puede recorrerse una vez. Si necesitamos recorrer los resultados más de una vez, debemos convertirlo a una colección como List o Vector antes de iterar.
val results = numPattern.findAllIn(address).toList
// Ahora podemos recorrerlo múltiples veces
results.foreach(println)
results.foreach(r => println(s"Encontrado: $r"))
Las expresiones regulares en Scala son una herramienta poderosa y flexible gracias a la clase Regex y la sintaxis simplificada que ofrece el método .r. En este artículo vimos cómo encontrar la primera coincidencia con findFirstIn, cómo obtener todas las coincidencias con findAllIn, cómo trabajar con grupos de captura usando findFirstMatchIn, y cómo reemplazar texto con replaceFirstIn y replaceAllIn.
Recordemos que findFirstIn retorna un Option[String], por lo que siempre debemos manejar tanto el caso exitoso (Some) como el caso en que no se encuentra coincidencia (None). Del mismo modo, findAllIn retorna un iterador que solo puede recorrerse una vez, así que si necesitamos reutilizar los resultados conviene convertirlo a una colección.