Expresiones Regulares

Las expresiones regulares, aunque complicadas en apariencia, son una herramienta poderosa para trabajar con texto. Son utilizadas para identificar patrones y procesar texto. Ruby contiene herramientas para trabajar con expresiones regulares.

Mucha gente considera a las expresiones regulares difíciles de usar, de leer, de mantener y, a final de cuentas, contraproducentes. Es posible que termines utilizando sólo un número modesto de expresiones regulares en tus aplicaciones Ruby on Rails. Convertirte en un mago de las expresiones regulares no es un requisito para programar en Rails. Sin embargo, es recomendable aprender al menos los conceptos básicos de cómo funcionan las expresiones regulares.

Una expresión regular es sólo una manera de especificar un patrón de caracteres que deben ser encontrados en una cadena. En Ruby, las expresiones regulares se crean normalmente escribiendo patrones de caracteres entre barras diagonales (/patrón/). Las expresiones regulares son objetos de la clase Regexp y pueden ser modificadas como tal. / / es una expresión regular y una instancia de la clase Regexp:

1 //.class #Regexp

Puedes crear un patrón que encuentre ocurrencias del texto Pune o el texto Ruby usando la siguiente expresión regular:

1 /Pune | Ruby/

Las barras diagonales delimitan el patrón que consiste de dos secuencias de caracteres que tratamos de encontror en la cadena, separadas por una barra vertical (|). La barra vertical significa "cualquiera de las dos secuencias de caracteres", en este caso Ruby y Pune.

La manera más sencilla de saber si hay una ocurrencia entre el patrón y una cadena es con el método match. Puedes hacerlo en cualquier dirección: Tanto los objetos de clase Regexp como los de clase Sring responden al método match. Si no hay ninguna ocurrencia del patrón en la cadena, el método regresa nil. Si hay ocurrencias del patrón (es decir, si el patrón especificado en la expresión regular se encuentra en la cadena), match regresa un objeto de clase MatchData.

También podemos utilizar el operador match =~. Si el patrón es encontrado en la cadena, =~ regresa la posición dentro de la cadena en la que el patrón comienza, de lo contrario regresa nil.

1 m1 = /Ruby/.match("El futuro es Ruby")
2 puts m1.class #MatchData
3 
4 m2 = "El futuro es Ruby" =~ /Ruby/
5 puts m2 #13

Los componentes que es posible encontrar en una expresión regular son los siguientes:

Caracteres literales

Cualquier caracter literal que incluyas en una expresión regular se busca ocurrencias de si mismo dentro de la cadena.

1 /a/

Esta expresión regular encuentra ocurrencias de la letra "a".

Algunos caracteres tienen significados especiales para el parser de expresiones regulares. Cuando quieras encontrar ocurrencias de estos caracteres, debes anteponer una barra diagonal invertida (\). Por ejemplo, para encontrar ocurrencias del caracter ? debes escribir:

1 /\?/

La diagonal invertida quiere decir: "no trates el caracter que sigue como un carater especial, trátalo como a si mismo".

Los caracteres especiales incluyen: ^, $, ?, ., /, \, [, ], {, }, (, ), + y *.

El caracter comodín

Algunas veces quieres encontrar ocurrencias de cualquier caracter en algun lugar del patrón. Esto se consigue utilizando el comodín . (punto). Un punto encuentra ocurrencias de cualquier caracter con excepción de \n. La expresión regular

1 /.asa/

Encuentra ocurrencias de "casa", "masa" y "tasa". También de "%asa", "basa", "1asa", "2asa", etc. El comodín (.) es útil pero algunas veces encuentra más ocurrencias de las que en realidad quieres. Sin embargo, puedes establecer límites mientras permites encontrar múltiples ocurrencias usando clases de caracteres.

Clases de caracteres

Una clase de caracteres es una lista explícita de caracteres colocada dentro de la expresión regular entre corchetes.

1 /[ctm]asa/

Lo anterior quiere decir: encuentra ocurrencias de "c" o "m" o "t" seguidas de "asa". Este nuevo patrón encuentra ocurrencias de "casa", "masa" y "tasa" pero no de "sasa" ni de "1asa". Una clase de caracteres es un cuasi-comodín: permite múltiples posibles caracteres pero sólo un número limitado.

Dentro de una clase de caracteres puedes insertar un rango de caracteres. Un caso común es el siguiente:

1 /[a-z]/

Para encontrar ocurrencias de dígitos hexadecimales puedes usar varios rangos dentro de una clase de caracteres:

1 /[A-Fa-f0-9]/

La expresión anterior encuentra ocurrencias de cualquier caracter de la a a la z (mayúsculas o minúsculas) o cualquier dígito.

Algunas veces necesitas encontrar ocurrencias de cualquier caracter excepto aquellos contenidos en una lista especial. Puedes, por ejemplo, estar buscando el primer caracter en una cadena que no sea un dígito hexadecimal.

Este tipo de búsquedas 'negativas' se realizan negando una clase de caracteres. Para hacerlo, debes insertar el caracter ^ al inicio de la clase. La suigiente expresión regular enucuentra cualquier caracter excepto los dígitos hexadecimales:

1 /[^A-Fa-f0-9]

Algunas clases de caracteres son tan comunes que tienen abreviaciones epeciales.

Abreviaciones especiales de clases de caracteres comunes

Para encontrar ocurrencias de cualquier dígito puedes usar la siguiente expresión regular:

1 /[0-9]/

pero también puedes lograr lo mismo de manera más concisa de la siguiente manera:

1 /\d/

La secuencia \d encuentra ocurrencias de dígitos.

Otras dos secuencias especiales muy útiles para clases de caracteres son:

  • \w encuentra ocurrencias de cualquier dígito, caracter del alfabeto o guiones bajos (_)
  • \s encuentra ocurrencias de espacios en blanco (espacio, tabulador, salto de línea)

Cada una de estas secuencias especiales tiene una forma negativa. Puedes encontrar ocurrencias de caracteres que NO sean dígitos usando la siguiente expresión:

1 /\D/

De manera similar, \W encuentra ocurrencias de cualquier caracter excepto caracteres alfanuméricos o guiones bajos y \S encuentra cualquier caracter que no sea espacio en blanco.

Cada operación de match o es exitosa (encuentra ocurrencias) o falla (no encuentra ocurrencias) de la expresión regular en la cadena. Empecemos con el caso mas simple: la falla. Cuando tratas de encontrar ocurrencias de una expresión regular en una cadena y la operación no tiene éxito, el resultado es siempre nil:

1 /a/.match('b') #nil

Contrario a nil, el objeto MatchData que una operación match exitosa regresa, tiene el valor Booleano verdadero, lo que lo hace útil para pruebas simples. Mas allá de esto, tambíen guarda información del resultado de la operación match, que puedes obtener usando los métodos adecuados del objeto MatchData: ¿en qué posición de la cadena inicia la ocurrencia de la expresión regular?, ¿qué porción de la cadena cubre?, ¿qué valores fueron capturados en los grupos entre paréntesis?, etc.

Para usar el objeto MatchData debes guardarlo primero. Considera un ejemplo en el que queremos obtener un número de teléfono de una cadena y guardar cada una de sus partes. Ejemplo p064regexp.rb

 1 cadena = "Mi número de teléfono es (123) 555-1234."
 2 telefono_re = /\((\d{3})\)\s+(\d{3})-(\d{4})/
 3 m = telefono_re.match(cadena)
 4 unless m
 5         puts "No se encontraron ocurrencias..."
 6         exit
 7 end
 8 print "La cadena completa: "
 9 puts m.string
10 print "La parte de la cadena que resulto en una ocurrencia"
11 puts m[0]
12 puts "Los grupos capturados: "
13 3.times do |i|
14         puts "Grupo ##{i+ 1}: #{m.captures[i]}"
15 end
16 puts "Esta es otra manera de obtener el primer grupo capturado"
17 print "Grupo #1: #{m[1]} "
18 

En este programa, utilizamos el método string del objeto MatchData (puts m.string) para obtener la cadena completa en la cual la operación match fue realizada. Para obtener la parte de la cadena que igaló a la expresión regular, indexamos el objeto m con el índice 0: m[0].

Utilizamos también el método times para hacer exactamente tres iteraciones y obtener los tres grupos encontrados en sucesión. Dentro del bloque del iterador utilizamos el método captures que contiene las subcadenas que regresaron ocurrencias de las partes entre paréntesis de la expresión regular. Finalmente, obtenemos nuevamente el primer grupo capturado esta vez con una técnica diferente: indexando el objeto MatchData m directamente.

El resultado es el siguiente: