Expresiones regulares de Python

Las expresiones regulares son un lenguaje potente para hacer coincidir patrones de texto. En esta página, se ofrece una introducción básica a las expresiones regulares, suficiente para nuestros ejercicios de Python, y se muestra cómo funcionan las expresiones regulares en Python. El módulo “re” de Python proporciona compatibilidad con expresiones regulares.

En Python, una búsqueda de expresiones regulares suele escribirse de la siguiente manera:

match = re.search(pat, str)

El método re.search() toma un patrón de expresión regular y una cadena, y busca ese patrón dentro de la cadena. Si la búsqueda es exitosa, search() devuelve un objeto de coincidencia o None en caso contrario. Por lo tanto, la búsqueda suele ir inmediatamente seguida de una sentencia if para probar si la búsqueda se realizó correctamente, como se muestra en el siguiente ejemplo, que busca el patrón "word:" seguido de una palabra de 3 letras (los detalles se muestran a continuación):

import re

str = 'an example word:cat!!'
match = re.search(r'word:\w\w\w', str)
# If-statement after search() tests if it succeeded
if match:
  print('found', match.group()) ## 'found word:cat'
else:
  print('did not find')

El código match = re.search(pat, str) almacena el resultado de la búsqueda en una variable llamada "match". Luego, la sentencia "if" prueba la coincidencia; si es verdadera, la búsqueda se realizó correctamente y match.group() es el texto coincidente (p.ej., 'word:cat'). De lo contrario, si la coincidencia es falsa (ninguna para ser más específico), la búsqueda no se realizó correctamente y no hay texto coincidente.

La “r” al comienzo de la cadena del patrón designa una cadena “sin procesar” de Python que pasa por barras inversas sin cambios, lo cual es muy útil para las expresiones regulares (Java necesita esta función desfavorablemente). Te recomiendo que siempre escribas cadenas de patrones con la "r" como hábito.

Patrones básicos

El poder de las expresiones regulares es que pueden especificar patrones, no solo caracteres fijos. Estos son los patrones más básicos que coinciden con caracteres individuales:

  • a, X, 9, < -- los caracteres comunes simplemente coinciden exactamente con ellos. Estos son los metacaracteres que no se ajustan a sí mismos debido a que tienen significados especiales: ^ $ * + ? { [ ] \ | ( ) (más detalles a continuación)
  • . (un punto): coincide con cualquier carácter único, excepto la línea nueva '\n'
  • \w -- (w minúscula) coincide con un carácter "palabra": una letra o un dígito o debajo de una barra [a-zA-Z0-9_]. Ten en cuenta que, aunque "word" es el nombre mnemotécnico de esta palabra, solo coincide con el carácter de una sola palabra, no con una palabra completa. \W (W mayúscula) coincide con cualquier carácter que no sea una palabra.
  • \b: límite entre una palabra y una que no lo es
  • \s -- (s minúscula) coincide con un solo carácter de espacio en blanco: espacio, línea nueva, retorno, tabulación, forma [ \n\r\t\f]. \S (S mayúscula) coincide con cualquier carácter que no sea un espacio en blanco.
  • \t, \n, \r -- tabulación, nueva línea, volver
  • \d -- dígito decimal [0-9] (algunas utilidades de regex anteriores no admiten \d, pero todas admiten \w y \s)
  • ^ = inicio, $ = fin -- coincide con el principio o el final de la cadena
  • \ -- inhiben la "especialidad" de un carácter. Por ejemplo, usa \. para que coincida con un punto o \\ para que coincida con una barra. Si no estás seguro de si un carácter tiene un significado especial, como '@', puedes intentar colocar una barra delantera, \@. Si no es una secuencia de escape válida, como \c, tu programa Python se detendrá con un error.

Ejemplos básicos

Bromas: ¿Cómo se dice un cerdo con tres ojos? ¡Piii!

Las reglas básicas para buscar un patrón dentro de una cadena mediante expresiones regulares son las siguientes:

  • La búsqueda avanza por la cadena de principio a fin y se detiene en la primera coincidencia encontrada.
  • Debe coincidir todo el patrón, pero no toda la string
  • Si match = re.search(pat, str) es correcto, la coincidencia no es None y, en particular, match.group() es el texto coincidente.
  ## Search for pattern 'iii' in string 'piiig'.
  ## All of the pattern must match, but it may appear anywhere.
  ## On success, match.group() is matched text.
  match = re.search(r'iii', 'piiig') # found, match.group() == "iii"
  match = re.search(r'igs', 'piiig') # not found, match == None

  ## . = any char but \n
  match = re.search(r'..g', 'piiig') # found, match.group() == "iig"

  ## \d = digit char, \w = word char
  match = re.search(r'\d\d\d', 'p123g') # found, match.group() == "123"
  match = re.search(r'\w\w\w', '@@abcd!!') # found, match.group() == "abc"

Repetición

Todo se vuelve más interesante cuando utilizas + y * para especificar la repetición en el patrón.

  • + -- 1 o más apariciones del patrón a la izquierda, por ejemplo, "i+" = una o más i
  • * -- 0 o más apariciones del patrón a la izquierda
  • ? -- coinciden con 0 o 1 casos del patrón a la izquierda.

Más a la izquierda y más grande

Primero, la búsqueda encuentra la coincidencia que se encuentra más a la izquierda para el patrón y, en segundo lugar, intenta usar la mayor cantidad de cadenas posible; es decir, los signos + y * van lo más lejos posible (se dice que los signos + y * son "codiciosos").

Ejemplos de repetición

  ## i+ = one or more i's, as many as possible.
  match = re.search(r'pi+', 'piiig') # found, match.group() == "piii"

  ## Finds the first/leftmost solution, and within it drives the +
  ## as far as possible (aka 'leftmost and largest').
  ## In this example, note that it does not get to the second set of i's.
  match = re.search(r'i+', 'piigiiii') # found, match.group() == "ii"

  ## \s* = zero or more whitespace chars
  ## Here look for 3 digits, possibly separated by whitespace.
  match = re.search(r'\d\s*\d\s*\d', 'xx1 2   3xx') # found, match.group() == "1 2   3"
  match = re.search(r'\d\s*\d\s*\d', 'xx12  3xx') # found, match.group() == "12  3"
  match = re.search(r'\d\s*\d\s*\d', 'xx123xx') # found, match.group() == "123"

  ## ^ = matches the start of string, so this fails:
  match = re.search(r'^b\w+', 'foobar') # not found, match == None
  ## but without the ^ it succeeds:
  match = re.search(r'b\w+', 'foobar') # found, match.group() == "bar"

Ejemplo de correos electrónicos

Supongamos que quieres encontrar la dirección de correo electrónico dentro de la cadena "xyz alice-b@google.com mono violeta". Usaremos esto como un ejemplo en ejecución para demostrar más funciones de expresiones regulares. En este intento, se usa el patrón r'\w+@\w+':

  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'\w+@\w+', str)
  if match:
    print(match.group())  ## 'b@google'

En este caso, la búsqueda no obtiene la dirección de correo electrónico completa porque la “\w” no coincide con el “-” o el “.” de la dirección. Solucionaremos este problema con las funciones de expresiones regulares que aparecen a continuación.

Corchetes

Se pueden usar corchetes para indicar un conjunto de caracteres, por lo que [abc] coincide con “a”, “b” o “c”. Los códigos \w, \s, etc. también funcionan entre corchetes, con la excepción de que el punto (.) solo se refiere a un punto literal. Para el problema de los correos electrónicos, los corchetes son una forma fácil de agregar “.” y “-” al conjunto de caracteres que pueden aparecer alrededor de la @ con el patrón r'[\w.-]+@[\w.-]+' para obtener toda la dirección de correo electrónico:

  match = re.search(r'[\w.-]+@[\w.-]+', str)
  if match:
    print(match.group())  ## 'alice-b@google.com'
(Más funciones entre corchetes) También puedes usar un guion para indicar un rango, de modo que [a-z] coincida con todas las letras minúsculas. Para usar un guion sin indicar un rango, coloca el guion en último lugar; p.ej., [abc-]. Un sombrero arriba (^) al comienzo de un conjunto de corchetes (^) lo invierte, de modo que [^ab] significa cualquier carácter excepto “a” o “b”.

Extracción de grupos

La función "agrupar" de una expresión regular te permite seleccionar partes del texto coincidente. Supongamos que, para el problema de los correos electrónicos, queremos extraer el nombre de usuario y el host por separado. Para hacerlo, agrega paréntesis ( ) alrededor del nombre de usuario y host en el patrón, de esta manera: r'([\w.-]+)@([\w.-]+)'. En este caso, los paréntesis no cambian con qué coincidirá el patrón, sino que establecen "grupos" lógicos dentro del texto de coincidencia. En una búsqueda correcta, match.group(1) es el texto de coincidencia correspondiente al primer paréntesis izquierdo y match.group(2) es el texto correspondiente al segundo paréntesis izquierdo. El match.group() sin formato sigue siendo el texto completo de coincidencia, como es habitual.

  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'([\w.-]+)@([\w.-]+)', str)
  if match:
    print(match.group())   ## 'alice-b@google.com' (the whole match)
    print(match.group(1))  ## 'alice-b' (the username, group 1)
    print(match.group(2))  ## 'google.com' (the host, group 2)

Un flujo de trabajo común con expresiones regulares es escribir un patrón para lo que estás buscando y agregar grupos de paréntesis para extraer las partes que deseas.

encontrartodo

findall() es probablemente la función más poderosa del módulo re. Arriba usamos re.search() para encontrar la primera coincidencia de un patrón. findall() encuentra *todas* las coincidencias y las muestra como una lista de cadenas, donde cada cadena representa una coincidencia.
  ## Suppose we have a text with many email addresses
  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'

  ## Here re.findall() returns a list of all the found email strings
  emails = re.findall(r'[\w\.-]+@[\w\.-]+', str) ## ['alice@google.com', 'bob@abc.com']
  for email in emails:
    # do something with each found email string
    print(email)

findall con Files

Para los archivos, es posible que tengas el hábito de escribir un bucle para iterar sobre las líneas del archivo y, luego, podrías llamar a findall() en cada línea. En su lugar, deja que findall() haga la iteración por ti, ¡mucho mejor! Simplemente ingresa todo el texto del archivo en findall() y deja que devuelva una lista de todas las coincidencias en un solo paso (recuerda que f.read() devuelve todo el texto de un archivo en una sola cadena):

  # Open file
  f = open('test.txt', encoding='utf-8')
  # Feed the file text into findall(); it returns a list of all the found strings
  strings = re.findall(r'some pattern', f.read())

findall y Grupos

El mecanismo de grupo de paréntesis ( ) se puede combinar con findall(). Si el patrón incluye 2 o más grupos de paréntesis, entonces en lugar de devolver una lista de cadenas, findall() devuelve una lista de *tuplas*. Cada tupla representa una coincidencia del patrón y, dentro de la tupla, están los datos group(1), group(2). Por lo tanto, si se agregan 2 grupos de paréntesis al patrón de correo electrónico, findall() devuelve una lista de tuplas, cada una de las cuales contiene el nombre de usuario y el host, p.ej., ("alice", "google.com").

  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  tuples = re.findall(r'([\w\.-]+)@([\w\.-]+)', str)
  print(tuples)  ## [('alice', 'google.com'), ('bob', 'abc.com')]
  for tuple in tuples:
    print(tuple[0])  ## username
    print(tuple[1])  ## host

Una vez que tengas la lista de tuplas, puedes realizar un bucle sobre ella para hacer algunos cálculos por cada tupla. Si el patrón no incluye paréntesis, entonces findall() devuelve una lista de cadenas encontradas, como en los ejemplos anteriores. Si el patrón incluye un solo conjunto de paréntesis, findall() devuelve una lista de cadenas correspondientes a ese único grupo. (Función opcional clara: a veces hay grupos de paréntesis ( ) en el patrón, pero no es recomendable extraerlos. En ese caso, escribe los paréntesis con un ?: al comienzo, p.ej., (?: ) y ese paréntesis izquierdo no contará como un resultado grupal.

Flujo de trabajo y depuración de RE

Los patrones de expresión regular empaquetan mucho significado en solo algunos caracteres, pero son tan densos que puedes pasar mucho tiempo depurando tus patrones. Configura tu tiempo de ejecución para poder ejecutar un patrón e imprimir fácilmente lo que coincida. Por ejemplo, ejecútalo en un texto de prueba pequeño e imprime el resultado de findall(). Si el patrón no coincide con nada, intenta debilitar el patrón quitando partes para obtener demasiadas coincidencias. Cuando no coincide, no puede avanzar, ya que no hay nada concreto para observar. Una vez que coincidan demasiado, puedes trabajar para ajustarlas de forma incremental y lograr solo lo que quieres.

Opciones

Las funciones nuevas toman opciones para modificar el comportamiento de la coincidencia de patrones. La marca de opción se agrega como un argumento adicional a search() o findall(), etc., p.ej., re.search(pat, str, re.IGNORECASE).

  • IGNORECASE: ignora las diferencias en mayúsculas y minúsculas para la coincidencia, de modo que “a” coincide con “a” y “A”.
  • DOTALL: permite el punto (.) para que coincida con la línea nueva (normalmente, coincide con cualquier línea, menos la línea nueva). Esto puede molestarte, ya que crees que .* coincide con todo, pero, de forma predeterminada, no pasa más allá del final de una línea. Ten en cuenta que \s (espacio en blanco) incluye saltos de línea, por lo que si quieres hacer coincidir una ejecución de espacio en blanco que podría incluir un salto de línea, simplemente puedes usar \s*.
  • MULTILINE: dentro de una cadena compuesta por muchas líneas, permite que ^ y $ coincidan con el principio y el final de cada línea. Normalmente, ^/$ solo coincidiría con el principio y el final de toda la cadena.

Codicioso frente a no codicioso (opcional)

Esta es una sección opcional que muestra una técnica de expresión regular más avanzada que no es necesaria para los ejercicios.

Supongamos que tienes texto con etiquetas: <b>foo</b> y <i>así sucesivamente</i>

Supongamos que intentas hacer coincidir cada etiqueta con el patrón "(<.*>)". ¿Qué coincide primero?

El resultado es un poco sorprendente, pero el aspecto voraz de .* hace que coincida con todo el '<b>foo</b> y <i>así</i>' como una gran coincidencia. El problema es que .* llega lo más lejos posible, en lugar de detenerse en el primer > (es decir, es "codicioso").

Hay una extensión para la expresión regular en la que se agrega un signo ? al final, como .*? o .+?, y los cambia para que no sean voraz. Ahora se detienen lo antes posible. Entonces, el patrón '(<.*?>)' solo tendrá '<b>' como la primera coincidencia, y '</b>' como la segunda coincidencia, y así sucesivamente. En general, el estilo consiste en usar .*? inmediatamente seguido de algún marcador concreto (> en este caso) al que se fuerza la ejecución de .*?.

La extensión *? se originó en Perl, y las expresiones regulares que incluyen extensiones de Perl se conocen como expresiones regulares compatibles con Perl, pcre. Python incluye compatibilidad con Pcre. Muchas utilidades de línea de comandos, etc., tienen una marca en la que aceptan patrones pcre.

Una técnica antigua, pero muy utilizada para programar esta idea de "todos estos caracteres, excepto parar en X", utiliza el estilo de corchetes. Para lo anterior, puedes escribir el patrón, pero en lugar de .* para obtener todos los caracteres, usa [^>]*, que omite todos los caracteres que no son > (el ^ inicial "invierte" el conjunto de corchetes, por lo que coincide con cualquier carácter que no esté entre corchetes).

Sustitución (opcional)

La función re.sub(pat, replace, str) busca todas las instancias de patrón en la cadena dada y las reemplaza. La string de reemplazo puede incluir “\1”, “\2”, que hacen referencia al texto de group(1), group(2), etc. a partir del texto coincidente original.

A continuación, verás un ejemplo en el que se buscan todas las direcciones de correo electrónico y se modifican para mantener al usuario (\1) pero que tenga yo-yo-dyne.com como host.

  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  ## re.sub(pat, replacement, str) -- returns new string with all replacements,
  ## \1 is group(1), \2 group(2) in the replacement
  print(re.sub(r'([\w\.-]+)@([\w\.-]+)', r'\1@yo-yo-dyne.com', str))
  ## purple alice@yo-yo-dyne.com, blah monkey bob@yo-yo-dyne.com blah dishwasher

Ejercicio

Para practicar expresiones regulares, consulta el Ejercicio de nombres de bebés.