Raul E. Lopez Briega

Matemáticas, análisis de datos y python

Expresiones Regulares con Python

Expresiones regulares

Introducción

Uno de los problemas más comunes con que nos solemos encontrar al desarrollar cualquier programa informático, es el de procesamiento de texto. Esta tarea puede resultar bastante trivial para el cerebro humano, ya que nosotros podemos detectar con facilidad que es un número y que una letra, o cuales son palabras que cumplen con un determinado patrón y cuales no; pero estas mismas tareas no son tan fáciles para una computadora. Es por esto, que el procesamiento de texto siempre ha sido uno de los temas más relevantes en las ciencias de la computación. Luego de varias décadas de investigación se logró desarrollar un poderoso y versátil lenguaje que cualquier computadora puede utilizar para reconocer patrones de texto; este lenguale es lo que hoy en día se conoce con el nombre de expresiones regulares; las operaciones de validación, búsqueda, extracción y sustitución de texto ahora son tareas mucho más sencillas para las computadoras gracias a las expresiones regulares.

¿Qué son las Expresiones Regulares?

Las expresiones regulares, a menudo llamada también regex, son unas secuencias de caracteres que forma un patrón de búsqueda, las cuales son formalizadas por medio de una sintaxis específica. Los patrones se interpretan como un conjunto de instrucciones, que luego se ejecutan sobre un texto de entrada para producir un subconjunto o una versión modificada del texto original. Las expresiones regulares pueden incluir patrones de coincidencia literal, de repetición, de composición, de ramificación, y otras sofisticadas reglas de reconocimiento de texto . Las expresiones regulares deberían formar parte del arsenal de cualquier buen programador ya que un gran número de problemas de procesamiento de texto pueden ser fácilmente resueltos con ellas.

Componentes de las Expresiones Regulares

Las expresiones regulares son un mini lenguaje en sí mismo, por lo que para poder utilizarlas eficientemente primero debemos entender los componentes de su sintaxis; ellos son:

  • Literales: Cualquier caracter se encuentra a sí mismo, a menos que se trate de un metacaracter con significado especial. Una serie de caracteres encuentra esa misma serie en el texto de entrada, por lo tanto la plantilla "raul" encontrará todas las apariciones de "raul" en el texto que procesamos.

  • Secuencias de escape: La sintaxis de las expresiones regulares nos permite utilizar las secuencias de escape que ya conocemos de otros lenguajes de programación para esos casos especiales como ser finales de línea, tabs, barras diagonales, etc. Las principales secuencias de escape que podemos encontrar, son:

Secuencia de escape Significado
\n Nueva línea (new line). El cursor pasa a la primera posición de la línea siguiente.
\t Tabulador. El cursor pasa a la siguiente posición de tabulación.
\\ Barra diagonal inversa
\v Tabulación vertical.
\ooo Carácter ASCII en notación octal.
\xhh Carácter ASCII en notación hexadecimal.
\xhhhh Carácter Unicode en notación hexadecimal.
  • Clases de caracteres: Se pueden especificar clases de caracteres encerrando una lista de caracteres entre corchetes [], la que que encontrará uno cualquiera de los caracteres de la lista. Si el primer símbolo después del "[" es "^", la clase encuentra cualquier caracter que no está en la lista.

  • Metacaracteres: Los metacaracteres son caracteres especiales que son la esencia de las expresiones regulares. Como son sumamente importantes para entender la sintaxis de las expresiones regulares y existen diferentes tipos, voy a dedicar una sección a explicarlos un poco más en detalle.

Metacaracteres

Metacaracteres - delimitadores

Esta clase de metacaracteres nos permite delimitar dónde queremos buscar los patrones de búsqueda. Ellos son:

Metacaracter Descripción
^ inicio de línea.
$ fin de línea.
\A inicio de texto.
\Z fin de texto.
. cualquier caracter en la línea.
\b encuentra límite de palabra.
\B encuentra distinto a límite de palabra.

Metacaracteres - clases predefinidas

Estas son clases predefinidas que nos facilitan la utilización de las expresiones regulares. Ellos son:

Metacaracter Descripción
\w un caracter alfanumérico (incluye "_").
\W un caracter no alfanumérico.
\d un caracter numérico.
\D un caracter no numérico.
\s cualquier espacio (lo mismo que [ \t\n\r\f]).
\S un no espacio.

Metacaracteres - iteradores

Cualquier elemento de una expresion regular puede ser seguido por otro tipo de metacaracteres, los iteradores. Usando estos metacaracteres se puede especificar el número de ocurrencias del caracter previo, de un metacaracter o de una subexpresión. Ellos son:

Metacaracter Descripción
* cero o más, similar a {0,}.
+ una o más, similar a {1,}.
? cero o una, similar a {0,1}.
{n} exactamente n veces.
{n,} por lo menos n veces.
{n,m} por lo menos n pero no más de m veces.
*? cero o más, similar a {0,}?.
+? una o más, similar a {1,}?.
?? cero o una, similar a {0,1}?.
{n}? exactamente n veces.
{n,}? por lo menos n veces.
{n,m}? por lo menos n pero no más de m veces.

En estos metacaracteres, los dígitos entre llaves de la forma {n,m}, especifican el mínimo número de ocurrencias en n y el máximo en m.

Metacaracteres - alternativas

Se puede especificar una serie de alternativas para una plantilla usando "|" para separarlas, entonces do|re|mi encontrará cualquier "do", "re", o "mi" en el texto de entrada.Las alternativas son evaluadas de izquierda a derecha, por lo tanto la primera alternativa que coincide plenamente con la expresión analizada es la que se selecciona. Por ejemplo: si se buscan foo|foot en "barefoot'', sólo la parte "foo" da resultado positivo, porque es la primera alternativa probada, y porque tiene éxito en la búsqueda de la cadena analizada.

Ejemplo:

foo(bar|foo) --> encuentra las cadenas 'foobar' o 'foofoo'.

Metacaracteres - subexpresiones

La construcción ( ... ) también puede ser empleada para definir subexpresiones de expresiones regulares.

Ejemplos:

(foobar){10} --> encuentra cadenas que contienen 8, 9 o 10 instancias de 'foobar'

foob([0-9]|a+)r --> encuentra 'foob0r', 'foob1r' , 'foobar', 'foobaar', 'foobaar' etc.

Metacaracteres - memorias (backreferences)

Los metacaracteres \1 a \9 son interpretados como memorias. \ encuentra la subexpresión previamente encontrada #.

Ejemplos:

(.)\1+ --> encuentra 'aaaa' y 'cc'.

(.+)\1+ --> también encuentra 'abab' y '123123'

(['"]?)(\d+)\1 --> encuentra '"13" (entre comillas dobles), o '4' (entre comillas simples) o 77 (sin comillas) etc.

Expresiones Regulares con Python

Luego de esta introducción, llegó el tiempo de empezar a jugar con las expresiones regulares y Python.

Como no podría ser de otra forma tratandose de Python y su filosofía de todas las baterías incluídas; en la librería estandar de Python podemos encontrar el módulo re, el cual nos proporciona todas las operaciones necesarias para trabajar con las expresiones regulares.

Por lo tanto, en primer lugar lo que debemos hacer es importar el modulo re.

In [1]:
# importando el modulo de regex de python
import re  

Buscando coincidencias

Una vez que hemos importado el módulo, podemos empezar a tratar de buscar coincidencias con un determinado patrón de búsqueda. Para hacer esto, primero debemos compilar nuestra expresion regular en un objeto de patrones de Python, el cual posee métodos para diversas operaciones, tales como la búsqueda de coincidencias de patrones o realizar sustituciones de texto.

In [2]:
# compilando la regex
patron = re.compile(r'\bfoo\b')  # busca la palabra foo

Ahora que ya tenemos el objeto de expresión regular compilado podemos utilizar alguno de los siguientes métodos para buscar coincidencias con nuestro texto.

  • match(): El cual determinada si la regex tiene coincidencias en el comienzo del texto.
  • search(): El cual escanea todo el texto buscando cualquier ubicación donde haya una coincidencia.
  • findall(): El cual encuentra todos los subtextos donde haya una coincidencia y nos devuelve estas coincidencias como una lista.
  • finditer(): El cual es similar al anterior pero en lugar de devolvernos una lista nos devuelve un iterador.

Veamoslos en acción.

In [3]:
# texto de entrada
texto = """ bar foo bar
foo barbarfoo
foofoo foo bar
"""
In [4]:
# match nos devuelve None porque no hubo coincidencia al comienzo del texto
print(patron.match(texto))
None
In [5]:
# match encuentra una coindencia en el comienzo del texto
m = patron.match('foo bar')
m
Out[5]:
<_sre.SRE_Match object; span=(0, 3), match='foo'>
In [6]:
# search nos devuelve la coincidencia en cualquier ubicacion.
s = patron.search(texto)
s
Out[6]:
<_sre.SRE_Match object; span=(5, 8), match='foo'>
In [7]:
# findall nos devuelve una lista con todas las coincidencias
fa = patron.findall(texto)
fa
Out[7]:
['foo', 'foo', 'foo']
In [8]:
# finditer nos devuelve un iterador
fi = patron.finditer(texto)
fi
Out[8]:
<callable_iterator at 0x7f413db74240>
In [9]:
# iterando por las distintas coincidencias
next(fi)
Out[9]:
<_sre.SRE_Match object; span=(5, 8), match='foo'>
In [10]:
next(fi)
Out[10]:
<_sre.SRE_Match object; span=(13, 16), match='foo'>

Como podemos ver en estos ejemplos, cuando hay coincidencias, Python nos devuelve un Objeto de coincidencia (salvo por el método findall() que devuelve una lista). Este Objeto de coincidencia también tiene sus propios métodos que nos proporcionan información adicional sobre la coincidencia; éstos métodos son:

  • group(): El cual devuelve el texto que coincide con la expresion regular.
  • start(): El cual devuelve la posición inicial de la coincidencia.
  • end(): El cual devuelve la posición final de la coincidencia.
  • span(): El cual devuelve una tupla con la posición inicial y final de la coincidencia.
In [11]:
# Métodos del objeto de coincidencia
m.group(), m.start(), m.end(), m.span()
Out[11]:
('foo', 0, 3, (0, 3))
In [12]:
s.group(), s.start(), s.end(), s.span()
Out[12]:
('foo', 5, 8, (5, 8))

Modificando el texto de entrada

Además de buscar coincidencias de nuestro patrón de búsqueda en un texto, podemos utilizar ese mismo patrón para realizar modificaciones al texto de entrada. Para estos casos podemos utilizar los siguientes métodos:

  • split(): El cual divide el texto en una lista, realizando las divisiones del texto en cada lugar donde se cumple con la expresion regular.
  • sub(): El cual encuentra todos los subtextos donde existe una coincidencia con la expresion regular y luego los reemplaza con un nuevo texto.
  • subn(): El cual es similar al anterior pero además de devolver el nuevo texto, también devuelve el numero de reemplazos que realizó.

Veamoslos en acción.

In [13]:
# texto de entrada
becquer = """Podrá nublarse el sol eternamente; 
Podrá secarse en un instante el mar; 
Podrá romperse el eje de la tierra 
como un débil cristal. 
¡todo sucederá! Podrá la muerte 
cubrirme con su fúnebre crespón; 
Pero jamás en mí podrá apagarse 
la llama de tu amor."""
In [14]:
# patron para dividir donde no encuentre un caracter alfanumerico
patron = re.compile(r'\W+')
In [15]:
palabras = patron.split(becquer)
palabras[:10]  # 10 primeras palabras
Out[15]:
['Podrá',
 'nublarse',
 'el',
 'sol',
 'eternamente',
 'Podrá',
 'secarse',
 'en',
 'un',
 'instante']
In [16]:
# Utilizando la version no compilada de split.
re.split(r'\n', becquer)  # Dividiendo por linea.
Out[16]:
['Podrá nublarse el sol eternamente; ',
 'Podrá secarse en un instante el mar; ',
 'Podrá romperse el eje de la tierra ',
 'como un débil cristal. ',
 '¡todo sucederá! Podrá la muerte ',
 'cubrirme con su fúnebre crespón; ',
 'Pero jamás en mí podrá apagarse ',
 'la llama de tu amor.']
In [17]:
# Utilizando el tope de divisiones
patron.split(becquer, 5)
Out[17]:
['Podrá',
 'nublarse',
 'el',
 'sol',
 'eternamente',
 'Podrá secarse en un instante el mar; \nPodrá romperse el eje de la tierra \ncomo un débil cristal. \n¡todo sucederá! Podrá la muerte \ncubrirme con su fúnebre crespón; \nPero jamás en mí podrá apagarse \nla llama de tu amor.']
In [18]:
# Cambiando "Podrá" o "podra" por "Puede"
podra = re.compile(r'\b(P|p)odrá\b')
puede = podra.sub("Puede", becquer)
print(puede)
Puede nublarse el sol eternamente; 
Puede secarse en un instante el mar; 
Puede romperse el eje de la tierra 
como un débil cristal. 
¡todo sucederá! Puede la muerte 
cubrirme con su fúnebre crespón; 
Pero jamás en mí Puede apagarse 
la llama de tu amor.
In [19]:
# Limitando el número de reemplazos
puede = podra.sub("Puede", becquer, 2)
print(puede)
Puede nublarse el sol eternamente; 
Puede secarse en un instante el mar; 
Podrá romperse el eje de la tierra 
como un débil cristal. 
¡todo sucederá! Podrá la muerte 
cubrirme con su fúnebre crespón; 
Pero jamás en mí podrá apagarse 
la llama de tu amor.
In [20]:
# Utilizando la version no compilada de subn
re.subn(r'\b(P|p)odrá\b', "Puede", becquer)  # se realizaron 5 reemplazos
Out[20]:
('Puede nublarse el sol eternamente; \nPuede secarse en un instante el mar; \nPuede romperse el eje de la tierra \ncomo un débil cristal. \n¡todo sucederá! Puede la muerte \ncubrirme con su fúnebre crespón; \nPero jamás en mí Puede apagarse \nla llama de tu amor.',
 5)

Funciones no compiladas

En estos últimos ejemplos, pudimos ver casos donde utilizamos las funciones al nivel del módulo split() y subn(). Para cada uno de los ejemplos que vimos (match, search, findall, finditer, split, sub y subn) existe una versión al nivel del módulo que se puede utilizar sin necesidad de compilar primero el patrón de búsqueda; simplemente le pasamos como primer argumento la expresion regular y el resultado será el mismo. La ventaja que tiene la versión compila sobre las funciones no compiladas es que si vamos a utilizar la expresion regular dentro de un bucle nos vamos a ahorrar varias llamadas de funciones y por lo tanto mejorar la performance de nuestro programa.

In [21]:
# Ejemplo de findall con la funcion a nivel del modulo
# findall nos devuelve una lista con todas las coincidencias
re.findall(r'\bfoo\b', texto)
Out[21]:
['foo', 'foo', 'foo']

Banderas de compilación

Las banderas de compilación permiten modificar algunos aspectos de cómo funcionan las expresiones regulares. Todas ellas están disponibles en el módulo re bajo dos nombres, un nombre largo como IGNORECASE y una forma abreviada de una sola letra como I. Múltiples banderas pueden ser especificadas utilizando el operador "|" OR; Por ejemplo, re.I | RE.M establece las banderas de E y M.

Algunas de las banderas de compilación que podemos encontrar son:

  • IGNORECASE, I: Para realizar búsquedas sin tener en cuenta las minúsculas o mayúsculas.
  • VERBOSE, X: Que habilita la modo verborrágico, el cual permite organizar el patrón de búsqueda de una forma que sea más sencilla de entender y leer.
  • ASCII, A: Que hace que las secuencias de escape \w, \b, \s and \d funciones para coincidencias con los caracteres ASCII.
  • DOTALL, S: La cual hace que el metacaracter . funcione para cualquier caracter, incluyendo el las líneas nuevas.
  • LOCALE, L: Esta opción hace que \w, \W, \b, \B, \s, y \S dependientes de la localización actual.
  • MULTILINE, M: Que habilita la coincidencia en múltiples líneas, afectando el funcionamiento de los metacaracteres ^ and $.
In [22]:
# Ejemplo de IGNORECASE
# Cambiando "Podrá" o "podra" por "Puede"
podra = re.compile(r'podrá\b', re.I)  # el patrón se vuelve más sencillo
puede = podra.sub("puede", becquer)
print(puede)
puede nublarse el sol eternamente; 
puede secarse en un instante el mar; 
puede romperse el eje de la tierra 
como un débil cristal. 
¡todo sucederá! puede la muerte 
cubrirme con su fúnebre crespón; 
Pero jamás en mí puede apagarse 
la llama de tu amor.
In [23]:
# Ejemplo de VERBOSE
mail = re.compile(r"""
\b             # comienzo de delimitador de palabra
[\w.%+-]       # usuario: Cualquier caracter alfanumerico mas los signos (.%+-)
+@             # seguido de @
[\w.-]         # dominio: Cualquier caracter alfanumerico mas los signos (.-)
+\.            # seguido de .
[a-zA-Z]{2,6}  # dominio de alto nivel: 2 a 6 letras en minúsculas o mayúsculas.
\b             # fin de delimitador de palabra
""", re.X)
In [24]:
mails = """raul.lopez@relopezbriega.com, Raul Lopez Briega,
foo bar, relopezbriega@relopezbriega.com.ar, raul@github.io, 
https://relopezbriega.com.ar, https://relopezbriega.github.io, 
python@python, river@riverplate.com.ar, pythonAR@python.pythonAR
"""
In [25]:
# filtrando los mails con estructura válida
mail.findall(mails)
Out[25]:
['raul.lopez@relopezbriega.com',
 'relopezbriega@relopezbriega.com.ar',
 'raul@github.io',
 'river@riverplate.com.ar']

Como podemos ver en este último ejemplo, la opción VERBOSE puede ser muy util para que cualquier persona que lea nuestra expresion regular pueda entenderla más fácilmente.

Nombrando los grupos

Otra de las funciones interesantes que nos ofrece el módulo re de Python; es la posibilidad de ponerle nombres a los grupos de nuestras expresiones regulares. Así por ejemplo, en lugar de acceder a los grupos por sus índices, como en este caso...

In [26]:
# Accediendo a los grupos por sus indices
patron = re.compile(r"(\w+) (\w+)")
s = patron.search("Raul Lopez")
In [27]:
# grupo 1
s.group(1)
Out[27]:
'Raul'
In [28]:
# grupo 2
s.group(2)
Out[28]:
'Lopez'

Podemos utilizar la sintaxis especial (?P<nombre>patron) que nos ofrece Python para nombrar estos grupos y que sea más fácil identificarlos.

In [29]:
# Accediendo a los grupos por nombres
patron = re.compile(r"(?P<nombre>\w+) (?P<apellido>\w+)")
s = patron.search("Raul Lopez")
In [30]:
# grupo nombre
s.group("nombre")
Out[30]:
'Raul'
In [31]:
# grupo apellido
s.group("apellido")
Out[31]:
'Lopez'

Otros ejemplos de expresiones regulares

Por último, para ir cerrando esta introducción a las expresiones regulares, les dejo algunos ejemplos de las expresiones regulares más utilizadas.

Validando mails

Para validar que un mail tenga la estructura correcta, podemos utilizar la siguiente expresion regular:

regex: \b[\w.%+-]+@[\w.-]+\.[a-zA-Z]{2,6}\b

Este es el patrón que utilizamos en el ejemplo de la opción VERBOSE.

Validando una URL

Para validar que una URL tenga una estructura correcta, podemos utilizar esta expresion regular:

regex: ^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$

In [32]:
# Validando una URL
url = re.compile(r"^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$")

# vemos que https://relopezbriega.com.ar lo acepta como una url válida.
url.search("https://relopezbriega.com.ar")
Out[32]:
<_sre.SRE_Match object; span=(0, 27), match='https://relopezbriega.com.ar'>
In [33]:
# pero https://google.com/un/archivo!.html no la acepta por el carcter !
print(url.search("https://google.com/un/archivo!.html"))
None

Validando una dirección IP

Para validar que una dirección IP tenga una estructura correcta, podemos utilizar esta expresión regular:

regex: ^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$

In [34]:
# Validando una dirección IP
patron = ('^(?:(?:25[0-5]|2[0-4][0-9]|'
          '[01]?[0-9][0-9]?)\.){3}'
          '(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$')

ip = re.compile(patron)

# la ip 73.60.124.136 es valida
ip.search("73.60.124.136")
Out[34]:
<_sre.SRE_Match object; span=(0, 13), match='73.60.124.136'>
In [35]:
# pero la ip 256.60.124.136 no es valida
print(ip.search("256.60.124.136"))
None

Validando una fecha

Para validar que una fecha tenga una estructura dd/mm/yyyy, podemos utilizar esta expresión regular:

regex: ^(0?[1-9]|[12][0-9]|3[01])/(0?[1-9]|1[012])/((19|20)\d\d)$

In [36]:
# Validando una fecha
fecha = re.compile(r'^(0?[1-9]|[12][0-9]|3[01])/(0?[1-9]|1[012])/((19|20)\d\d)$')

# validando 13/02/1982
fecha.search("13/02/1982")
Out[36]:
<_sre.SRE_Match object; span=(0, 10), match='13/02/1982'>
In [37]:
# no valida 13-02-1982
print(fecha.search("13-02-1982"))
None
In [38]:
# no valida 32/12/2015
print(fecha.search("32/12/2015"))
None
In [39]:
# no valida 30/14/2015
print(fecha.search("30/14/2015"))
None

Y con estos ejemplos termino este tutorial, espero que les haya sido de utilidad.

Saludos!

Este post fue escrito utilizando IPython notebook. Pueden descargar este notebook o ver su version estática en nbviewer.

Este articulo fue escrito utilizando Jupyter notebook. Presionar aquí para la versión interactiva: Binder badge

Comments