Saltar a contenido

Expresiones regulares

Banner

Imagen generada con Inteligencia Artificial

Paradoja

Si tienes un problema y lo intentas resolver con expresiones regulares, entonces tienes dos problemas.

El módulo re permite trabajar con expresiones regulares.

Expresión regular

Una expresión regular (también conocida como «regex» o «regexp» por su contracción anglosajona «reg-ular exp-ression») es una cadena de texto que conforma un patrón de búsqueda. Se utiliza principalmente para la búsqueda de patrones en cadenas de caracteres u operaciones de sustitución.

Se trata de una herramienta ampliamente utilizada en las ciencias de la computación y necesaria para multitud de aplicaciones que traten con información textual.

Pero... ¿qué pinta tiene una expresión regular? Veamos un primer ejemplo de expresión regular:

>>> regex = '^\d{8}[A-Z]$'

La expresión regular anterior nos permite comprobar que una cadena de texto dada es un DNI válido. Si analizamos parte por parte tendríamos lo siguiente:

  • ^ comienzo de línea.
  • \d{8} 8 dígitos.
  • [A-Z] letra en mayúsculas.
  • $ final de línea.

Sintaxis

Las expresiones regulares pueden contener tanto caracteres especiales como caracteres ordinarios. La mayoría de los caracteres ordinarios, como 'A', 'b' o '0' son las expresiones regulares más sencillas; simplemente se ajustan a sí mismas.

Existen una serie de caracteres que tienen un significado especial dentro de una expresión regular:

Caracter Descripción Ejemplo
. Coincide con cualquier carácter excepto con una nueva línea. 'a.b' acb, a b, aab, abb, ... bxa, a\nb, ab, abc, ...
^ Coincide con el comienzo de la línea o cadena '^ab' ab, abc, abab, abcd, ... ba, aa, bb, axb, ...
$ Coincide con el final de la línea o cadena. 'ab$' ab, aab, bab, ab ... b, bb, ba, aa, ...
* Coincide con 0 o más repeticiones de la expresión regular precedente. 'a*b' b, ab, aab, aaab, ... ba, a, aa, acb, ...
+ Coincide con 1 o más repeticiones de la expresión regular precedente. 'a+b' ab, aab, aaab, ... b, cb, ba, bb, ...
? Coincide con 0 o 1 repetición de la expresión regular precedente. 'a?b' b, ab aab, ba, aaab, cb, ...
{m} Coincide con exactamente m repeticiones de la expresión regular precedente. 'a{3}' aaa a, aa, aaaa, ...
{m,n} Coincide de m a n repeticiones de la expresión regular precedente, tratando de coincidir con el mayor número de repeticiones posibles. 'a{2,4}' aa, aaa, aaaa a, aaaaa, ...
{m,} Coincide como mínimo con m repeticiones de la expresión regular precedente. 'a{2,}' aa, aaa, aaaa, ... '', a, ...
{,n} Coincide como máximo con n repeticiones de la expresión regular precedente. 'a{,2}' '', a, aa, aaa, aaaa, ...
[] Coincide con el conjunto de caracteres indicados dentro de los corchetes. '[abc]' a, b, c aa, d, ab, ...
[^] Coincide con cualquier caracter fuera de los caracteres indicados dentro de los corchetes. '[^abc]' d, e, f, ... a, b, c
[m-n] Coincide con el conjunto de caracteres indicados dentro de los corchetes. '[a-d]' a, b, c, d aa, f, ab, ...
[^m-n] Coincide con cualquier caracter fuera de los caracteres indicados dentro de los corchetes. '[^a-d]' e, f, g, ... a, b, c, d
| Coincide con una expresión regular u otra, separadas por este símbolo. 'a+|b+' a, aa, b, bb, ... ab, aabb, abab, ...
() Coincide con cualquier expresión regular que esté dentro de los paréntesis, e indica el comienzo y el final de un grupo de captura; el contenido de un grupo puede ser recuperado después de que se haya realizado una coincidencia. '(ab)' ab a, b, abc, ...
(?P<name>) Coincide con cualquier expresión regular que esté dentro de los paréntesis; el contenido del grupo de captura es accesible por name. '(?P<test>ab)' ab a, b, abc, ...
(?:) Coincide con cualquier expresión regular que esté dentro de los paréntesis pero no crea un grupo de captura. '(?:ab)' ab a, b, abc, ...
\number Coincide con el contenido del grupo de captura del mismo número. Se usa en conjunción con (). r'(.):\1' a:a, b:b, c:c, ... a:b, b:c, c:d, ...
(?P=name) Coincide con el contenido del grupo de captura del mismo nombre. Se usa en conjunción con (). '(?P<c1>.):(?P=c1)' a:a, b:b, c:c, ... a:b, b:c, c:d, ...
\b Coincide con el comienzo o el final de una palabra. r'\ba\b' a;b, a b, a%b, ... ab, ba, aa, ...
\B Coincide con cualquier caracter que no sea comienzo o final de una palabra. r'a\Bb' ab a b, a;b, a!b, ...
\d Coincide con cualquier dígito decimal. Equivalente a [0-9]. r'a\db' a0b, a3b, a9b, ... ab, 1ab, ab2, ...
\D Coincide con cualquier carácter que no sea un dígito decimal. Equivalente a [^0-9]. r'a\Db' acb, aab, a;b, ... a0b, a3b, a9b, ...
\s Coincide con cualquier espacio en blanco. Equivalente a [ \t\n\r\f\v]. r'a\sb' a b, a\tb, a\nb, ... acb, abb, aab, ...
\S Coincide con cualquier carácter que no sea un espacio en blanco. Equivalente a [^ \t\n\r\f\v]. r'a\Sb' a.b, abb, aab, ... a b, a\tb, a\nb, ...
\w Coincide con cualquier carácter alfanumérico. Equivalente a [a-zA-Z0-9_]. r'a\wb' aab, aAb, acb, ... a;b, a!b, a.b, ...
\W Coincide con cualquier carácter que no sea un carácter alfanumérico. Equivalente a [^a-zA-Z0-9_]. r'a\Wb' a;b, a!b, a.b, ... aab, aAb, acb, ...
\ Permite «escapar» el caracter que le sigue, es decir, quitarle el significado especial que tiene. r'a\.b' a.b acb, aab, abb, ...

Cadenas en crudo

Cuando hay barras invertidas en la expresión regular (\d, \s, \w, \b, \1, ...) es recomendable el uso de cadenas en crudo o «raw strings» ya que de no hacerlo podríamos obtener errores del estilo: SyntaxWarning: invalid escape sequence '\d'.

En general, siempre que uses expresiones regulares en Python, lo mejor es usar r'' para evitar confusiones y errores. Por ejemplo r'\d+' en vez de '\d+'.

Ejercicio

Coge papel y lápiz e intenta escribir una expresión regular para los siguientes escenarios:

  1. Documento nacional de identidad en España.

    • Ten en cuenta que se descartan las letras I, Ñ, O, U
    • Ejemplos: 76548971F,45432197W
  2. Número de identidad de extranjero en España.

    • Ten en cuenta formatos válidos antiguos y modernos.
    • Ejemplos: X43517865A, Z6547387T
  3. Matrículas automovilísticas en España.

    • Ten en cuenta únicamente el formato de matrículas del sistema actual.
    • Ten en cuenta que las letras utilizadas son las consonantes B, C, D, F, G, H, J, K, L, M, N, P, R, S, T, V, W, X, Y, Z.
    • Puede aparecer uno o varios espacios en blanco entre los dígitos y las letras.
    • Ejemplos: 5144FTY, 2131 HBB
  4. Código de aeropuertos de IATA.

    • Ejemplos: TFN, JFK
  5. Prefijos telefónicos mundiales.

    • Ten en cuenta todos los posibles formatos existentes.
    • Ten en cuenta los prefijos especiales/reservardos.
    • Ejemplos: +1-441, +678, +882-16
  6. Tamaños de papel ISO-DIN.

    • Ten en cuenta las series A, B y C.
    • Ejemplos: A10, B5, C8.

Operaciones

Una vez encontrada la expresión regular correspondiente, Python nos ofrece distintos mecanismos que aplicar.

La búsqueda de patrones es una de las principales utilidades de las expresiones regulares.

Supongamos un ejemplo en el que queremos buscar un número de teléfono dentro de un texto. Para ello vamos a utilizar la función search():

>>> import re#(1)!

>>> text = 'Estaré disponible en el +34755142009 el lunes por la tarde'#(2)!

>>> regex = r'\+?\d{2}\d{9}'#(3)!

>>> m = re.search(regex, text)

>>> m#(4)!
<re.Match object; span=(24, 36), match='+34755142009'>

  1. Para poder trabajar con expresiones regular debemos importar el paquete re de la librería estándar.
  2. Texto de entrada.
  3. Definición de la expresión regular:
    • \+? Puede aparecer el signo + como prefijo del teléfono (lo escapamos ya que el punto . es un caracter especial en sí mismo).
    • \d{2} Dos repeticiones de un dígito (prefijo).
    • \d{9} Nueve repeticiones de un dígito (número telefónico en sí mismo).
  4. La función search() nos devuelve un objeto tipo Match donde span indica la «ventana» de coincidencia: text[24:36] '+34755142009'

Podemos acceder a la coincidencia encontrada de varias formas:

>>> m[0]#(1)!
'+34755142009'

>>> m.span(0)#(2)!
(24, 36)

    • El acceso por índice nos devuelve las coindicencias encontradas.
    • Equivalente a usar m.group(0).
    • El método span() nos devuelve una tupla con los índices de comienzo y finalización de la coincidencia.
    • Equivalente a usar m.start() y m.end().

Podemos aplicar grupos de captura para separar el prefijo y el teléfono siguiendo con el ejemplo anterior:

>>> import re

>>> text = 'Estaré disponible en el +34755142009 el lunes por la tarde'#(2)!

>>> regex = r'\+?(\d{2})(\d{9})'

>>> m = re.search(regex, text)

>>> m[0]
'+34755142009'

>>> m[1]
'34'

>>> m[2]
'755142009'
>>> import re

>>> text = 'Estaré disponible en el +34755142009 el lunes por la tarde'#(2)!

>>> regex = r'\+?(?P<prefix>\d{2})(?P<number>\d{9})'

>>> m = re.search(regex, text)

>>> m[0]
'+34755142009'

>>> m['prefix']
'34'

>>> m['number']
'755142009'

Ignorando mayúsculas y minúsculas

Si queremos ignorar mayúsculas y minúsculas a la hora de hacer una búsqueda, sólo tendremos que usar un tercer parámetro indicándolo:

import re

re.search(regex, text, re.IGNORECASE)#(1)!

  1. También se puede abreviar como re.I

Búsqueda múltiple

En el ejemplo anterior hemos estado buscando una única coincidencia. Imaginemos ahora que queremos encontrar todos los teléfonos. Para ello vamos a utilizar la función findall():

>>> import re

>>> text = """
... Datos de contacto:
...   - Marketing: Rubén López (+49677543181)
...   - Ventas: Sara Mondragón (+34681788902)
...   - Desarrollo: Eva Blasco (+51682131262)
... © Saturno Desarrollos de Software
... """

>>> regex = r'\+?\d{2}\d{9}'

>>> re.findall(regex, text)#(1)!
['+49677543181', '+34681788902', '+51682131262']

  1. La función findall() devuelve una lista con las coincidencias encontradas.

Es posible utilizar grupos de captura con la función findall(). Imaginemos que sólo nos interesan los prefijos telefónicos del ejemplo anterior:

>>> import re

>>> text = """
... Datos de contacto:
...   - Marketing: Rubén López (+49677543181)
...   - Ventas: Sara Mondragón (+34681788902)
...   - Desarrollo: Eva Blasco (+51682131262)
... © Saturno Desarrollos de Software
... """

>>> regex = r'\+?(\d{2})\d{9}'#(1)!

>>> re.findall(regex, text)
['49', '34', '51']

  1. Mediante los paréntesis () definimos el grupo de captura sobre el prefijo.

Separar

Otras de las operaciones ampliamente usadas con expresiones regulares es la separación o división de una cadena de texto mediante un separador.

En su momento vimos el uso de la función split() para cadenas de texto, pero era muy limitada para patrones avanzados. Veamos el uso de la función re.split() dentro de este módulo de expresiones regulares.

Un ejemplo muy sencillo sería separar la parte entera de la parte decimal en un determinado número flotante:

>>> regex = r'[.,]'

>>> re.split(regex, '3.14')
['3', '14']

>>> re.split(regex, '3,14')
['3', '14']

Python también nos da la posibilidad de «capturar» el separador. Siguiendo el ejemplo anterior:

>>> regex = r'([.,])'#(1)!

>>> re.split(regex, '3.14')
['3', '.', '14']

>>> re.split(regex, '3,14')
['3', ',', '14']

  1. Usamos paréntesis para añadir un grupo de captura.

Reemplazar

El paquete de expresiones regulares re también nos ofrece la posibilidad de reemplazar ocurrencias dentro de un texto. Para ello disponemos de la función sub (regla mnemotécnica viene del inglés «substitute»).

Veamos a continuación un ejemplo de uso en el que recibimos el nombre de una persona en formato <nombre> <apellidos> y queremos convertirlo a formato <apellidos>, <nombre>.

Veamos dos soluciones a este problema utilizando la función re.sub() mediante:

En este caso los grupos de captura se referencian por su posición con \1, \2, ...

>>> import re

>>> name = 'Alan Turing'

>>> regex = r'(\w+) +(\w+)'#(1)!
>>> repl = r'\2, \1'#(2)!

>>> re.sub(regex, repl, name)#(3)!
'Turing, Alan'

  1. Utilizamos grupos de captura posicionales para nombre y apellidos.
  2. Hacemos referencia a los grupos de captura en orden «inverso».
  3. La función re.sub() recibe la expresión de búsqueda, la expresión de reemplazo y la cadena de texto sobre la que operar.

En este caso los grupos de captura se referencian por su nombre con \g<name>, \g<surname>, ...

>>> import re

>>> name = 'Alan Turing'

>>> regex = r'(?P<name>\w+) +(?P<surname>\w+)'#(1)!
>>> repl = r'\g<surname>, \g<name>'#(2)!

>>> re.sub(regex, repl, name)#(3)!
'Turing, Alan'    

  1. Utilizamos grupos de captura nominales para nombre y apellidos.
  2. Hacemos referencia a los grupos de captura en orden «inverso».
  3. La función re.sub() recibe la expresión de búsqueda, la expresión de reemplazo y la cadena de texto sobre la que operar.

La función re.sub() admite un uso más avanzado ya que podemos pasar una función en vez de una cadena de texto de reemplazo, lo que nos abre un abanico de posibilidades.

Siguiendo con el ejemplo anterior, supongamos ahora que queremos hacer la misma transformación pero convirtiendo el apellido a mayúsculas, y asegurarnos de que el nombre queda como título:

>>> import re

>>> name = 'Alan Turing'

>>> regex = r'(\w+) +(\w+)'

>>> re.sub(regex, lambda m: f'{m[2].upper()}, {m[1].title()}', name)#(1)!
'TURING, Alan'

  1. La función «lambda» recibe el objeto «matcheado» y realiza su modificación mediante los grupos de captura.

Contando reemplazos

Existe una función re.subn() que devuelve una tupla con la nueva cadena de texto reemplazada y el número de sustituciones realizadas.

Casar

Si lo que estamos buscando es comprobar si una determinada cadena de texto «casa» (coincide) con un patrón de expresión regular, podemos hacer uso de la función re.fullmatch().

A continuación se presenta un primer ejemplo que comprueba si un texto dado es un DNI válido:

>>> import re

>>> regex = r'\d{8}[A-Z]'#(1)!

>>> text = '54632178Y'
>>> re.fullmatch(regex, text)#(2)!
<re.Match object; span=(0, 9), match='54632178Y'>

>>> text = '87896532$'#(3)!
>>> re.fullmatch(regex, text)

  1. Esta expresión regular es una «simplificación» ya que la letra del DNI (dígito de control) habría que calcularla utilizando un algoritmo definido.
  2. Cuando la cadena de texto «casa» con la expresión regular se devuelve un objeto de tipo Match.
  3. Cuando la cadena de texto no «casa» con la expresión regular se devuelve None.

En este tipo de escenarios es habitual utilizar el operador morsa para discernir los casos a la vez que creamos una variable:

>>> import re

>>> def check_id_card(text: str) -> None:
...     REGEX = r'(\d{8})([A-Z])'
...     if m := re.fullmatch(REGEX, text):#(1)!
...         print(f'{text} es un DNI válido')
...         print(f'N: {m[1]}  CC: {m[2]}')#(2)!
...     else:
...         print(f'{text} no es un DNI válido')

>>> check_id_card('54632178Y')
54632178Y es un DNI válido
N: 54632178  CC: Y

>>> check_id_card('87896532$')
87896532$ no es un DNI válido

  1. En la variable m tendremos el objeto Match en el caso de que la cadena de texto haya casado.
  2. Acceso a los grupos de captura.

Cuidado con re.match()

Hay una variante más «flexible» para casar que es re.match() y comprueba la existencia del patrón sólo desde el comienzo de la cadena. Es decir, que si el final de la cadena no coincide sigue casando.

Casa...

>>> regex = r'\d{8}[A-Z]'
>>> text = '54632178Y###'

>>> re.match(regex, text)
<re.Match object; span=(0, 9), match='54632178Y'>

No casa...

>>> regex = r'\d{8}[A-Z]'
>>> text = '###54632178Y'

>>> re.match(regex, text)

En cualquier caso podemos hacer que re.match() se comporte como re.fullmatch() si especificamos los indicadores de comienzo y final de línea en el patrón:

>>> regex = r'^\d{8}[A-Z]$'#(1)!
>>> text = '54632178Y'

>>> re.match(regex, text)
<re.Match object; span=(0, 9), match='54632178Y'>

    • ^ indica comienzo de línea.
    • $ indica final de línea.

Manejando expresiones largas

Hay ocasiones en las que debemos afrontar la elaboración de una expresión regular extensa que incluye varios componentes y puede resultar complicada de leer, o incluso de escribir.

Veamos por ejemplo una expresión regular para comprobar la fortaleza de una contraseña:

>>> import re

>>> regex = r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};:"\\|,.<>\/?]).{8,20}$'

>>> re.match(regex, '1234')
>>> re.match(regex, 'python-m0lA')
<re.Match object; span=(0, 11), match='python-m0lA'>

Obviamente resulta difícil de entender al estar escrito todo en una misma línea. Podemos mejorar esta situación desde tres enfoques:

Aquí utilizamos cadenas multilínea para construir la expresión regular:

>>> regex = r"""
...     ^                                            # inicio de cadena
...     (?=.*[a-z])                                  # al menos una letra minúscula
...     (?=.*[A-Z])                                  # al menos una letra mayúscula
...     (?=.*\d)                                     # al menos un dígito
...     (?=.*[!@#$%^&*()_+\-=\[\]{};:"\\|,.<>\/?])   # al menos un símbolo
...     .{8,20}                                      # longitud de 8 a 20 caracteres
...     $                                            # final de cadena
... """

>>> re.match(regex, '1234')

>>> re.match(regex, 'python-m0lA', re.VERBOSE)#(1)!
<re.Match object; span=(0, 11), match='python-m0lA'>

  1. Es necesario pasar el flag re.VERBOSE para poder usar cadenas multilínea. En su versión corta se puede escribir RE.X

Aquí utilizamos f-strings para construir la expresión regular:

>>> lowercase = r'(?=.*[a-z])'
>>> uppercase = r'(?=.*[A-Z])'
>>> digit = r'(?=.*\d)'
>>> symbol = r'(?=.*[!@#$%^&*()_+\-=\[\]{};:"\\|,.<>\/?])'
>>> length = r'.{8,20}'

>>> regex = rf'^{lowercase}{uppercase}{digit}{symbol}{length}$'

>>> re.match(regex, '1234')

>>> re.match(regex, 'python-m0lA')
<re.Match object; span=(0, 11), match='python-m0lA'>

Aquí utilizamos f-strings dentro de una cadena multilínea para construir la expresión regular:

>>> lowercase = r'(?=.*[a-z])'
>>> uppercase = r'(?=.*[A-Z])'
>>> digit = r'(?=.*\d)'
>>> symbol = r'(?=.*[!@#$%^&*()_+\-=\[\]{};:"\\|,.<>\/?])'
>>> length = r'.{8,20}'

>>> regex = rf"""
...     ^             # inicio de cadena
...     {lowercase}   # al menos una letra minúscula
...     {uppercase}   # al menos una letra mayúscula
...     {digit}       # al menos un dígito
...     {symbol}      # al menos un símbolo
...     {length}      # longitud de 8 a 20 caracteres
...     $             # final de cadena
... """

>>> re.match(regex, '1234')

>>> re.match(regex, 'python-m0lA', re.VERBOSE)#(1)!
<re.Match object; span=(0, 11), match='python-m0lA'>

  1. Es necesario pasar el flag re.VERBOSE para poder usar cadenas multilínea. En su versión corta se puede escribir RE.X

Aclaraciones sobre corchetes

Hay que tener en cuenta ciertos matices al utilizar corchetes [] en una expresión regular:

  1. Los símbolos incluidos en los corchetes pierden su significado especial:

    >>> re.match(r'[.]', 'A')#(1)!
    
    >>> re.match(r'[.]', '.')#(2)!
    <re.Match object; span=(0, 1), match='.'>
    

    1. No casa...
    2. Sí casa...
  2. El guión medio hay que escaparlo en situaciones donde no represente un rango:

    >>> re.match(r'[-\d\s]', '-')#(1)!
    <re.Match object; span=(0, 1), match='-'>
    
    >>> re.match(r'[\d\s-]', '-')#(2)!
    <re.Match object; span=(0, 1), match='-'>
    
    >>> re.match(r'[\d\-\s]', '-')#(3)!
    <re.Match object; span=(0, 1), match='-'>
    

    1. No hay que escapar ya que no representa un rango.
    2. No hay que escapar ya que no representa un rango.
    3. Hay que escapar porque «parece» que representa un rango.

Ejercicios

  1. pypas   vowel-words
  2. pypas   valid-float
  3. pypas   valid-email
  4. pypas   calc-from-str
  5. pypas   valid-url