Expresiones regulares de Python

Las expresiones regulares son un lenguaje eficaz para hacer coincidir patrones de texto. En esta página, se ofrece una introducción básica a las expresiones regulares que sirven para nuestros ejercicios de Python y se muestra cómo funcionan las expresiones regulares en Python. El “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, a la búsqueda suele ir inmediatamente seguida una instrucción "if" para probar si la búsqueda se realizó correctamente, como se muestra en el siguiente ejemplo, en el que se busca el patrón "palabra:": seguida de una palabra de 3 letras (los detalles se encuentran 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 instrucción if prueba la coincidencia. Si es verdadera, la búsqueda se realiza 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ífica), la búsqueda no se realizó correctamente y no hay texto que coincida.

La “r” al comienzo de la cadena de patrón, designa un “raw” como “raw” en Python. que pasa por barras inversas sin realizar cambios, lo que es muy útil para las expresiones regulares (Java necesita esta función muy poco). Recomiendo que siempre escribas las cadenas de patrón con la “r” solo como un hábito.

Patrones básicos

La ventaja 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 únicos:

  • a, X, 9, < los caracteres comunes simplemente coinciden exactamente. Los metacaracteres que no coinciden con sí mismos porque tienen significados especiales son los siguientes: . ^ $ * + ? { [ ] \ | ( ) (detalles a continuación)
  • . (un punto): Coincide con cualquier carácter único, excepto el salto de línea “\n”
  • \w -- (w minúscula) coincide con una "palabra" carácter: una letra o un dígito, o una barra inferior [a-zA-Z0-9_]. Ten en cuenta que, aunque la palabra clave es el mnemónico de esto, solo coincide con un carácter de una sola palabra, no con una palabra completa. \W (W en mayúsculas) coincide con cualquier carácter que no sea una palabra.
  • \b: límite entre palabra y no palabra.
  • \s -- (con s minúscula) coincide con un solo carácter de espacio en blanco: espacio, línea nueva, retorno, tabulación, formulario [ \n[/\t\f]. \S (S mayúscula) coincide con cualquier carácter que no sea un espacio en blanco.
  • \t, \n, \t -- tab, nueva línea, retorno
  • \d -- dígito decimal [0-9] (algunas utilidades de regex más antiguas 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 poner una barra delante de él, \@. Si no es una secuencia de escape válida, como \c, tu programa de Python se detendrá con un error.

Ejemplos básicos

Broma: ¿Cómo se llama un cerdo con tres ojos? ¡Piii!

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

  • La búsqueda avanza por la cadena de principio a fin y se detiene en la primera coincidencia encontrada
  • Todo el patrón debe coincidir, pero no toda la cadena
  • Si match = re.search(pat, str) tiene éxito, 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

La situación se vuelve más interesante cuando usas los signos + y * para especificar la repetición en el patrón.

  • + -- 1 o más repeticiones del patrón a la izquierda, p.ej., "i+" = uno o más i
  • * -- 0 o más repeticiones del patrón a su izquierda
  • ? -- haz coincidir 0 o 1 repetición del patrón a su izquierda

Más a la izquierda y Más grande

En primer lugar, la búsqueda encuentra la coincidencia más a la izquierda para el patrón, y, en segundo lugar, intenta utilizar la mayor cantidad posible de la cadena; es decir, + y * van lo más posible (los signos + y * se dicen "voraces").

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 morado mono". Usaremos esto como ejemplo para demostrar más funciones de expresiones regulares. Este es un intento con 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 signo "-". o “.” en la dirección. Corregiremos esto con las funciones de expresión regular que aparecen a continuación.

Corchetes

Los corchetes pueden usarse para indicar un conjunto de caracteres, de modo que [abc] coincida con “a” o "b" o "c". Los códigos \w, \s, etc., también funcionan entre corchetes, con la excepción de que el punto (.) solo significa un punto literal. Para el problema de los correos electrónicos, los corchetes son una manera fácil de agregar "." y “-” al conjunto de caracteres que pueden aparecer alrededor del signo @ con el patrón r'[\w.-]+@[\w.-]+' para obtener la dirección de correo electrónico completa:

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

Extracción de grupos

El "grupo" de una expresión regular te permite seleccionar partes del texto que coincide. Supongamos que, para el problema de los correos electrónicos, queremos extraer el nombre de usuario y el host por separado. Para ello, encierra entre paréntesis ( ) el nombre de usuario y el host en el patrón, como se muestra a continuación: r'([\w.-]+)@([\w.-]+)'. En este caso, los paréntesis no cambian lo que coincidirá el patrón, sino que establecen "grupos" lógicos. dentro del texto de la coincidencia. En una búsqueda correcta, match.group(1) es el texto de coincidencia que corresponde al primer paréntesis izquierdo, y match.group(2) es el texto que corresponde al segundo paréntesis izquierdo. El elemento match.group() sin formato sigue siendo el texto completo de la coincidencia, como de costumbre.

  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 que se escribe un patrón para lo que se busca y se agregan grupos de paréntesis para extraer las partes que se desean.

buscar todos

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 a fin de iterar a través de 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 muestre 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 del 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 mostrar una lista de cadenas, findall() devuelve una lista de *tuplas*. Cada tupla representa una coincidencia del patrón y, dentro de la tupla, se encuentran 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 2 grupos con 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 aplicar un bucle a ella para hacer algunos cálculos para cada tupla. Si el patrón no incluye paréntesis, 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 solo grupo. (Función opcional oculta): A veces, hay agrupaciones entre paréntesis ( ) en el patrón, pero que no deseas extraer. 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 expresiones regulares incluyen mucho significado en unos pocos caracteres, pero son tan densos que puedes dedicar mucho tiempo a depurar tus patrones. Configura tu tiempo de ejecución para que puedas ejecutar un patrón e imprimir lo que coincida con facilidad. 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 debilitarlo y quita partes de él para obtener demasiadas coincidencias. Cuando no coincide con nada, no se puede progresar, ya que no hay nada concreto para ver. Una vez que la combinación sea excesiva, puedes ajustarla gradualmente para lograr lo que quieres.

Opciones

Las nuevas funciones 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).

  • IGNORAR CASE: ignora las diferencias en mayúsculas y minúsculas para la coincidencia, por lo tanto, “a” coincide con ambas “a” y "A".
  • DOTALL -- permite que el punto (.) coincida con la línea nueva: por lo general, coincide con cualquier cosa que no sea una línea nueva. Esto puede equivocarte; crees que .* coincide con todo, pero, de forma predeterminada, no supera el final de una línea. Ten en cuenta que \s (espacio en blanco) incluye líneas nuevas, así que si quieres hacer coincidir una ejecución de espacios en blanco que pueda incluir una línea nueva, puedes usar \s*
  • MULTILÍNEA: 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 vs. no codicioso (opcional)

Esta sección es opcional y 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 “(<.*>)” - ¿con qué coincide primero?

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

Hay una extensión de expresión regular en la que agregas un signo "?" al final, como .*? o .+?, cambiándolas para que no sean codiciosas. 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í obtener cada <..> un par por separado. Por lo general, el estilo consiste en usar .*? inmediatamente seguido por algún marcador concreto (> en este caso) al que se puede agregar .*? se ve forzada a extenderse.

El símbolo *? se originó en Perl, y las expresiones regulares que incluyen las 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 usada, para codificar esta idea de "todos estos caracteres, excepto detenerse en X" usa 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, de modo 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 cadena de reemplazo puede incluir “\1”, “\2” que se refieren al texto del grupo(1), grupo(2), etc. del texto coincidente original.

Este es un ejemplo que busca todas las direcciones de correo electrónico y las cambia para mantener el usuario (\1), pero que yo-yo-dyne.com sea el 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.