Inicio > .NET Framework, C#, Cuestionario > Cuestionario de expresiones regulares: 2. creación de expresiones

Cuestionario de expresiones regulares: 2. creación de expresiones


En la primera parte de este cuestionario revisamos como manipular expresiones regulares; es decir, ya que contamos con el texto de una expresión, cómo usamos .NET Framework para manipular los resultados. Pero then again, no hemos visto cómo crear las expresiones que .NET evaluará.

Crear estas expresiones es un tema complicado y muy extenso, pero trataremos de ver los símbolos más utilizados. Prosigamos.

¿Cómo hago concordar texto simple?

El patrón para el texto simple es… simple. Si quiero encontrar todas las cadenas que contengan “fer”, el patrón es ese tal cual: “fer”.

string pattern = @"fer";

Regex regex = new Regex(pattern);
string text = "fernando";
Match match = regex.Match(text);
if (match.Success)
  Console.WriteLine(match.Value);
else
  Console.WriteLine("No hay concordancias.");

Aquí, “fernando” contiene el patrón “fer”, por lo que concuerda con el texto fuente.

¿Y que tal si quiero buscar al inicio o al final del texto?

Para concordar texto al inicio, se utiliza el símbolo ^, mientras que $ se utiliza para concordar al final. Así, “^fer” concordará con “fernando”, pero “fer$” no lo hará. Para que concuerde al inicio y al final, pues combinamos ambos: “^fer$” buscará cadenas que concuerden únicamente con “fer”.

¿Cómo puedo concordar con el inicio o fin de una palabra?

Los símbolos ^ y $ sirven para concordar con el inicio o fin del texto, pero si queremos buscar en inicio o fin de palabras intermedias no nos sirve. Supongamos que dado un texto quiero buscar palabras que comiencen con “fer”. En este tenor, “mi nombre es fernando gómez” y “es un día feriado” concordarían, mientras que “una categoría inferior”.

Para ello, utilizamos \b (así con la diagonal invertida) antes del patrón, para hacer la concordancia al inicio de la palabra, o después del patrón para concordar al final. El patrón para el ejemplo anterior sería “\bfer”.

string pattern = @"\bfer";
Regex regex = new Regex(pattern);
string text = "mi nombre es fernando gómez";
Match match = regex.Match(text);
if (match.Success)
  Console.WriteLine(match.Value);

Lo contrario a lo anterior, es decir buscar una concordancia que no esté al inicio o al final de una palabra, se logra utilizando de forma similar \B. Así, “\Bfer” concuerda con “categoría inferior”, pero no con “mi nombre es fernando”.

¿Cómo puedo concordar puros dígitos?

Si queremos hacer una búsqueda por puros números (es decir, del 0 al 9) utilizamos \d. En contraparte, podemos utilizar \D si queremos buscar cualquier caracter menos un dígito. Así, el patrón “\d\d” (dos dígitos consecutivos) concuerda con “tengo 27 años” (y la concordancia regresa “27”, obviamente).

¿Y qué pasa con los caracteres en blanco?

Similar a lo anterior, tenemos a \s para concordar con espacios blancos (espacios, tabs, salto de líneas, etc.), mientras que \S concuerda con cualquier caracter que no sea espacio blanco.

¿Cómo se identifican las palabras?

Una palabra, en el contexto de expresiones regulares, se define como un conjunto de caracteres consecutivos que puede ser cualquier combinación de letras mayúsculas (de la A a la Z), minúsculas (de la a a la z), dígitos (del 0 al 9) y el caracter de guión bajo _. Si queremos concordar un caracter de una palabra (es decir, A-Z, a-z, 0-9 ó _ ) utilizamos \w y en contraparte, \W para cualquier caracter que no pertenezca a una palabra.

¿Y qué pasa si quiero concordar varios dígitos, caracteres en blanco o de palabras?

Por ejemplo, ¿qué pasa si quiero concordar cinco dígitos? Por supuesto, podría escribir “\d\d\d\d\d”, pero esto es poco práctico (¿qué pasaría si en lugar de cinco fueran veinte?). Para solventar este problema existe la expresión {n}, donde n es un número natural que repite n veces la expresión inmediata anterior. Así, \d{5} concordaría cinco dígitos, \w{3} concordaría 3 caracteres de palabra y \s{7} serían siete espacios en blanco.

Más aún, si agregamos una coma después del número, esto es {n,}, indicamos que queremos concordar al menos n caracteres. Así, \d{5,} concordaría con al menos cinco dígitos (por ejemplo, “12345678”).

Y si incluimos algún número después de la coma, es decir {n,m} (con n <= m) indicamos que queremos concordar al menos n y a lo más m caracteres. Así, \w{3,10} concordaría con caracteres de palabra (recordemos: A-Z, a-z, 0-9 y _ ) que midan tres y hasta diez caracteres de longitud.

Por cierto, existe el símbolo especial ? que concuerda la subexpresión anterior cero o una vez, y evidentemente es equivalente a {0,1}. Por ejemplo, \d? concuerda con cero o un dígito, y es equivalente a \d{0,1}. Es solo que {0,1} es tan empleado que se le asignó un símbolo especial.

De igual forma, podemos representar {0,} (es decir, cero concordancias en adelante) con el símbolo *. Así, f\w*r concuerda con fr, fer y fabricar.

Y ya entrados en gastos, también existe abreviación para {1,}, es decir, una o más concordancias, y se utiliza el símbolo +. Así, f\w+r concuerda con fer y fabricar, pero no con fr.

¿Cómo podría concordar con algún conjunto de caracteres?

Supongamos que queremos buscar un conjunto de caracteres explícitamente especificado. Por ejemplo, supongamos que queremos concordar las vocales a, e, i. ¿Cómo se puede indicar?

Los conjuntos se forman con corchetes y poniendo adentro los caracteres que nos interesan. Por ejemplo, [aei] busca una a, una e o una i, pero no busca ni la o ni la u. Igual podemos mezclarlo con otros caracteres. Por ejemplo, [cpv]elo concuerda con celo, pelo y velo, pero no concuerda con lelo.

¿No hay otra forma de especificar un conjunto de caracteres?

Digo, si queremos concordar, por ejemplo, que una cadena tenga puras letras minúsculas, ni modo de escribir [abcdefghijklmnñopqrstuvwxyz]. Digo, es válido, pero qué hueva. En lugar de eso, podemos especificar rango de caracteres poniendo un guión entre los caracteres (o dígitos) que funjan como cotas. El ejemplo anterior quedaría reducido a [a-z]. Por supuesto, [A-Z] concordaría un texto con puras mayúsculas.

Y aún podemos especificar varios rangos en el mismo conjunto. Por ejemplo, [A-Za-z] concordaría cualquier caracter con mayúsculas y minúsculas, pero sin dígitos.

En base a esta definición, podríamos decir que \w equivale a [A-Za-z0-9_].

¿De qué forma especifico que se pueda concordar una opción u otra?

Por ejemplo, puedo querer concordar todos los caracteres que sean 0 o 1. Es decir, dar una serie de opciones sobre las que elegir.

Para lograr lo anterior, utilizamos una barra vertical | para separar cada una de las opciones. Por ejemplo, dado [0|1]+ y buscando 99901, obtendría como concordancia 01 y se desecharía el 999.

¿Hay alguna forma de concordar cualquier caracter especial o no especial?

A veces podemos estar haciendo validaciones sobre texto que está escrito en otro idioma que no es nuestro español. Y a veces no disponemos en nuestro teclado de caracteres especiales de otros idiomas. Por ejemplo, si el texto está en alemán quizás queramos concordar el caracter especial “scharfes s”, ß, que seguramente estará incluido en teclados alemanes pero no en español (ni siquiera en inglés).

Por ejemplo, si queremos buscar en un texto palabras que contengan la scharfes s, podríamos recurrir al mapa de caracteres de Windows y escribir la expresión regular \w*ß\w*. Así, “essen” (comer) no concordaría, pero “Straße” (calle) sí que lo haría.

Sin embargo, podemos representar cualquier caracter a través de su código Unicode. Para ello, empleamos \uNNNN, donde NNNN es el código Unicode del caracter. Así, dado que ß tiene como valor Unicode 00DF (en hexadecimal), podemos reescribir la expresión regular como \w*\u00DF\w* y listo, “Straße” concordaría sin problemas.

Esta técnica es especialmente útil cuando uno trata con caracteres especiales. Por ejemplo, \u00A9 concordaría el símbolo de copyright © mientras que \u20AC concordaría con el símbolo del euro €.

¿De qué forma podemos obtener el conjunto de caracteres contrario a una expresión dada?

Supongamos que tenemos la expresión [0-9], que quiere decir cualquier dígito entre el cero y el nueve. Y supongamos que queremos expresar: cualquier caracter que no esté entre el 0 y el nueve. Para lograr esto, utilizamos el símbolo ^ dentro de los corchetes. Si lo especificamos fuera, indicamos que la expresión tiene que concordar al inicio de la cadena. Así, nuestro ejemplo quedaría como [^0-9], lo cual, dicho sea de paso, es equivalente a \D. De igual forma, \W es equivalente a [^A-Za-z0-9_].

¿Qué es un grupo?

Un grupo es una construcción que nos permite capturar subexpresiones dentro de una expresión regular. Por ejemplo, podemos crear una expresión regular sobre la cual queramos buscar los diferentes componentes que contenga, o bien aplicar algún modificador (como * o ? o +) sobre una subexpresión.

Por ejemplo, pensemos que queremos que una cadena de texto termine en “.com” o “.net”. tendríamos que especificar \w+ seguido de \. para indicar al punto, y com|net para especificar las opciones com ó net. Pero queremos asegurarnos que .com o .net aparezcan forzosamente. Esto lo logramos con ?, pero ¿cómo aplicarlo tanto para el punto como para el com|net? La solución es agrupar. Para hacerlo, simplemente ponemos la subexpresión entre paréntesis. Así, la expresión regular para nuestro ejemplo anterior sería \w+\.(com|net) la cual concordaría con “gmail.com” y “gmail.net”, pero no con “gmail.mx”. Nota que ya no fue necesario el ? porque con agrupar com|net fue más que suficiente.

La otra opción para los grupos es identificar los componentes de las subexpresiones. Pensemos una versión sencilla (y ciertamente no muy rigurosa) de una expresión regular que valide un correo electrónico estándar. Este estará formado por un conjunto de caracteres de palabra, seguido de una arroba, seguido de otro conjunto de caracteres palabra, seguido de un punto y una palabra de dos o tres letras. Algo como: fernando@gmail.com. Una expresión regular que cumpla con esto podría ser: \w+@\w+\.[A-Za-z]{2,3} donde:

  • \w+ – una palabra de por lo menos un caracter de longitud.
  • @ – concuerda exactamente con la arroba.
  • \w+ – otra palabra de por lo menos un caracter de longitud.
  • \. – el caracter punto (nota la diagonal invertida: tenemos que escapar el caracter).
  • [A-Za-z]{2,3} – representa cualquier palabra de dos o tres caracteres con mayúsculas o minúsculas: por ejemplo, “com” o “mx”.

Dado este sencillo ejemplo, fernando@gmail.com y fernando@unam.mx concuerdan con nuestro patrón. De esta forma, ya podríamos hacer una validación sencilla de un correo electrónico.

Sin embargo, supón que tu jefe te dice: bien, ahora quiero que me digas cuál es el nombre de usuario y cuál es el dominio. De pronto, el mundo se viene encima. ¿Cómo podríamos obtener el dominio y el usuario? Con grupos.

.NET Framework provee una forma de acceder a los grupos, ya sea por nombre o por ubicación (contando de izquierda a derecha, siendo el primer grupo siempre la expresión completa). Así, podríamos escribir nuestra expresión como (\w+)@(\w+\.[A-Za-z]{2,3}). Nota que creamos dos grupos: (\w+) nos da el grupo correspondiente al nombre de usuario (es decir, lo que va antes de la arroba) mientras que el grupo (\w+\.[A-Za-z]{2,3}) nos da el dominio, así sin más. De esta forma, tenemos que el primer grupo es la expresión completa (siempre el primer grupo es toda la expresión regular, siempre siempre siempre), el segundo grupo nos da el usuario y el tercero el dominio. Ahora solo tenemos que usar las clases de .NET para obtener el segundo y tercer grupo, y listo: problema resuelto.

¿Y cómo obtengo los grupos usando las clases en .NET?

Muy fácil. La clase Match contiene la propiedad Groups, de tipo GroupCollection. Basta con obtener, en el ejemplo de la pregunta anterior, el segundo y tercer elemento de la colección.

string pattern = @"(\w+)@(\w+\.[A-Za-z]{2,3})";

Regex regex = new Regex(pattern);
string text = "fernando@gmail.com";
Match match = regex.Match(text);
if (match.Success)
{
  Console.WriteLine(match.Value);
  string user = match.Groups[1].Value;
  string domain = match.Groups[2].Value;
  Console.WriteLine("Usuario: {0}", user);
  Console.WriteLine("Dominio: {0}", domain);
}
else
  Console.WriteLine("No hay concordancias.");

Este ejemplo muestra lo siguiente en pantalla:

fernando@gmail.com
Usuario: fernando
Dominio: gmail.com

¿Ves qué sencillo?

¿Por qué no hablaste de los grupos en la primera parte del cuestionario, cuando vimos las clases de .NET para las expresiones regulares?

Bueno, cierto que se suponía que la primera parte del cuestionario trataría código y la segunda, las expresiones como tal. Sin embargo, el problema es que los grupos son, a final de cuentas, parte de las expresiones, y no tenía mucho sentido hablar de la clase Group y la propiedad Match.Groups si no sabías antes qué es un grupo y para qué sirve. Y eso solo lo podía explicar cuando tratara las expresiones. De hecho, dada su importancia, me vi tentado a crear una tercera parte del cuestionario para tratar el tema de los grupos, pero la verdad es que, fuera de lo expuesto en las dos preguntas anteriores, no hay mucho más que tratar. Bueno, quizás solo el hecho de que puedes asignar un nombre a un grupo…

¿Uh? ¿Cómo está eso de que puedes nombrar un grupo?

Y sí, es posible. Imagínate una expresión con muchos grupos. Sería un tedio buscar los grupos por su índice. Una forma más fácil sería hacer:

string user = match.Groups["Usuario"].Value;

Para ello, evidentemente habríamos de asignar al grupo 1 el nombre de “Usuario”…

¿Y cómo diantres hago eso?

Ah, sencillo. Después del paréntesis, pones ?<nombre>, donde nombre es… ahm… el nombre del grupo…

Solo que aguas. El nombre no debe contener ningún signo de puntuación ni empezar con algún número. Así, la expresión (\w+)@(\w+\.[A-Za-z]{2,3}) la podríamos reescribir como:

(?<Usuario>\w+)@(?<Dominio>\w+\.[A-Za-z]{2,3})

y ahora sí, match.Groups contendrá tres grupos: el primero, sin nombre, que contiene toda la expresión regular. El segundo será llamado “Usuario” y el tercero, “Dominio”. Podemos reescribir el código de ejemplo de la siguiente forma:

string pattern = @"(?<Usuario>\w+)@(?<Dominio>\w+\.[A-Za-z]{2,3})";

Regex regex = new Regex(pattern);
string text = "fernando@gmail.com";
Match match = regex.Match(text);
if (match.Success)
{
  Console.WriteLine(match.Value);
  string user = match.Groups["Usuario"].Value;
  string domain = match.Groups["Dominio"].Value;
  Console.WriteLine("Usuario: {0}", user);
  Console.WriteLine("Dominio: {0}", domain);
}
else
  Console.WriteLine("No hay concordancias.");

y obtendríamos el mismo resultado. Y eso es todo lo que tengo que decir de los grupos.

¿Seguro es todo o nada más te haces güey?

Osh, bueno (don|doña) perfect(o|a), por supuesto que no es todo, hay muchas cosas más que decir. Pero no puedo decirlo todo en un simple cuestionario, y menos aún en un blog. Como he mencionado, se pueden escribir libros enteros sobre expresiones regulares. Pero a final de cuentas, esto que mencioné ha sido lo más importante.

Si tienes más dudas sobre grupos, te recomiendo que leas el artículo construcciones de agrupamiento en la documentación de MSDN.

Y colorín colorado…

…este cuestionario ha terminado.

Espero que este cuestionario de dos partes te sirva, por lo menos, para introducirte al fascinante, potente y complejo mundo de las expresiones regulares. Por supuesto, hay varios temas que dejé fuera, por lo que te recomiendo ampliamente que no dejes de echarle un vistazo a la documentación de MSDN: elementos del lenguaje de expresiones regulares.

Lo que sigue depende de tí. Evidentemente hay que practicar mucho y repasar estos temas seguido, ya que siendo un tema complejo luego se nos puede olvidar algún aspecto. Bueno, al menos yo sí tengo que regresar a la documentación de forma seguida para refrescar la memoria.

Y bueno, ahora sí a dormir, que nuevamente termino una entrada en el blog a las tres de la mañana. Con suerte, si esto te sirve de algo, habrá valido la pena.

Auf wiedersehen!

Anuncios
Categorías:.NET Framework, C#, Cuestionario Etiquetas:
  1. And
    noviembre 11, 2012 en 1:35 pm

    Muchas gracias. Has sido de gran ayuda.

  1. No trackbacks yet.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s