Tracks
/
R
R
/
Temario
/
Expresiones regulares
Ex

Expresiones regulares en R

1 ejercicio

Acerca de Expresiones regulares

Las expresiones regulares son una forma muy versátil de hacer coincidir patrones en strings, usando un lenguaje de dominio específico (DSL) diseñado para ese propósito.

Advanced

Como muchos otros lenguajes de programación, R no intenta implementar su propia biblioteca de expresiones regulares. En su lugar, envuelve el motor de expresiones regulares ICU. ICU es similar a la popular biblioteca PCRE2 que usan la mayoría de los lenguajes más nuevos, aunque algunos casos límite se manejan de forma distinta.

Muchas capacidades de las expresiones regulares vienen incluidas en el R base, y es posible que veas ejemplos en código más antiguo.

Para código nuevo, se recomienda encarecidamente usar la biblioteca stringr.

Como se señala en el concepto de Strings, stringr tiene buena documentación, normalmente escrita por el autor o los autores del paquete.

Note

Este temario de R supone que ya conoces la sintaxis básica de las expresiones regulares. Nos concentraremos únicamente en las características específicas de R.

Abajo encontrarás algunos recursos para repasar tus conocimientos sobre expresiones regulares.

Muchas de las funciones que ya vimos en el concepto Strings están diseñadas para hacer búsquedas con expresiones regulares de forma estándar.

Mantuvimos el concepto Strings relativamente simple al hacer coincidencias solo con literales de string, pero en su lugar se puede usar cualquier patrón de regex.

Formalmente, un regex se crea en R llamando a regex(). Por comodidad, las funciones de stringr tratan cualquier string de patrón como un regex. En el ejemplo de abajo, el primer str_replace_all() es azúcar sintáctico para la segunda versión:

input <- "Some input string"

# "\\s" matches any whitespace character
input |> str_replace_all("\\s", "_")
[1] "Some_input_string"

input |> str_replace_all(regex("\\s"), "_")
[1] "Some_input_string"

La hoja de referencia es una guía útil: todas las funciones con un argumento pattern (impresas en rojo) aceptan un regex, y la página 2 resume la sintaxis de las expresiones regulares.

Ten en cuenta que la función regex() puede ser importante si necesitas anular los valores predeterminados, como la distinción entre mayúsculas y minúsculas o la configuración regional.

# default is case sensitive
"A Mixed Assortment" |> 
    str_replace_all(regex("[aeiou]", ignore_case = TRUE), "*")
#> [1] "* M*x*d *ss*rtm*nt"

Muchos códigos de expresiones regulares incluyen una barra invertida, como el \s que usamos arriba. En los strings normales de R, \ debe escaparse duplicándolo, de ahí "\\s".

Las barras invertidas pueden multiplicarse y volverse confusas, así que puede ayudar usar strings sin procesar en casos complejos.

De forma predeterminada, la sintaxis para definir un string sin procesar es r"( )", pero se permiten otros delimitadores. Como los paréntesis ( ) y los corchetes [ ] son comunes en la sintaxis de las expresiones regulares (para grupos de captura y grupos de opciones), las llaves { } podrían ser una buena elección.

input |> str_replace_all(r"{\s\w}", "__")
#> [1] "Some__nput__tring"

Detectar coincidencias

Aquí tienes algunas preguntas y funciones que pueden ayudarte a responderlas.

Algunos ejemplos usan fruit, un vector de 80 nombres de frutas incluido con stringr para practicar.

¿Un string contiene una coincidencia? str_detect(), o str_starts() y str_ends() para evitar usar los anclajes ^ o $.

# berries begining with b
fruit |> str_detect("^b.*berry") |> head(15)
#> [1] FALSE FALSE FALSE FALSE FALSE  TRUE  TRUE FALSE FALSE  TRUE  TRUE FALSE FALSE FALSE FALSE

# alternative syntax
fruit |> str_starts("b.*berry") |> head(15)
#> [1] FALSE FALSE FALSE FALSE FALSE  TRUE  TRUE FALSE FALSE  TRUE  TRUE FALSE FALSE FALSE FALSE

¿Devolver solo los strings que contienen una coincidencia? Usa str_subset() para obtener los strings, str_which() para obtener los índices.

fruit |> str_subset("a...e")
[1] "apple"       "blackberry"  "mandarine"   "nectarine"   "pineapple"   "pomegranate" "raspberry"   "salal berry"
> fruit |> str_which("a...e")
[1]  1  7 48 51 62 64 70 73

¿Cuántas coincidencias hay en cada string? str_count()

# count vowels
fruit |> str_count("[aeiou]") |> head(10)
#> [1] 2 3 4 3 3 2 2 3 5 3

¿Dónde está la coincidencia? str_view() para uso interactivo, str_locate() o str_locate_all() de forma más general.

# str_view only shows matching elements, with match in < > delimiters
fruit |> str_view("a...e")
 [1] │ <apple>
 [7] │ bl<ackbe>rry
[48] │ mand<arine>
[51] │ nect<arine>
[62] │ pine<apple>
[64] │ pomegr<anate>
[70] │ r<aspbe>rry
[73] │ sal<al be>rry

# str_locate returns all elements
fruit |> str_locate("a...e") |> head(10)
      start end
 [1,]     1   5
 [2,]    NA  NA
 [3,]    NA  NA
 [4,]    NA  NA
 [5,]    NA  NA
 [6,]    NA  NA
 [7,]     3   7
 [8,]    NA  NA
 [9,]    NA  NA
[10,]    NA  NA

Capturas

Con frecuencia queremos saber qué coincide. Esto se logra incluyendo grupos de captura entre paréntesis dentro de la expresión regular.

Es posible referirse a las capturas dentro del mismo regex, usando \1, \2, etc. Por ejemplo, para encontrar pares de letras repetidos:

fruit |> str_subset("(..)\\1")
[1] "banana"      "coconut"     "cucumber"    "jujube"      "papaya"      "salal berry"

Con una sintaxis similar, las capturas se pueden usar en los strings de reemplazo:

# double to triple
fruit |> 
  str_subset("(..)\\1") |> 
  str_replace("(..)\\1", "\\1\\1\\1")
[1] "bananana"      "cococonut"     "cucucumber"    "jujujube"      "papapaya"      "salalal berry"

Reúne las coincidencias para procesarlas después con la función str_match() si solo quieres la primera coincidencia, o str_match_all() para todas las coincidencias.

Los grupos de captura suelen producir resultados complicados en cualquier lenguaje. El enfoque de R (que además está vectorizado como reto adicional) es generar una matriz a partir de str_match(), con una fila por cada string de entrada, la columna 1 con el string completo que coincidió y las columnas restantes con los distintos grupos de captura.

Usar grupos con nombre puede ser una ventaja, ya que se convierten en nombres de columna en la matriz de resultados.

recipe <- "dissolve 25g sugar in 200ml water"
recipe |> str_match("(\\d+g) .* (\\d+ml)")
     [,1]                 [,2]  [,3]   
[1,] "25g sugar in 200ml" "25g" "200ml"

# use named capture groups
recipe |> str_match("(?<wt>\\d+g) .* (?<vol>\\d+ml)")
                          wt    vol    
[1,] "25g sugar in 200ml" "25g" "200ml"

No hemos hablado mucho de matrices en este temario, aunque hay un documento de concepto que puedes consultar. Las matrices no encajan bien con el ecosistema Tidyverse, a pesar de formar parte del R base desde hace mucho tiempo.

En resumen: trata las matrices como vectores con 2 índices, en el orden [rows, cols]. Deja cualquiera de los índices en blanco para obtener toda la fila o columna.

Cuando obtienes una sola fila r con m[r,] o una sola columna c con m[, c], el valor devuelto se aplana a un vector de forma predeterminada.

> amounts <- recipe |> str_match("(?<wt>\\d+g) .* (?<vol>\\d+ml)")

# get all of row 1, as a named vector
> amt1 <- amounts[1,]
> amt1
                                       wt                  vol 
"25g sugar in 200ml"                "25g"              "200ml" 

# get a vector element
amt1["wt"] |> unname()
[1] "25g"

Inevitablemente, str_match_all() produce una salida con una estructura aún más compleja:

  • El resultado es una lista, con un elemento por cada string de entrada.
  • Si hay un solo string de entrada, extrae la salida de la lista con el indexado [[1]], como se describe en el concepto de Listas.
  • Cada elemento es una matriz, con columnas iguales que las de match().
  • Cada fila de la matriz es una captura exitosa.
mix <- "Mix 200g of sugar, 180g of flour and 150g of butter"

# raw output
> str_match_all(mix, "(\\d+)g")
[[1]]
     [,1]   [,2] 
[1,] "200g" "200"
[2,] "180g" "180"
[3,] "150g" "150"

# we just want the first list element
> str_match_all(mix, "(\\d+)g")[[1]]
     [,1]   [,2] 
[1,] "200g" "200"
[2,] "180g" "180"
[3,] "150g" "150"

# captures are in column 2, so get that as a vector
> str_match_all(mix, "(\\d+)g")[[1]][,2]
[1] "200" "180" "150"

Por supuesto, las coincidencias pueden fallar. El resultado será entonces el valor especial NA. Prepárate para comprobarlo, como se describe en el concepto de Nada.

# failed match
recipe |> str_match("(not here)")
     [,1] [,2]
[1,] NA   NA  
Note

R for Data Science señala:

str_match() returns a matrix, so it’s not particularly easy to work with.

Con la nota al pie:

Mostly because we never discuss matrices in this book!

Uno de los autores del libro también escribió la biblioteca stringr, y está criticando implícitamente su propio trabajo.

Será interesante ver cómo evoluciona la implementación de esta función en versiones futuras.

Editar en GitHub El enlace se abre en una ventana o pestaña nueva

Aprende Expresiones regulares