Introducción

“Expresiones regulares”, muchas veces abreviados por RE de regular expressions en inglés, sirven para buscar un texto dentro de otro de una forma sofisticada. Pueden servir, además, para dividir un texto complejo en componentes simples.

Un programador que domina expresiones regulares puede conseguir a dividir el código de un programa entero en símbolos con una sola llamada. No obstante, para el novato el escenario se presenta más bien al revés: puede gastarse el tiempo de escribir un programa entero para hacer bien una sola llamada con expresiones regulares.

Expresiones regulares son fascinantes y frustrantes a la vez. Este artículo trata de como podemos acercarnos al probablemente más ilegible lenguaje de programación con utilidad.

El próposito de expresiones regulares

En la programación se da muchas veces el caso de buscar una cadena de texto en otra. Por ejemplo, podemos buscar el texto “rata” en la cadena “errata”. “rata” se denomina el “patrón de búsqueda” (search pattern en inglés) o “subcadena” y “errata” el destino. (Para el destino hay más variedad de nombres.)

Si el patrón de búsqueda se encuentra dentro del texto a rastrear, es decir, si hay un match en inglés, entonces una función de búsqueda típicamente devuelve el índice de la primera ocurrencia de la subcadena (como la función strpos en PHP, o un puntero a la subcadena (como la función strstr en C).

Las funciones de búsqueda sin expresiones regulares existen también sin distinguir entre mayúsculas y minúsculas (case-insensitive en inglés) y comenzando la búsqueda desde el final – como stripos y strrpos en PHP, respectivamente.

Estas funciones son fáciles de entender, pero no ofrecen mucha flexibilidad. Por ejemplo, ¿qué hago para buscar la palabra “rata” y no simplemente la secuencia de cadena r-a-t-a dentro de cualquier palabra? Quiero una función que encuentre “rata” en “El gato caza la rata” pero no en “errata”. Podría apañarme buscando algo como espacio más “rata” más otro espacio, pero esto no funcionará si rata es la última o primera palabra. Es decir, lo que necesito es una función que encuentre mi patrón de búsqueda sólo cuando es una palabra entera y no parte de una palabra.

Pues, esta funcionalidad se obtiene con las expresiones regulares: una búsqueda con alternativas y cadenas variables en longitud y contenido.

Un ejemplo de carácter especial

Las expresiones regulares se basan en usar unos caracteres como especiales. Por ejemplo, el carácter “+” significa “el símbolo delante una o más veces. La expresión regular “ra+to” correspondería a “rato”, “raato” o “raaaaato”, pero no a “rto” (cero veces la letra a).

Hay muchos símbolo especiales que suelen estar explicados en la referencia de las funciones que usan expresiones regulares. Hay que tener en cuenta que existen pequeñas pero a veces frustrantes diferencias entre diferentes lenguajes de programación y proveedores de compiladores respecto a las expresiones regulares. Un dialecto de expresiones regulares que está muy difundido es el de perl – un lenguaje que hace mucho uso de expresiones regulares.

Componer expresiones regulares

En este artículo no quiero enumerar los códigos especiales – esto ya se hace bastante exhaustivamente en muchos manuales de referencia – sino quiero dar unas pistas de como un humano puede componer y entender un código como este:

\[\s*\w+(\s+\w+)*\s*\]

Mi forma preferida de crear una expresión regular es componerla de objetos cada vez más complejos. El código anterior corresponde en lenguaje humano a encontar “una o varias palabras entre corchetes separados o no por espacios”. Por ejemplo, algo como “[sección]”, “[índice de array]” o “[ corchetes con espacios ]”. Como una expresión regular es una cadena de texto, podemos codificarla como string.

Analizemos la definición “una o varias palabras entre corchetes separados o no por espacios”. Una palabra está compuestos por letras. El código de una letra cualquiera en una expresión regular es \w. “Uno o más” se códifica con +. Es decir, podemos decir que

const string PALABRA = "\\w+";

Aquí la barra inversa \ está duplicada porque se encuentra dentro de comillas. La mayoría de los lenguajes de programación usan la letra \ como letra de escape dentro de una cadena de texto que altera el significado de la letra siguiente. Por ejemplo, \t no es “barra más t” sino un carácter tabulador. Pues, si se quiere escribir una barra inversa, entonces la combinación es \\.

Una letra de espacio se codifica con \s. La letra * significa “cero o más”. Por lo tanto, \s* representa uno, varios o ningún espacio, es decir “separados o no por espacios”.

const string ESPACIOS_OPCIONALES = "\\s*";

Nótese que hemos no hemos denominado la constante “cero o más espacios” sino “espacios opcionales”. Es importante escoger nombres significativos pero los más cortos posibles para mejorar la calidad del código. Así conviene tener en cuenta palabras abstractas y colectivas.

De forma similar podemos codificar “uno o más espacios” con +.

const string AL_MENOS_UN_ESPACIO = "\\s+";

Como vemos, la ídea es reemplazar los códigos crípticos de las expresiones regulares por identificadores con una semántica más clara. Se pueden construir expresiones más complejas concatenando cadenas de texto, sin embargo esto no es siempre lo más práctico. Por ejemplo, es más intuitivo implementar una subexpresión regular (algo entre paréntesis) con una función.

string subexpr(string contenido)
{
    return "(" + contenido + ")";
}

“Ninguno o más” se puede implementar por

string ninguno_y_mas(string contenido)
{
    return subexpr(contenido) + "*";
}

Por mantener un mismo estilo conviene “transcribir” también construcciones que se usan menos a menudo como “en corchetes”.

string en_corchetes(string contenido)
{
    return "\\[" + contenido + "\\]";
}

Con las definiciones anteriores podemos crear expresiones más complejas como “entre espacios opcionales”

string entre_espacios_opcionales(string contenido)
{
    return ESPACIOS_OPCIONALES + contenido + ESPACIOS_OPCIONALES;
}

o varias “palabras” en lugar de una “palabra” simple

const string PALABRAS =
    PALABRA + ninguno_y_mas(AL_MENOS_UN_ESPACIO + PALABRA);

Con todo esto conseguimos que finalmente podemos construir una expresión para “Una o varias palabras entre corchetes separados o no por espacios”.

// Una o varias palabras entre corchetes separados o no por espacios
const string PALABRAS_ENTRE_CORCHETES =
	en_corchetes(entre_espacios_opcionales(PALABRAS));

Ahora la pregunta: ¿Qué es más intelligible? ¿PALABRAS_ENTRE_CORCHETES o el formato original \[\s*\w+(\s+\w+)*\s*\]?

Crítica de la composición

Una de las desventajas de componener expresiones regulares es que se puede perder la vista sobre las subexpressiones anidados. Cada función puede añadir paréntesis o no y subir el nivel de anidamiento. Normalmente esto no es un problema. Sin embargo, unas bibliotecas devuelven también resultados que se basan en estas subexpresiones – normalmente de una forma ordenada en un array. Cambiar la estructura de subexpresiones cambia el orden y la cantidad de estos subresultados. No será demasiado difícil adaptar el código si siempre se analiza la misma expresión regular, pero puede complicarse cuando la expresión es variable.

Otra desventaja es que la composición requiere un tiempo de cálculo durante la ejecución del programa. Sin embargo, esto es despreciable en la mayoría de las veces y suele ser suficiente componer una expresión regular una vez y luego guardarla en una variable estática.

La composición debe tener en cuenta los diferentes dialectos de expresiones regulares si quieren ser generales. También puede ser necesario añadir caracteres adicionales. Algunas funciones como preg_match requieren limitar una expresión regular por dos símbolos iguales como “/” + expresión + “/”. Tras el último limitador puede haber modificadores como “i” como case-insensitive.

Analizar expresiones regulares

Los ejemplos para buscar un determinado tipo de texto suelen dar las expresiones regulares tan feas como inevitablemente son. Para dar con este chorro de caracteres usamos sustituciones similares como en el apartado de composición aunque esta vez sin la necesidad de obtener un programa compilable.

Como ejemplo analizamos el ya conocido código de \[\s*\w+(\s+\w+)*\s*\] y pretendemos que aún no sabemos lo que es. Podríamos reemplazar los \s por <espacio> y los \w por <carácter>. Sin embargo es más directo si tenemos en cuenta los cuantificadores + (uno o más) y * (cero o más veces). De hecho, con un poco de práctica podemos interpretar el código de “un carácter o más” \w+ como “palabra”.

// El código original
\[\s*\w+(\s+\w+)*\s*\]

// se puede quedar en algo como
\[
<espacios opcionales>
<palabra>(<uno o más espacios><palabra>)*
<espacios opcionales>
\]

Resolvemos los paréntesis en la línea central de nuestro pseudo-código.

// La subexpresión ()* se repite entre cero y n veces
<palabra>(<uno o más espacios><palabra>)*

// se puede quedar en algo como
<palabra> cero_o_mas(<espacios><palabra>)

Una palabra más cero o más “espacios + palabra” son simplemente “palabras separadas por espacios” – popularmente conocido como “texto”. Los códigos \[ y \] son simplemente corchetes. Se deben escapear estos símbolos con \ ya que los corchetes tienen un significado especial en una expresión regular.

Con todo esto, la expresión regular dice algo como

[
<espacios opcionales>
<texto>
<espacios opcionales>
]

A esta altura ya podemos ver que la expresión \[\s*\w+(\s+\w+)*\s*\] representa a un texto entre corchetes que no necesitan estar pegados a la primera y última letra del texto.

Observaciones sobre el análisis

Como hemos visto, el análisis puede acabar en líneas largas. Si tenemos en cuenta que no necesitamos seguir ninguna sintaxis particular para el análisis de una expresión regular, entonces podemos añadir espacios o salto de líneas a nuestro gusto. Podemos copiar la expresión regular en un procesador de texto como Microsoft Word y cambiar el color de texto o la fuente si nos ayuda. El análisis es para que nosotros entendamos – no el ordenador.

Hemos hecho este análisis escribiendo, pero con un poco de práctica se puede hacer en la mente. No hay un camino de oro de como empezar. Puede convenir a reemplazar en primer lugar trozos pequeños de la expresión regular por identificadores singificativos o puede ser mejor empezar por la sustitución de subexpresiones entre paréntesis. Lo más importante es que no dejemos de ver el bosque por tantos árboles y escogemos los identificadores adecuados. El código \w+ no es “uno o más caracteres” sino una “palabra”.

Conclusión

Como expresiones regulares no permiten espacios y saltos de línea como elemento de estructuración, se hacen difícil a leer y entender. He propuesto una manera de componer y analizar expresiones regulares que transforma símbolos crípticos a conceptos humanos.

Hay muchos artículos sobre qué es una expresión regular pero hasta ahora no he visto ningún artículo similar para no perderse en ellas. Un poco me sorprende porque mi enfoque me ha ayudado bastante y creo todo el mundo tiene problemas similares a la hora de iniciarse en las expresiones regulares.

De hecho podría ser un avance de crear un lenguaje de expresiones regulares estructuradas que permite escribir patrones de búsqueda en un formato más legible para los humanos y que se dejaría transformar fácilmente en las expresiones regulares habituales.

Referencias

Páginas externos
En este mismo sitio