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 buscar coincidencias en strings, usando un lenguaje específico de dominio (DSL) diseñado para ello.

Advanced

Como muchos otros lenguajes de programación, R no intenta implementar su propia biblioteca de regex. 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 recientes, aunque algunos casos límite se tratan de forma diferente.

Muchas capacidades de las regex están integradas en R base, y es posible que veas ejemplos en código más antiguo.

Para código nuevo, te recomendamos encarecidamente usar la biblioteca stringr.

Como se indica en el concepto de strings, stringr tiene una buena documentación, normalmente escrita por el autor o los autores del paquete.

Note

Este temario de R da por supuesto que ya estás familiarizado con la sintaxis básica de las expresiones regulares. Nos centraremos únicamente en las características específicas de R.

A continuación tienes algunos recursos para refrescar tus conocimientos sobre expresiones regulares.

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

Mantuvimos el concepto de strings relativamente sencillo y solo buscábamos coincidencias en literales de string, pero en su lugar se puede usar cualquier patrón de regex.

Formalmente, en R se crea una regex llamando a regex(). Por comodidad, las funciones de stringr tratan cualquier string de patrón como una regex. En el ejemplo siguiente, 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 chuleta es una guía útil: todas las funciones con un argumento pattern (impresas en rojo) aceptan una regex, y la página 2 resume la sintaxis de las regex.

Ten en cuenta que la función regex() aún 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 regex incluyen una barra invertida, como el \s usado arriba. En los strings normales de R, \ hay que escaparla duplicándola, de ahí "\\s".

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

De forma predeterminada, la sintaxis para definir un string raw es r"( )", pero se permiten otros delimitadores. Como los paréntesis ( ) y los corchetes [ ] son comunes en la sintaxis de las regex (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 alguna coincidencia? str_detect(), o str_starts(), 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

A menudo queremos saber qué coincide. Esto se consigue incluyendo grupos de captura entre paréntesis dentro de la regex.

Es posible referirse a las capturas dentro de la misma regex, usando \1, \2, etc. Por ejemplo, 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 más tarde con la función str_match() si solo quieres la primera coincidencia, o str_match_all() para todas.

Los grupos de captura tienden a producir una salida complicada en cualquier lenguaje. El enfoque de R (que además está vectorizado, como reto adicional) consiste en generar una matriz a partir de str_match(), con una fila por cada string de entrada, la columna 1 contiene el string completo que coincide y las columnas restantes contienen 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 sobre las matrices en este temario, aunque tienes a tu disposición un documento de concepto. Las matrices no encajan bien con el ecosistema Tidyverse, a pesar de formar parte de 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 dos índices en blanco para obtener toda la fila o toda la 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 mayor:

  • El resultado es una lista, con un elemento por cada string de entrada.
  • Si solo hay un string de entrada, convierte la salida en un vector con unlist, usando la indexación [[1]] como se describe en el concepto de listas.
  • Cada elemento es una matriz, con las columnas igual que en match().
  • Cada fila de la matriz es una captura correcta.
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 la nada.

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

R for Data science señala:

str_match() devuelve una matriz, así que no es especialmente fácil de manejar.

Con la nota al pie:

¡Sobre todo porque en este libro nunca hablamos de matrices!

Uno de los autores del libro también escribió la biblioteca stringr, y en el fondo está criticando su propio trabajo.

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

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

Aprende Expresiones regulares