martes, 9 de junio de 2015

Expresiones regulares en .NET

De todos es sabido que cuando tienes un problema e intentas solucionarlo con una expresión regular, acabas teniendo 2 problemas (el original y el añadido de tener que entender y mantener la expresión regular en tu código).

En cualquier caso, hay veces donde una expresión regular sí es la solución y es entonces cuando resulta necesario tener claros algunos conceptos. Inicialmente, mi escaso conocimiento sobre las expresiones regulares no era un tema que me preocupara (mas me vale por a día de hoy tampoco soy ningún experto), sabía lo que sabía y con ello sobrevivía, pero a medida que las he ido utilizando con más asiduidad (esto es en vez de “nunca” a “de pascuas a ramos”) he ido perfilando una guía de supervivencia que espero me permita enfrentarme a ellas con una mayor y “aparente” seguridad.

Para trabajar con expresiones regulares en .NET, utilizaremos la clase Regex del espacio de nombres System.Text.RegularExpressions.

Regex expone tanto métodos estáticos como métodos de instancia.

Los métodos estáticos crearán internamente una instancia de la propia clase Regex, pero cachearán la instancia creada para no tener que instanciarla de nuevo cuando se vuelva a llamar a un método estático con la misma expresión regular (el número de entradas de la caché es configurable según la propiedad CacheSize). De este modo, si sabemos de antemano que instanciaremos frecuentemente la misma expresión regular, utilizar los métodos estáticos nos reportará un mejor rendimiento.

Si trabajamos con métodos de instancia y relacionado con el rendimiento, otra opción sería compilar la expresión regular, que tardaría un poco más en instanciar el objeto pero a cambio sucesivas llamadas a la misma instancia no tendrían que interpretarla de nuevo cada vez. También podemos compilar expresiones regulares en un ensamblado con CompileToAssembly, pero lo cierto es que a día de hoy veo lejana esta opción :)

Para crear una instancia de Regex tendremos que suministrarle un patrón con la expresión regular y, opcionalmente, un opciones en un valor del tipo RegexOptions. Las opciones son modificadores para la expresión regular especificada en el patrón. Por ejemplo IgnoreCase (para no distinguir mayúsculas de minúsculas), Multiline (para cambiar el significado de ^ y $), etc. RegexOptions es un enumerado decorado con el atributo [Flags], luego acepta varias valores.

var pattern = "^sergio$";
// con RegexOptions.IgnoreCase | RegexOptions.Multiline se encontrarán 2 coincidencias, sin estas opciones no se encontrará ninguna
var regex = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Multiline);
var input = "sergio\nSERGIO";
var match = regex.Match(input);
while (match.Success)
{
Console.WriteLine("Value {0} Index {1} Length {2}", match.Value, match.Index, match.Length);
match = match.NextMatch();
}
Si especificamos las opciones a través de RegexOptions se aplicarán a toda la expresión regular. Sin embargo, también es posible establecer estas opciones en línea en el texto de la propia expresión regular, que además permitirá entonces activar o desactivar la opción selectivamente. Por ejemplo, la anterior expresión regular podría rescribirse como sigue:
var pattern = "(?im)^sergio$";

Y aplicar las opciones de forma selectiva podemos verlo en el siguiente pantallazo que además muestra como ReSharper nos puede ayudar a depurar fácilmente nuestras expresiones regulares. Por ejemplo en esta expresión regular estamos diciendo que inicialmente sea case-insensitive y multiline pero que “gio” sea case-sensitive.

image

Otro gran problema de las expresiones regulares es que cuando vuelvas a ellas tiempo después ya no sabrás que querías buscar. En mi caso utilizo normalmente una herramienta como http://www.regexr.com/ para crear la expresión regular y, después de mucho método ensayo-error, doy con la expresión adecuada que finalmente copio y pego en mi código con algún que otro comentario más o menos afortunado…

// multiline
// al comienzo de cada línea y entre paréntesis un prefijo
// (+número) pudiendo haber espacios antes y después del/los número(s)
// opcionalmente espacios
// 9 números sin espacios para terminar la línea
var pattern = @"(?m)^\(\s*\+\s*(\d+)\s*\)\s+(\d{9})$";

Desde luego más vale ese comentario que nada, pero hay otra formas mejor de hacerlo que es incluir comentarios en la propia expresión regular. El primer tipo de comentario que podemos incluir es un comentario en línea con la sintaxis (?# comentario…)

var pattern = @"(?m)(?# multiline)^\(\s*\+\s*(\d+)\s*\)(?# prefijo)\s+(\d{9})(?# número)$";

Probablemente los comentarios en líneas sea útiles para expresiones regulares sencillas pero en el momento que sean complicadas o simplemente tengan un tamaño considerable, parece mejor usar los comentarios “hasta final de línea”. Para incluir este segundo tipo de comentarios tenemos que activar la opción/modificador IgnoreWhiteSpace que lo hace es que se ignoren los espacios en blanco en la expresión regular, habilitando de este modo que podamos incluir este nuevo tipo de comentarios con la sintaxis # comentario

var pattern = @"(?mx) # MultiLine, IgnoreWhiteSpace
^\(\s*\+\s*(\d+)\s*\) # Prefijo con paréntesis inicial y final obligatorio
# con espacios opcionales después del paréntesis inicial y antes del parántesis final
# en medio desde 1 a n dígitos contiguos
\s+ # espacios desde 1 a n
(\d{9})(?# número)$ # Teléfono de 9 dígitos contiguos
";

Otro concepto que me parece importante entender para poder avanzar y no usar sólo el método IsMatch (que está bien, evita overhead porque no captura nada, pero por contra nos estamos perdiendo todo el potencial de las expresiones regulares) es qué son las coincidencias, grupos y capturas.

Cuando buscamos coincidencias en un texto según una expresión regular, no sólo podemos comprobar que el texto coincide con nuestro patrón, sino también capturar ciertas porciones de la cadena para poder trabajar con ellas después. Esto es un grupo y es representado por el objeto Match. Para capturar un grupo utilizamos los paréntesis.

var pattern = @"(.+)@(.+)";
var regex = new Regex(pattern);
var input = "sergio @panicoenlaxbox\ncarmen @panicoenel20";
var match = regex.Match(input);
if (match.Success)
{
do
{
Console.WriteLine("Match - Value {0} Index {1} Length {2}", match.Value, match.Index, match.Length);
for (int i = 1; i < match.Groups.Count; i++)
{
Group group = match.Groups[i];
Console.WriteLine("Group - Value {0} Index {1} Length {2}", group.Value, group.Index, group.Length);
}
match = match.NextMatch();
} while (match.Success);
}

Imprime por consola lo siguiente:

Match - Value sergio @panicoenlaxbox Index 0 Length 22
Group - Value sergio Index 0 Length 6
Group - Value panicoenlaxbox Index 8 Length 14
Match - Value carmen @panicoenel20 Index 23 Length 20
Group - Value carmen Index 23 Length 6
Group - Value panicoenel20 Index 31 Length 12

Como vemos no hay en .NET un modificador g (global) sino que se accede a la siguiente coincidencia llamando a NextMatch() o directamente utilizando el método Matches en vez de Match.

var matches = regex.Matches(input);
foreach (Match match in matches)
{
//...
}

Además de coincidencias (Match) y grupos (Group) también aparecen las capturas (Capture). Una captura se produce cuando un grupo tiene un cuantificador y por ende se captura n veces. Por defecto, cuando preguntamos por el valor de un grupo nos devuelve el valor de su última captura, pero esto podría no servirnos si las capturas de un grupo tienen distintos valores. Por ejemplo:

image

Otra cosa acerca de los grupos es que además de poner acceder a ellos por un índice también podemos darles un nombre con la sintaxis ?<nombre>

var pattern = @"(?<name>.+)@(?<twitter>.+)";
var regex = new Regex(pattern);
var input = "sergio @panicoenlaxbox\ncarmen @panicoenel20";
var matches = regex.Matches(input);
foreach (Match match in matches)
{
Console.WriteLine("Match - Value {0} Index {1} Length {2}", match.Value, match.Index, match.Length);
Group group = match.Groups["name"];
Console.WriteLine("Group - Value {0} Index {1} Length {2}", group.Value, group.Index, group.Length);
group = match.Groups["twitter"];
Console.WriteLine("Group - Value {0} Index {1} Length {2}", group.Value, group.Index, group.Length);
}

Alrededor de los grupos gira mucho de las capacidades de la expresiones regulares, por ejemplo las backing references permiten hace mención a un grupo previamente capturado con \número_de_grupo. En el siguiente ejemplo, tanto el primer caracter como el último tienen que ser iguales:

image

Incluso sería más sencillo si le damos un nombre al grupo y después lo referenciamos con \k<nombre_de_grupo>

(?<delimiter>.{1}).+\k<delimiter>{1}

También existe el concepto de no capturar un grupo. Es decir, necesitamos paréntesis para aplicar un cuantificador o cualquier otro modificador pero no queremos que se produzca una captura del grupo. Esto lo conseguimos con lo que se denomina un “non-capturing group”

(?:pattern)

Con la opción ExplicitCaptures lo que conseguimos es que no se capture ningún grupo excepto aquellos que tengan nombre..

Algo que también crea mucha confusión es que un cuantificador es greedy por defecto (avaricioso). Esto significa que consumirá la mayor cantidad posible de texto para satisfacer al cuantificador. A veces justamente queremos lo contrario, que consuma sólo lo mínimo e imprescindible para que el cuantificador sea cumplido. Para indicar a un cuantificador que sea Lazy hay que utilizar ? a continuación del cuantificador.

El ejemplo canónico para ver el tema de greedy vs lazy es capturar en un grupo la etiqueta de apertura y cierre de un elemento HTML.

  • Con la expresión <.+> y para el texto <p>Sergio</p> habrá un match que será todo el texto “<p>Sergio</p>”. Esto es porque el cuantificador + del punto “encaja” con todo el texto y sólo se detiene en el último caracter para satisfacer el > final.
  • Sin embargo con <.+?> tendremos 2 matches. “<p>” y “</p>”. El operador lazy le dice la expresión regular que tiene que parar en el primer > que encontró (el del cierre de la apertura de la etiqueta).

Otra posibilidad que nos dan los grupos es la de utilizarlos para reemplazar texto con el método Replace. En este método podemos hacer mención a un grupo capturado con $número_de_grupo o ${nombre_de_grupo}. Por ejemplo (y utilizando ahora el plugin Regular Expression Tester Extension.

image

La verdad es que las expresiones regulares dan para muchos posts, pero como decía al comienzo, tampoco quiero excesivos problemas y me basta con saber lo justo y necesario :)

Un saludo!