Primero que todo ¿Qué es una expresión regular?
Una expresión regular también conocida en la teoría computacional como “regex”, es en sí misma un pequeño lenguaje de programación que puede ser usado por otros, como Python, para hacer búsquedas o extracciones de patrones en las cadenas de caracteres (strings).
Entonces ¿Por qué tendría que saber usar expresiones regulares?
Lo bueno de las expresiones regulares en cualquier lenguaje de programación es que no es obligatorio usarlas, incluso hay algunos que pueden llegar a amarlas y otros a odiarlas. Lo verdaderamente importante es que una vez que las sabes, puedes abarcar problemas de búsquedas en textos; como cuando se está haciendo scraping o buscando una URL en Django, que regularmente se hacen en muchos pasos, hacerlo en una sola línea. Es como decirle a la computadora “toma, esta es mi expresión, búscala”.
Todo muy bueno pero ¿Cómo se usan las expresiones regulares?
Describir detalladamente cada aspecto que puede ser usado por las expresiones regulares puede ser algo tedioso, así que debemos ir mirando paso a paso cómo podemos sacarle el mejor provecho a esta gran herramienta.
Primero que todo, en lo que nos ayudan las expresiones regulares es a encontrar patrones o concordancias en un texto dado, según las reglas que nosotros le impongamos, por ejemplo, si en el string “esto es una prueba” queremos buscar directamente la palabra “prueba”, la expresión regular buscará directamente una concordancia exacta con esa palabra; esto se llama patrón simple, pero lo realmente valioso es que no solo podemos buscar esas concordancias exactas, sino patrones que puedan ser semejantes a lo que estamos buscando, con unas simples reglas a las cuales llamaremos Metacaracteres como los que vemos a continuación:
. Representa a cualquier carácter
^ Representa el comienzo de la línea
$ Representa el final de la línea
* Se repite un carácter cero o más veces
+ Se repite un carácter una o más veces
? Se repite un carácter cero o una vez
\ Escape o de secuencias especiales
| Se verifica uno de los dos caracteres dados (or)
( ) Agrupación para un patrón
[ ] Especifican clases de caracteres
{ } Rangos para un patrón
Estos son los principales metacaracteres que hay en las expresiones regulares, así que comencemos desde los más comunes e importantes y a medida de que nos vayamos adentrando más en el tema irán apareciendo los demás usos para ellos.
Como podemos ver los símbolos [ y ] son llamados para especificar las clases de caracteres, pero ¿qué son las clases de caracteres? Son un conjunto de caracteres que dentro de los símbolos queremos que sean encontrados, estos pueden ser puntualmente un carácter o un rango de ellos y pueden ser alfanuméricos, por ejemplo [aeiou] le indica a la expresión regular que busque en el string dado cualquiera de las vocales y por otro lado [0-9] le indica que busque en el string un número en el rango de 0 a 9.
Una cuestión importante en estas clases de caracteres, es que los metacaracteres cambian su comportamiento normal dentro de ellas, por ejemplo el símbolo ^ dentro de una clase de caracteres ya no busca el inicio de una línea, sino que niega el carácter dentro de esta misma, así [^r] buscará cualquier carácter alfanumérico menos la r. En cambio si usamos el símbolo $ dentro de una clase, lo interpretará como simplemente el símbolo $ en un string y no como final de la línea.
Ahora veamos las secuencias especiales, que son un conjunto de caracteres y reglas ya predefinidas en las expresiones regulares con las cuales nos ahorraremos algo de trabajo y nos hará más fácil buscar algún patrón:
\d Encuentra cualquier número, igual que [0-9]
\D Encuentra cualquier carácter que NO sea número, igual que [^0-9]
\s Encuentra cualquier espacio en blanco, igual que [ \t\n\r\f\v]
\S Encuentra cualquier carácter que NO sea espacio en blanco, igual que [^ \t\n\r\f\v]
\w Encuentra cualquier carácter alfanumérico, igual que [a-zA-Z0-9_]
\W Encuentra cualquier carácter NO alfanumérico, igual que [^a-zA-Z0-9_]
Estas secuencias especiales también pueden ser usadas dentro de las clases de caracteres, haciendo incluso más profundas nuestras búsquedas de patrones, por ejemplo **[\S,] **nos ayudará a encontrar cadenas en donde no haya espacios en blanco y tengan comas (como los archivos csv). El símbolo \ también nos sirve para colocar caracteres de escape, es decir algún símbolo de metacaracter que queramos usar para su búsqueda como string, por ejemplo [ nos dejaría hacer la búsqueda del caracter “[“ en un texto dado.
Para finalizar con los Metacaracteres de patrones simples tenemos el comodín por defecto, el símbolo . el cual nos indica que encontrará cualquier carácter alfanumérico y en cualquier cantidad excepto una nueva línea (\n) con lo cual podremos armar infinidad de patrones como son mama, manzana, mañana, etc. Usando “m.a”.
Ahora bien, lo anteriormente dicho es lo básico para poder encontrar patrones simples, pero tenemos muchas más posibilidades, como la repetición de caracteres. Los metacaracteres que nos permiten hacer iteraciones son *, + y ? siendo cada uno de ellos especial por su comportamiento. Empecemos con *, el cual nos permite buscar cero o más veces un carácter, como por ejemplo “car*o” sería compatible con cao, caro, carro, carrro, etc.
Hay que tener algo muy pendiente cuando usamos los metacaracteres de repetición, ya que estos por defecto hacen una búsqueda greedy (codiciosa, en inglés) y si encuentra alguna concordancia nos traerá la concordancia más grande posible (como carrro en el ejemplo anterior). Nuestro segundo metacarácter de repetición es + el cual nos encuentra una o más repeticiones del carácter dado, como por ejemplo “l+evo” sería concordado por levo, llevo, lllevo, etc. El siguiente metacarácter de repetición es el símbolo ? el cual nos encuentra cero o una repetición del carácter que queramos, por ejemplo “post-?apocalíptico” encuentra tanto la palabra postapocalíptico como post-apocalíptico. Por último tenemos unos símbolos muy especiales y peculiares en los metacaracteres de repetición, son { y } ya que con ellos podemos crear un rango de “mínimo número” de repeticiones y “máximo número” de repeticiones, veámoslo en un ejemplo muy claro: “a ={1,2} b” sería concordante tanto con a = b como a == b pues mínimo puede tener 1 repetición y máximo 2, siendo el número mínimo que podríamos colocar en el rango 0 y el máximo “vacío” (infinito o el número máximo de dígitos enteros que acepte nuestro SO). Anteriormente dije que este era peculiar, y es que si lo analizamos detalladamente nos podemos dar cuenta de lo siguiente:
* Se puede expresar también como {0,}
+ Se puede expresar también como {1,}
? Se puede expresar también como {0,1}
Interesante ver cómo podemos tener distintas maneras de usar nuestros metacaracteres para crear nuevos patrones, unos más complicados que otros, pero ya es nuestra elección cual usar.
Ya que comenzamos a hablar de complicaciones, sigamos ahondando más en nuestros metacaracteres para sacar mejores y más completos patrones de nuestros textos, y por eso hablaremos de los metacaracteres zero-width assertions los cuales no se detienen a analizar un carácter, no tienen valor como tal sino que tienen un comportamiento binario, o lo uno o lo otro. Veamos el metacarácter | el cual es usado como or lógico, ya que cuando lo usamos le estamos diciendo a nuestra expresión regular escoge uno de los dos, como por ejemplo “amo los perros|gatos” la cual encontrará concordancia tanto para amo a los perros, como para amo a los gatos. El siguiente metacarácter se trata de el símbolo ^ que ya hemos visto en las clases de caracteres, pero con un comportamiento diferente, ya que cuando está fuera de los [ ] nos encuentra el inicio de la línea, es decir “^concuerdo” nos serviría para un texto como “concuerdo en ese aspecto” y no para uno tipo “en ese aspecto, concuerdo”. Lo mismo que el anterior metacarácter, es el símbolo $ que nos indica el final de la línea.
Ahora entremos en otras secuencias especiales, que no hemos tratado al principio pues estas son más específicas que las anteriores:
\A Se comporta como ^ excepto cuando hay multilineas
\Z Encuentra el final de un string
\b Encuentra los límites de una secuencia alfanumérica (una palabra)
\B Encuentra los caracteres que NO son límites de una palabra
Estos anteriormente listados son un complemento a todos los metacaracteres que ya hemos visto, pero buscan cosas muy específicas, por ejemplo el uso de \b es bastante útil para cuando queremos buscar una palabra pero esta misma palabra puede ser el substring de otra, digamos que estamos buscando “\baero\b” y en el texto tenemos lo siguiente “un aeroplano no es una globo aerostático, ni una máquina aeroespacial” no nos resultaría nada en la búsqueda pues la palabra “aero” sin más no está.
Otra de las prácticas comunes y de mayor valor a la hora de usar las expresiones regulares para encontrar patrones en los textos es la de agrupación, pues esta parte los textos en varios pedazos y nos encuentra solo los patrones que necesitamos. Para hacer agrupaciones usamos los metacaracteres ( y ) que funcionan como en las expresiones matemáticas, agrupan en su interior la combinación de caracteres y metacaracteres que queramos, y fuera de ellos también podemos realizar operaciones. Por último hay que hacer una mención especial a otro zero-width assertion llamado lookahead assertion (aserción anticipada, en inglés) las cuales se pueden presentar de forma positiva o negativa con los símbolos (?=regex) y (?!regex) respectivamente. Para el primer patrón si es correcto, sigue con la expresión regular, sino hace un alto, y la forma negativa es lo contrario, si el patrón falla en la primera búsqueda, sigue adelante con la expresión.
Ahora sí, manos a la obra ¿No?
Para empezar a darle uso a todo este conocimiento que hemos estado recibiendo usaremos la librería re de Python que es la que se encarga de interpretar y analizar las expresiones regulares, así que comencemos con los ejemplos:
Primero que todo debemos importar la librería:
`import re`
Ahora para que el motor de expresiones regulares nos entienda los patrones que vamos a usar tenemos que hacer que lo compile, por ejemplo:
`patron = re.compile(“aei*”)`
El objeto compile() acepta otros flags que nos ayudarán a hacer más claras las búsquedas que queremos, y podemos usarlas así:
IGNORECASE, I Ignora el uso de mayúsculas
DOTALL, S Hace que el metacarácter . incluya las nuevas líneas (\n)
MULTILINE, M Afecta las búsquedas de ^ y $ para encontrar nuevas líneas (\n)
LOCALE, L Busca con los caracteres del lenguaje regional
VERBOSE, X Permite comentar y organizar mejor las expresiones regulares
UNICODE, U Cambia algunos escapes \ para que sean Unicode
Para el ejemplo anterior podríamos decir que ignore las mayúsculas usando una de estas banderas:
`patron = re.compile(“aei*”, re.I)`
La librería re también nos proporciona varios métodos en un objeto compilado muy útiles para poder hacer búsquedas y concordancias con los textos dados, las principales son:
search() Busca en el texto alguna concordancia con la expresión
findall() Encuentra todas las concordancias y las devuelve en una lista
finditer() Encuentra todas las concordancias y las devuelve como un iterador
match() Encuentra si hay o no alguna concordancia
Unos ejemplos de cómo podríamos usarlas serían los siguientes:
import re
patron = re.compile(“[0-9]+”)
print patron.match(“cadena”)
#nos imprimirá None porque no hay números
print patron.findall(“Hola, tengo 26 años y en Platzi llevo 3”)
#imprime [“26”, “3”] que son los números en el texto
string = “De las 1000 opciones que había, solo 1 era válida”
if re.search(patron, string): #busca el patrón en el string
print string #si existe alguna concordancia, imprime el string
iter = patron.finditer(“La niña contaba 1, 2, 3, 4, 5 y seguía”)
for i in iter: #iteramos sobre cada concordancia
print i.span() #imprime una tupla con inicio y fin del patrón
Cabe agregar que cada vez que alguno de estos métodos encuentra una concordancia crea un objeto match que es en sí el resultado de la concordancia, y este objeto a su vez tiene varios métodos también muy útiles, uno de ellos es el que vemos en la última línea del código de ejemplo:
span() Devuelve una tupla con (inicio, fin) de la concordancia
group() Devuelve la concordancia encontrada
start() Devuelve la posición inicial de la concordancia
end() Devuelve la posición final de la concordancia
Un aspecto importante que hay que tener en cuenta a la hora de crear nuestro objeto compilado es que Python puede tomar el string que le estamos pasando como patrón directamente como cadena de caracteres, para evitar esto siempre es recomendable utilizar antes de nuestro patrón una r la cual le dice al motor de expresiones regulares que lo que sigue es un string raw, en otras palabras, le decimos a Python que acepte la cadena que le estamos enviando y la tome tal cual, por ejemplo r”xy*” -> “xy*” o r”\w+\d+\dir” -> “\w+\d+\dir” o como lo vimos en la teoría a la hora de usar los límites de palabras \b es muy útil, pues si no usamos un string raw, \b será tomado por Python como el backspace (retorno de carro), veamos:
import re
raw = re.compile(r“\baero\b”)
print raw.search(“buscamos la palabra aero”)
#imprime el objeto match pues encuentra el patrón
print raw.search(“un cohete aeroespacial”) #imprime None
raw = re.compile(“\baero\b”)
print raw.search(“no existe la palabra aero”)
#nos imprime None, pues \b es tomado como backspace
A la hora de hacer búsquedas en textos, muchas veces solo queremos sacar una porción del mismo, buscar algo en concreto con algunas expresiones regulares un poco más elaboradas. Esto se nos hace posible en Python gracias al agrupamiento y a una manera especial que Python tomó de Perl que son las Extensiones, con las cuales al colocarle antes de una agrupación una P a la expresión regular, Python lo interpreta como un grupo con nombre, que es lo que usa Django también para poder colocar variables dentro de sus URL, veamos algunos ejemplos:
import re
grupo = re.compile(r”(?P<regex>\b\w+\b)”)
print grupo.search(“grupo saldrá en la búsqueda”).group(“regex”)
#imprime “grupo” pues es la primera palabra que encuentra
repetido = re.compile(r”(?P<rep>\b\w+)\s+(?P=rep)”)
print repetido.search(“buscamos una palabra vieja vieja”).group()
#nos imprime “vieja vieja” pues es la palabra que se repite
Para finalizar solo me queda agregar que Python nos da las herramientas para poder usar las expresiones regulares, pero que esto funcione como queremos depende completamente de nuestro razonamiento para crear la expresión correcta y que nos satisfaga las condiciones para los patrones que estamos buscando, lo cual con este artículo se puede lograr hacer tomándose el tiempo para detallar los ejemplos y la teoría, además de aumentar nuestro conocimiento con el curso de Python/Django que está en Platzi.