Receta. Jugando con expresiones regulares en Mono y .NET

Hola gente!

En esta ocasión les hablaré sobre expresiones regulares, una maravilla para el tratado de cadenas..

Si creías que la clase System.String esta lindísima por las operaciones que puedes realizar sobre cadenas de manera simple (especialmente a los programadores que venimos de C), las expresiones regulares son excepcionales.

Hoy veremos a fondo como construir patrones para hacer coincidir cadenas, buscar partes de una cadena y realizar operaciones sobre estas partes.

No corras si no haz aprendido a caminar

Primero que nada debemos entender que es una expresión regular y para que sirve.

Una Expresión Regular es un conjunto de caracteres y metacaracteres que definen reglas sintácticas para la evaluación de una cadena de texto. Esto es, podemos definir la forma que debe tomar una cadena ya sea simplemente para validar o hacer corresponder esta cadena contra nuestra Expresión Regular.

Caracteres.. metacaracteres ..¿Que es esto?

Los caracteres ya los conocemos, los metacaracteres son caracteres especiales que tienen significados especiales. Ahora veremos un ejemplo, para ello, haremos uso de la memoria para recordar cuando escribimos ordenes en nuestra querida terminal. El ejemplo mas sencillo es el siguiente:

ls *.cs

rm *.dll

Donde pedimos listar todos los archivos con extensión cs (codigos de C Sharp) y pedimos eliminar todos los archivos .dll (ensamblados de mono). Debemos notar que el asterisco se reemplaza por 'cualquier cosa' por lo tanto este es un metacaracter ya que no significa lo que realmente significa, pues, en caso contrario las ordenes listarian y eliminarian el archivo llamado "*.cs" y "*.dll".

Manos a la obra!

Lo primero que haremos es hacer coincidir una expresión de cadena contra una Expresión Regular que no contendrá metacaracteres. Para esto, escribiremos una pequeña utilidad que nos informará si las expresiones de cadena coinciden con nuestra Expresión Regular.

Para poder escribir nuestra utilidad necesitaremos importar el espacio de nombres System.Text.RegularExpressions que es donde encontraremos las clases que haran el trabajo duro de comprobar y agrupar las expresiones de acuerdo al patrón (Expresión Regular) definido.

using System;
using System.Text.RegularExpressions

Una vez hecho esto, hemos traído todas las clases del especio de nombres System.Text.RegularExpressions a nuestro contexto actual.

Sin mas preámbulos continuaremos con nuestra pequeña utilidad validadores de expresiones.

El codigo que debermos escribir es el siguiente. Mas adelante comentaremos su funcionamiento.

C#:
  1. using System;
  2. using System.Text.RegularExpressions;
  3.  
  4. public class Validador {
  5.  
  6.     private static string expresion_regular = "hola";
  7.     private static string [] expresiones_a_validar = {
  8.         "hola mundo", // Esta cadena coincidira
  9.         "Hola Mundo",
  10.         "Reholas", // Esta cadena coincidirá¡
  11.         "que tal Mundo",
  12.         "Que tal Mundo",
  13.         "saludos Mundo",
  14.         "Saludos Mundo",
  15.         "Por fin hola", // Esta cadena coincidirá¡
  16.         "hola" // Esta cadena coincidirá¡
  17.     };
  18.    
  19.     public static void Main ()
  20.     {
  21.         Regex regex = new Regex (expresion_regular);
  22.        
  23.         foreach (string cadena in expresiones_a_validar)
  24.             if (regex.IsMatch (cadena))
  25.                 Console.WriteLine ("Expresión coincidente : {0}", cadena);
  26.     }
  27. }

¿Como funciona esto?

El codigo es sencillo, creamos una clase Validador que dentro del Main crea un objeto del tipo Regex (System.Text.RegularExpressions.Regex). Regex es acrónimo de RegularExpression y desde esta clase contamos con algunos métodos y propiedades que nos servirán para trabajarlas.

El método que usamos para validar tiene la firma : bool Regex.IsMatch (string input). Donde input representa la cadena a validar, Este método es el mas simple para realizar validaciones, pero el mas rápido, pues no almacena información alguna de las coincidencias. Éste método se limita a devolver un valor true o false como resultado de si la cadena input coincide con el patrón pasado cuando construímos el objeto Regex.

En resumen, esta pequeña aplicación recorre los elementos del arreglo expresiones_a_validar comparando en cada iteración si la cadena coincide con el patrón denotado por expresion_regular.

Dado que es un patrón muy simple, se validará si cada cadena contiene el texto hola en cualquier posición de esta.

Salida de la utilidad

validador

Metacaracteres disponibles

Metacaracter Descripción
\d \dCoincide con cualquier caracter de dígito.
\DCoincide con cualquier caracter que no sea dígito
Ej1. \d\d coincidirá con 10 20 y 30
o cualquier numero de dos dígitos.
Ej2.\D\D coincidira con ho la y mu o
cualquier conjunto de dos caracteres que no sean dí­gitos.
\w y \W \w Coincide con cualquier caracter de palabra (incluidos los numeros)
\W Coincide con cualquier caracter que no sea de palabra (incluidos los numeros)
Ej1. hola\wmundo coincide con la frase holaPmundo y con cualquier caracter
de palabra que este entre holamundo
Ej2. hola\Wmundo coincide con la frase hola-mundo y con cualquier caracter
que no sea de palabra que este entre holamundo
\s y \S \s Coincide con el caracter de espacio.
\S Coincide con cualquier caracter que no sea espacio
Ej1. hola\smundo coincide con la frase hola mundo
Ej2. hola\Smundo coincide con la frase hola-mundo
holaPmundo y cualquier caracter que no sea un espacio entre
hola y mundo
\uNNNN Coincide con el caracter unicode con valor NNNN (puesto en hecadecimal).
Ej1. \u004A coincide con el caracter J
[ ] Coincidirá con cualquier carácter dentro del grupo.
Ej1. [aeiou] coincide con cualquier vocal.
Ej2. [a-zA-Z] coincide con cualquier caracter de la a
a la z y de la A a la Z
( ) Sirven para agrupar expresiones.
Ej1. (hola|mundo) Coincide con la palabra hola ó mundo
Ej2. ((\d+)|(\d+.\d+)) Coincide para cualquier numero entero o con decimales.
| Permite alternar entre posibles patrones (uno u otro).
Ej1. hola|mundo coincide con la palabra hola mundo
^ Excluye los caracteres.
Ej1. [^a-z] coincide con cualquier caracter que no sea de la a a la z
Ej2. [^0-9] coincide con cualquier caracter que no sea del 0 al 9

Cuantificadores

Los cuatificadores sirven para denotar repetición de patrones, hacen a las Expresiones Regulares
mas cortas y faciles de entender a la vista. los cuatificadores que tenemos disponibles se muestran a continuación.

Cuantificador Descripción
* La expresión coincide si se encuentra cero o mas veces.
Ej1. [a-z]* una palabra conformada por solo caracteres de la
a la z incluyendo las cadenas vacías.
Coincidencias :

hola
mundo
como
estas
+ La expresión coincide si se encuentra una o mas veces.
Ej1. [a-z]+ una palabra conformada por solo caracteres de la
a la z sin incluir las cadenas vacías.
Coincidencias :

hola
mundo
como
estas
{n,m} La expresión coincide si se encuentra como mí­nimo n veces y como
máximo m veces. n y m son opcionales.

Ej1. [A-Z]{2,3} coincide con dos hasta tres caracteres juntos de la
A a la Z
Ej2. [A-Z]{2,} coincide al menos dos caracteres juntos de la
A a la Z
Ej3. [A-Z]{,3} coincide con máximo tres caracteres juntos de la
A a la Z
Ej4. [A-Z]{5} coincide con 5 caracteres juntos de la
A a la Z

Veamos un ejemplo demostrativo

Imaginemos que nuestros compañeros de la clase de programación no saben definir variables en el superlenguaje de programación C#. Escribiremos una expresión regular que evalua si una variable esta definida correctamente (hablando en terminos de sintaxis).
Es más, escribiremos un patrón para la definición de de variables tipo int y string.

Las expresiones quedarán algo asi:

Cadena a validar Expresión Regular
int entero; int\s+[a-zA-Z_]+;
int entero = 10; int\s+[a-zA-Z_]+\s*=\s*\d+;
string cadena; string\s+\w+;
string cadena = "hola mundo"; string\s+\w+\s*=\s*\"[\w\W]*\";

Muy bien. Esta receta se esta haciendo algo interminable y aun faltan varias cosas por explicar. Espero alguien se digne a enviarme una pizza ;) (peperoni porfavor).

Hay muchas posibilidades para crear expresiones regulares tan complejas como desees. Será cuestión de pensar un poquito y ordenar los elementos a fin de obtener el resultado deseado.

Grupos

Los grupos son un concepto bastante bonito, sobre todo porque construyendo nuestras expresiones regulares podremos agrupar los resultados en forma de grupos, obteniendo así trozos de cadenas correspondientes con el criterio defindo en nuestro patrón. A estos grupos podremos asignar nombres para luego obtener todas esas fracciones de cadenas (parte de la expresión a validar).

Como crear grupos

Para crear grupos.. Recuerdas los parentesis? si regresas y le hechas un vistazo a la tabla de metacaracteres, veras que dice
"Sirven para agrupar expresiones". asi que hasta el momento no he mentido.

Cada expresión agrupada entre parentesis será procesada como un grupo e incluso podemos asignarles un nombre usando la sintaxis:

(<nombre_del_grupo>expresión)

Así podremos crear una expresión regular de la forma:

id=(<id>\d+)

Lo que nos evaluar­á una cadena y por cada resultado que coincida (ej. id=100) lo tomará y creará un grupo llamado id que contendr­á el valor del id (100 para el ejemplo).

Con esto se abre un abanico de posibilidades, ahora veremos por que.

Un ejemplo.

Tenemos el clásico ejemplo de una tabla de datos con varios campos almacenados en un archivo de texto. al estilo de:

id Nombre Fecha Nac.
1 Nombre1 01/05/83
2 Nombre2 15/12/80
3 Nombre3 03/12/85

Lo que nos interesa es cambiar el formato de fecha (dd/mm/aa) por (dd/mm/aaaa) para que muestre el año de nacimiento.
Probablemente si hubieramos escrito esta utilidad en el 99 alguna compañía nos la hubiera comprado en millones de dolares
para evitar el famoso problema Y2K.

Lastima que en aquel tiempo era un chiquitillo que se conformaba solo con un buen videojuego (y todavía ;)).

Pero bueno, continuemos.. Además de la conversión de la fecha cambiaremos el orden para que termine asi :

id, Fecha de Nac., Nombre

Para poder entender mucho mejor el concepto de grupos escribiremos la utilidad que realiza la conversión y guarda los datos en
un nuevo archivo que agregue la terminación .salida al nombre del mismo.

C#, C#, C# ..Por fin!

Ok, ha llegado la hora de la verdad. Escribimos una pequeña utilidad inútil unos parrafos atrás. Pero ahora descubriremos nuevos horizontes que vienen subrogados a las expresiones regulares.

La clase Regex tiene bastantes elementos dignos de estudio pero por ahora nos limitaremos a solo algunos. Tal vez si alguien desea que agregue información sobre los demas miembros de la clase puedo considerarlo y escribir otra receta ;).

Mono y .NET nos proporcionan varias características interesantes para trabajar con expresiones regulares y grupos ahora veremos el ejemplo. Ya me ardí­an las manos de no escribir código ;).

Antes debemos de saber que la clase Regex tiene mas elementos interesantes. probablemente debamos saber que contamos con una propiedad Regex.GetGroupsNames para obtener el nombre de todos los grupos definidos en nuestra expresión y Regex.GetGroupsNumbers para obtener todos los numeros de grupos que representan el indice de estos.

Si queremos obtener los grupos de una coincidencia primero debemos obtener la Coincidencia (Match)) y dentro de ella podremos obtener los grupos con sus valores.

Para obtener las coincidencias llamamos al método Matches que tiene la firma MatchCollection Matches (string input) donde input representa la cadena que evaluaremos con nuestro patrón y MatchCollection representa una colleccion que podremos recorrer a fin de obtener todos los elementos Match que se encontraron en la cadena input

Match es una clase que almacena información acerca de la coincidencia completa. Por medio de ella podemos obtener los grupos como una colección que implementa los miembros de System.Collections.ICollection. Cada grupo podremos accesarlo por medio de sus nombres (Si es que asignamos nombres) o tambien siempre podremos accederlos por medio de su indice.

siguiendo el ejemplo de id=(<id>\d+) y teniendo como cadena lo siguiente:

id=1
id=2
id=30
id=50

Cada coincidencia (Match) representa la concordancia de la expresión regular completa. Es decir, cada objeto Match guardara el texto de la coincidencia. en el ejemplo cada objeto Match contendra:

id=1
id=2
id=30
id=50

Seguidamente podremos obtener los grupos de cada coincidencia (objeto Match) por medio de la propiedad
Match.Groups que es una collección que tiene los grupos expuestos en la expresión regular.

Nota. Tanto la colección MatchCollection como GroupsCollection podremos recorrerlas con una estructura foreach o alguna estructura repetitiva como for, while, do-while usando un í­ndice. Pero GroupCollection nos permite accesar también por sus nombres de grupos! (genial no?).

Bueno continuemos con el codigo de nuestro ejemplo el cual toma una tabla de datos con los campos separados por tabuladores para adecuar el formato de fecha y cambiar el orden de las columnas.

El código de nuestra utilidad es el siguiente:

C#:
  1. using System;
  2. using System.IO;
  3. using System.Text.RegularExpressions;
  4.  
  5. public class CambiaFormato {
  6.  
  7.     public static void Main (string [] args)
  8.     {
  9.         if (args.Length <1) {
  10.             Console.WriteLine ("Sintaxis: CambiaFormato.exe &lt;ArchivoDeEntrada&gt;");
  11.             System.Environment.Exit (-1);
  12.         }
  13.        
  14.         actualizaFormato (args [0]);
  15.     }
  16.    
  17.     public static void actualizaFormato (string archivoEntrada)
  18.     {
  19.         string formatoViejo = string.Empty;
  20.  
  21.         using (StreamReader sr = new StreamReader (archivoEntrada))
  22.             formatoViejo = sr.ReadToEnd ();
  23.        
  24.         Regex regex = new Regex (@"(?<id>\d+)\t(?<nombre>\w+)\t(?<dia>\d\d)/(?<mes>\d\d)/(?<anio>\d\d)");
  25.        
  26.         Console.WriteLine ("Nombres de grupos :");
  27.         foreach (string nombre_grupo in regex.GetGroupNames ())
  28.             Console.WriteLine ("\t{0}", nombre_grupo);
  29.  
  30.         Console.WriteLine ("Numeros de grupos :");
  31.  
  32.         foreach (int numero_grupo in regex.GetGroupNumbers ())
  33.             Console.WriteLine ("\t{0}", numero_grupo);
  34.            
  35.         StreamWriter sw = new StreamWriter (archivoEntrada + ".salida");
  36.         foreach (Match match in regex.Matches (formatoViejo)) {
  37.             int dia = int.Parse (match.Groups ["dia"].ToString ());
  38.             int mes = int.Parse (match.Groups ["mes"].ToString ());
  39.             int anio = int.Parse (match.Groups ["anio"].ToString ());
  40.             anio += 1900;
  41.            
  42.             sw.Write (match.Groups ["id"].ToString ());
  43.             sw.Write ("\t");
  44.             sw.Write (anio);
  45.             sw.Write ("\t");
  46.             sw.Write (dia.ToString ("00"));
  47.             sw.Write ("/");
  48.             sw.Write (mes.ToString ("00"));
  49.             sw.Write ("/");
  50.             sw.WriteLine (match.Groups ["nombre"].ToString ());
  51.         }
  52.         sw.Close ();
  53.     }
  54. }

Cuando lanzemos nuestra utilidad debemos pasar un parametro que sera el nombre del archivo que tiene la tabla de datos, en mi caso mi tabla de datos tiene el nombre tabla.txt por lo que para lanzar la utilidad lo hago asi:

Compilo.
mcs CambiaFormato.cs

Ejecuto.
mono CambiaFormato.exe tabla.txt

Al terminar la ejecución de la aplicacion se genera un nuevo archivo llamado tabla.txt.salida que contendra el nuevo formato de mi tabla con la fecha adecuada. Ademas se imprimiran los nombres de grupos y numeros de grupos.

¿Como sucedió?

Simple. lo único digno de comentar es el método actualizaFormato que es quien hace el trabajo sucio.

Los pasos que seguimos fueron los siguientes

  1. Abrimos el archivo tomado de la linea de comandos para lectura
  2. Leemos su contenido y lo almacenamos en la variable formatoViejo
  3. Creamos un objeto Regex pasando la expresión regular que crea los grupos adecuados (analizala y verás)
  4. Abrimos un archivo con el mismo nombre del archivo de entrada pero agregando al final .salida
  5. Recorremos todos los resultados llamando al método Regex.Matches pasando como argumento la cadena de
    entrada que será el contenido del archivo que teníamos almacenado previamente en la variable formatoViejo
  6. Por cada coincidencia (objeto Match) obtenemos sus grupos y obtenemos cada valor por medio de sus í­ndices de cadena
  7. Guardamos los valores del archivo que tenemos abierto formateandolo a como lo tení­amos planeado
  8. Cerramos el archivo de salida

Al final el resultado es el esperado. Uff!! pense que jamas terminaría!.

El archivo de codigo fuente con la tabla los puedes obtener aqui

Al final

Como vimos las expresiones regulares y su uso nos permiten realizar tareas muy complejas con poco esfuerzo. Podemos analizar cadenas y obtener fragmentos de ellas para tratarlas a como nos plasca.

Por lo tanto, doy oficialmente terminado este tutorial!

Espero les haya sido de utilidad pues he visto que no hay mucha documentación en la red a cerca de este tema dentro de mono.

Saludos y nos vemos para la proxima!
Espera la siguiente receta que seguro estará muy interesante! :)


Atte.
Ricardo Medina <ricki@dana-ide.org>

3 Responses to “Receta. Jugando con expresiones regulares en Mono y .NET”

  1. knocte Says:

    ¡Hola!
    Muy interesante pero, en mi opinión, las expresiones regulares han muerto :)
    Adiós a RegEx, bienvenido sea ReadEx: http://flimflan.com/blog/ReadableRegularExpressions.aspx

    Saludos y felicidades por tu blog ;)

  2. Ricardo Says:

    Interesante biblioteca..

    Pero también es importante saber que aunque puede ser un poco más sencillo trabajar con cadenas, implica más código..

    Lo bonito de las expresiones regulares es que podemos trasladarlas a otras plataformas de desarrollo con cero o ligeras modificaciones (si se diera el caso)...

    Yo aun prefiero las expresiones regulares que la lib que mencionas :) aunque podría ser buena opción para otras personas..

    Por cierto, las expresiones regulares jamás morirán!! :)
    Gracias por tu comentario y la liga ;)

  3. Bardier Says:

    O_o interesante la vida de un pogramador carajo...solo entendi la mitad....yo estoy aprendiendo a programar pero esto es demasiado jejeje

    http://randomteam.blogspot.com/

    si quieres ver la vida desde la mano de 5 Inadaptados y loquisimos Diseñadores Digitales

Leave a Reply