Expresiones regulares

Hasta ahora hemos leído archivos, buscando patrones y extrayendo varias secciones de líneas que hemos encontrado interesantes. Hemos usado métodos de cadenas como split y find, así como rebanado de listas y cadenas para extraer trozos de las líneas.

Esta tarea de buscar y extraer es tan común que Python tiene una librería muy poderosa llamada expresiones regulares que maneja varias de estas tareas de manera bastante elegante. La razón por la que no presentamos las expresiones regulares antes se debe a que, aunque son muy poderosas, son un poco más complicadas y toma algo de tiempo acostumbrarse a su sintaxis.

Las expresiones regulares casi son su propio lenguaje de programación en miniatura para buscar y analizar cadenas. De hecho, se han escrito libros enteros sobre las expresiones regulares. En este capítulo, solo cubriremos los aspectos básicos de las expresiones regulares. Para más información al respecto, recomendamos ver:

https://es.wikipedia.org/wiki/Expresi%C3%B3n_regular

https://docs.python.org/library/re.html

Se debe importar la librería de expresiones regulares re a tu programa antes de que puedas usarlas. La forma más simple de usar la librería de expresiones regulares es la función search() (N. del T.: “search” significa búsqueda). El siguiente programa demuestra una forma muy sencilla de usar esta función.

# Búsqueda de líneas que contengan 'From'
import re
man = open('mbox-short.txt')
for linea in man:
    linea = linea.rstrip()
    if re.search('From:', linea):
        print(linea)

# Código: https://es.py4e.com/code3/re01.py

Abrimos el archivo, revisamos cada línea, y usamos la expresión regular search() para imprimir solo las líneas que contengan la cadena “From”. Este programa no toma ventaja del auténtico poder de las expresiones regulares, ya que podríamos simplemente haber usado line.find() para lograr el mismo resultado.

El poder de las expresiones regulares se manifiesta cuando agregamos caracteres especiales a la cadena de búsqueda que nos permite controlar de manera más precisa qué líneas calzan con la cadena. Agregar estos caracteres especiales a nuestra expresión regular nos permitirá buscar coincidencias y extraer datos usando unas pocas líneas de código.

Por ejemplo, el signo de intercalación (N. del T.: “caret” en inglés, corresponde al signo ^) se utiliza en expresiones regulares para encontrar “el comienzo” de una lína. Podríamos cambiar nuestro programa para que solo retorne líneas en que tengan “From:” al comienzo, de la siguiente manera:

# Búsqueda de líneas que contengan 'From'
import re
man = open('mbox-short.txt')
for linea in man:
    linea = linea.rstrip()
    if re.search('^From:', linea):
        print(linea)

# Código: https://es.py4e.com/code3/re02.py

Ahora solo retornará líneas que comiencen con la cadena “From:”. Este sigue siendo un ejemplo muy sencillo que podríamos haber implementado usando el método startswith() de la librería de cadenas. Pero sirve para presentar la idea de que las expresiones regulares contienen caracteres especiales que nos dan mayor control sobre qué coincidencias retornará la expresión regular.

Coincidencia de caracteres en expresiones regulares

Existen varios caracteres especiales que nos permiten construir expresiones regulares incluso más poderosas. El más común es el punto, que coincide con cualquier carácter.

En el siguiente ejemplo, la expresión regular F..m: coincidiría con las cadenas “From:”, “Fxxm:”, “F12m:”, o “F!@m:”, ya que los caracteres de punto en la expresión regular coinciden con cualquier carácter.

# # Búsqueda de líneas que comiencen con 'F', seguidas de
# 2 caracteres, seguidos de 'm:'
import re
man = open('mbox-short.txt')
for linea in man:
    linea = linea.rstrip()
    if re.search('^F..m:', linea):
        print(linea)

# Código: https://es.py4e.com/code3/re03.py

Esto resulta particularmente poderoso cuando se le combina con la habilidad de indicar que un carácter puede repetirse cualquier cantidad de veces usando los caracteres * o + en tu expresión regular. Estos caracteres especiales indican que en lugar de coincidir con un solo carácter en la cadena de búsqueda, coinciden con cero o más caracteres (en el caso del asterisco) o con uno o más caracteres (en el caso del signo de suma).

Podemos reducir más las líneas que coincidan usando un carácter comodín en el siguiente ejemplo:

# Búsqueda de líneas que comienzan con From y tienen una arroba
import re
man = open('mbox-short.txt')
for linea in man:
    linea = linea.rstrip()
    if re.search('^From:.+@', linea):
        print(linea)

# Código: https://es.py4e.com/code3/re04.py

La cadena ^From:.+@ retornará coincidencias con líneas que empiecen con “From:”, seguidas de uno o más caracteres (.+), seguidas de un carácter @. Por lo tanto, la siguiente línea coincidirá:

From: [email protected]

Puede considerarse que el comodín .+ se expande para abarcar todos los caracteres entre los signos : y @.

From:.+@

Conviene considerar que los signos de suma y los asteriscos “empujan”. Por ejemplo, la siguiente cadena marcaría una coincidencia con el último signo @, ya que el .+ “empujan” hacia afuera, como se muestra a continuación:

From: [email protected], [email protected], and cwen @iupui.edu

Es posible indicar a un asterisco o signo de suma que no debe ser tan “ambicioso” agregando otro carácter. Revisa la documentación para obtener información sobre cómo desactivar este comportamiento ambicioso.

Extrayendo datos usando expresiones regulares

Si queremos extraer datos de una cadena en Python podemos usar el método findall() para extraer todas las subcadenas que coincidan con una expresión regular. Usemos el ejemplo de querer extraer cualquier secuencia que parezca una dirección email en cualquier línea, sin importar su formato. Por ejemplo, queremos extraer la dirección email de cada una de las siguientes líneas:

From [email protected] Sat Jan  5 09:14:16 2008
Return-Path: <[email protected]>
          for <[email protected]>;
Received: (from apache@localhost)
Author: [email protected]

No queremos escribir código para cada tipo de líneas, dividiendo y rebanando de manera distinta en cada una. El siguiente programa usa findall() para encontrar las líneas que contienen direcciones de email y extraer una o más direcciones de cada línea.

import re
s = 'Una nota de [email protected] a [email protected] sobre una reunión @ 2PM'
lst = re.findall(r'\S+@\S+', s)
print(lst)

# Código: https://es.py4e.com/code3/re05.py

El método findall() busca en la cadena en el segundo argumento y retorna una lista de todas las cadenas que parecen ser direcciones de email. Estamos usando una secuencia de dos caracteres que coincide con un carácter distinto a un espacio en blanco (\S).

El resultado de la ejecución del programa debiera ser:

['[email protected]', '[email protected]']

Traduciendo la expresión regular al castellano, estamos buscando subcadenas que tengan al menos un carácter que no sea un espacio, seguido de una @, seguido de al menos un carácter que no sea un espacio. La expresión \S+ coincidirá con cuantos caracteres distintos de un espacio sea posible.

La expresión regular retornaría dos coincidencias ([email protected] y [email protected]), pero no coincidiría con la cadena “@2PM” porque no hay caracteres que no sean espacios en blanco antes del signo @. Podemos usar esta expresión regular en un programa para leer todas las líneas en un archivo e imprimir cualquier subcadena que pudiera ser una dirección de email de la siguiente manera:

# Búsqueda de líneas que tengan una arroba entre caracteres
import re
man = open('mbox-short.txt')
for linea in man:
    linea = linea.rstrip()
    x = re.findall(r'\S+@\S+', linea)
    if len(x) > 0:
        print(x)

# Código: https://es.py4e.com/code3/re06.py

Con esto, leemos cada línea y luego extraemos las subcadenas que coincidan con nuestra expresión regular. Dado que findall() retorna una lista, simplemente revisamos si el número de elementos en ésta es mayor a cero e imprimir solo líneas donde encontramos al menos una subcadena que pudiera ser una dirección de email.

Si ejecutamos el programa en mbox.txt obtendremos el siguiente resultado:

['[email protected]']
['[email protected]']
['<[email protected]>']
['<[email protected]>']
['<[email protected]>;']
['<[email protected]>;']
['<[email protected]>;']
['apache@localhost)']
['[email protected];']

Algunas de las direcciones tienen caracteres incorrectos como “<” o “;” al comienzo o al final. Declaremos que solo estamos interesados en la parte de la cadena que comience y termine con una letra o un número.

Para lograr esto, usamos otra característica de las expresiones regulares. Los corchetes se usan para indicar un conjunto de caracteres que queremos aceptar como coincidencias. La secuencia \S retornará el conjunto de “caracteres que no sean un espacio en blanco”. Ahora seremos un poco más explícitos en cuanto a los caracteres respecto de los cuales buscamos coincidencias.

Esta será nuestra nueva expresión regular:

[a-zA-Z0-9]\S*@\S*[a-zA-Z]

Esto se está complicando un poco; puedes ver por qué decimos que las expresiones regulares son un lenguaje en sí mismas. Traduciendo esta expresión regular, estamos buscando subcadenas que comiencen con una letra minúscula, letra mayúscula, o número “[a-zA-Z0-9]”, seguida de cero o más caracteres que no sean un espacio (\S*), seguidos de un signo @, seguido de cero o más caracteres que no sean espacios en blanco (\S*), seguidos por una letra mayúscula o minúscula. Nótese que hemos cambiado de + a * para indicar cero o más caracteres que no sean espacios, ya que [a-zA-Z0-9] implica un carácter distinto de un espacio. Recuerda que el * o + se aplica al carácter inmediatamente a la izquierda del signo de suma o del asterisco.

Si usamos esta expresión en nuestro programas, nuestros datos quedarán mucho más depurados:

# Búsqueda de líneas que tengan una arroba entre caracteres
# Los caracteres deben ser una letra o un número
import re
man = open('mbox-short.txt')
for linea in man:
    linea = linea.rstrip()
    x = re.findall(r'[a-zA-Z0-9]\S+@\S+[a-zA-Z]', linea)
    if len(x) > 0:
        print(x)

# Código: https://es.py4e.com/code3/re07.py
...
['[email protected]']
['[email protected]']
['[email protected]']
['[email protected]']
['[email protected]']
['[email protected]']
['[email protected]']
['apache@localhost']

Nótese que en las líneas donde aparece [email protected], nuestra expresión regular eliminó dos caracteres al final de la cadena (“>;”). Esto se debe a que, cuando agregamos [a-zA-Z] al final de nuestra expresión regular, estamos determinando que cualquier cadena que la expresión regular encuentre al analizar el texto debe terminar con una letra. Por lo tanto, cuando vea el “>” al final de “sakaiproject.org>;”, simplemente se detiene en el último carácter que haya encontrado que coincida con ese criterio (en este caso, la “g” fue la última coincidencia).

Nótese también que el resultado de la ejecución del programa es una lista de Python que tiene una cadena como su único elemento.

Combinando búsqueda y extracción

Si quisiéramos encontrar los números en las líneas que empiecen con la cadena “X-”, como por ejemplo:

X-DSPAM-Confidence: 0.8475
X-DSPAM-Probability: 0.0000

no queremos cualquier número de coma flotante contenidos en cualquier línea. Solo queremos extraer los números de las líneas que tienen la sintaxis ya mencionada.

Podemos construir la siguiente expresión regular para seleccionar las líneas:

^X-.*: [0-9.]+

Traduciendo esto, estamos diciendo que queremos líneas que empiecen con X-, seguido por cero o más caracteres (.*), seguido por un carácter de dos puntos (:) y luego un espacio. Después del espacio, buscamos uno o más caracteres que sean, o bien un dígito (0-9), o bien un punto [0-9.]+. Nótese que al interior de los corchetes el punto efectivamente corresponde a un punto (es decir, no funciona como comodín entre corchetes).

La siguiente es una expresión bastante comprimida que solo retornará las líneas que nos interesan:

# Búsqueda de líneas que comiencen con 'X' seguida de cualquier caracter que
# no sea espacio y ':' seguido de un espacio y cualquier número.
# El número incluye decimales.
import re
man = open('mbox-short.txt')
for linea in man:
    linea = linea.rstrip()
    if re.search(r'^X\S*: [0-9.]+', linea):
        print(linea)

# Código: https://es.py4e.com/code3/re10.py

Cuando ejecutamos el programa, vemos que los datos han sido procesados, mostrando solo las líneas que buscamos.

X-DSPAM-Confidence: 0.8475
X-DSPAM-Probability: 0.0000
X-DSPAM-Confidence: 0.6178
X-DSPAM-Probability: 0.0000

Ahora, debemos resolver el problema de extraer los númueros. Aunque sería bastante sencillo usar split, podemos echar mano a otra función de las expresiones regulares para buscar y analizar la línea a la vez.

Los paréntesis son otros caracteres especiales en las expresiones regulares. Al agregar paréntesis a una expresión regular, son ignorados a la hora de hacer coincidir la cadena. Pero cuando se usa findall(), los paréntesis indican que, aunque se quiere que toda la expresión coincida, solo interesa extraer una parte de la subcadena que coincida con la expresión regular.

Entonces, hacemos los siguientes cambios a nuestro programa:

# Búsqueda de líneas que comiencen con 'X' seguida de cualquier caracter que
# no sea espacio en blanco y ':' seguido de un espacio y un número.
# El número puede incluir decimales.
# Después imprimir el número si es mayor a cero.
import re
man = open('mbox-short.txt')
for linea in man:
    linea = linea.rstrip()
    x = re.findall(r'^X\S*: ([0-9.]+)', linea)
    if len(x) > 0:
        print(x)

# Código: https://es.py4e.com/code3/re11.py

En lugar de usar search(), agregamos paréntesis alrededos de la parte de la expresión regular que representa al número de coma flotante para indicar que solo queremos que findall() retorne la parte correspondiente a números de coma flotante de la cadena retornada.

El resultado de este programa es el siguiente:

['0.8475']
['0.0000']
['0.6178']
['0.0000']
['0.6961']
['0.0000']
..

Los números siguen estando en una lista y deben ser convertidos de cadenas a números de coma flotante, pero hemos usado las expresiones regulares para buscar y extraer la información que consideramos interesante.

Otro ejemplo de esta técnica: si revisan este archivo, verán una serie de líneas en el formulario:

Detalles: http://source.sakaiproject.org/viewsvn/?view=rev&rev=39772

Si quisiéramos extraer todos los números de revisión (el número entero al final de esas líneas) usando la misma técnica del ejemplo anterior, podríamos escribir el siguiente programa:

# Búsqueda de líneas que comiencen con 'Details: rev='
# seguido de números y '.'
# Después imprimir el número si es mayor a cero
import re
man = open('mbox-short.txt')
for linea in man:
    linea = linea.rstrip()
    x = re.findall('^Details:.*rev=([0-9.]+)', linea)
    if len(x) > 0:
        print(x)

# Código: https://es.py4e.com/code3/re12.py

Traducción de la expresión regular: estamos buscando líneas que empiecen con Details:, seguida de cualquier número de caracteres (.*), seguida de rev=, y después de uno o más dígitos. Queremos encontrar líneas que coincidan con toda la expresión pero solo queremos extraer el número entero al final de la línea, por lo que ponemos [0-9]+ entre paréntesis.

Al ejecutar el programa, obtenemos el siguiente resultado:

['39772']
['39771']
['39770']
['39769']
...

Recuerda que la expresión [0-9]+ es “ambiciosa” e intentará formar una cadena de dígitos lo más larga posible antes de extraerlos. Este comportamiento “ambicioso” es la razón por la que obtenemos los cinco dígitos de cada número. La expresiones regular se expande en ambas direcciones hasta que encuentra, o bien un carácter que no sea un dígito, o bien el comienzo o final de una línea.

Ahora podemos usar expresiones regulares para volver a resolver un ejercicio que hicimos antes, en el que nos interesaba la hora de cada email. En su momento, buscamos las líneas:

From [email protected] Sat Jan  5 09:14:16 2008

con la intención de extraer la hora del día en cada línea. Antes logramos esto haciendo dos llamadas a split. Primero se dividía la línea en palabras y luego tomábamos la quinta palabra y la dividíamos de nuevo en el carácter “:” para obtener los dos caracteres que nos interesaban.

Aunque esta solución funciona, es el resultado de código bastante frágil, que depende de asumir que las líneas tienen el formato adecuado. Si bien puedes agregar chequeos (o un gran bloque de try/except) para asegurarte que el programa no falle al encontrar líneas mal formateadas, esto hará que el programa aumente a 10 o 15 líneas de código, que además serán difíciles de leer.

Podemos lograr el resultado de forma mucho más simple utilizando la siguiente expresión regular:

^From .* [0-9][0-9]:

La traducción de esta expresión regular sería que estamos buscando líneas que empiecen con From (nótese el espacio), seguido de cualquier número de caracteres (.*), seguidos de un espacio en blanco, seguido de dos dígitos [0-9][0-9], seguidos de un carácter “:”. Esa es la definición de la clase de líneas que buscamos.

Para extraer solo la hora usando findall(), agregamos paréntesis alrededor de los dos dígitos, de la siguiente manera:

^From .* ([0-9][0-9]):

Esto resultará en el siguiente programa:

# Búsqueda de líneas que comienzan con From y un caracter seguido
# de un número de dos dígitos entre 00 y 99 seguido de ':'
# Después imprimir el número si este es mayor a cero
import re
man = open('mbox-short.txt')
for linea in man:
    linea = linea.rstrip()
    x = re.findall('^From .* ([0-9][0-9]):', linea)
    if len(x) > 0: print(x)

# Código: https://es.py4e.com/code3/re13.py

Al ejecutar el programa, obtendremos el siguiente resultado:

['09']
['18']
['16']
['15']
...

Escapado de Caracteres

Dado que en expresiones regulares usamos caracteres especiales para encontrar el comienzo o final de una línea o especificar comodines, necesitamos una forma de indicar que esos caracteres son “normales” y queremos encontrar la coincidencia con el carácter específico, como un “$” o “^”.

Podemos indicar que queremos encontrar la coincidencia con un carácter anteponiéndole una barra invertida. Por ejemplo, podemos encontrar cantidades de dinero utilizando la siguiente expresión regular:

import re
x = 'We just received $10.00 for cookies.'
y = re.findall('\$[0-9.]+',x)

Dado que antepusimos la barra invertida al signo “$”, encuentra una coincidencia con el signo en la cadena en lugar de con el final de la línea, y el resto de la expresión regular coincide con uno o más dígitos del carácter “.” Nota: dentro de los corchetes, los caracteres no se consideran “especiales”. Por tanto, al escribir [0-9.], efectivamente significa dígitos o un punto. Cuando no está entre corchetes, el punto es el carácter “comodín” que coincide con cualquier carácter. Cuando está dentro de corchetes, un punto es un punto.

Resumen

Aunque solo hemos escarbado la superficie de las expresiones regulares, hemos aprendido un poco sobre su lenguaje. Son cadenas de búsqueda con caracteres especiales en su interior, los que comunican tus deseos al sistema de expresiones regulares respecto de qué se considera una coincidencia y qué información es extraída de las cadenas coincidentes. A continuación tenemos algunos de estos caracteres y secuencias especiales:

^ Coincide con el comienzo de la línea.

$ Coincide con el final de la línea

. Coincide con cualquier carácter (un comodín).

\s Coincide con un espacio en blanco.

\S Coincide con un carácter que no sea un espacio en blanco (el opuesto a \s).

* Se aplica al carácter o caracteres inmediatamente anteriores, indicando que pueden coincidir cero o más veces.

*? Se aplica al carácter o caracteres inmediatamente anteriores, indicando que coinciden cero o más veces en modo “no ambicioso”.

+ Se aplica al carácter o caracteres inmediatamente anteriores, indicando que pueden coincidir una o más veces.

+? Se aplica al carácter o caracteres inmediatamente anteriores, indicando que pueden coincidir una o más veces en modo “no ambicioso”.

? Se aplica al carácter o caracteres inmediatamente anteriores, indicando que puede coincidir cero o una vez.

?? Se aplica al carácter o caracteres inmediatamente anteriores, indicando que puede coincidir cero o una vez en modo “no ambicioso”.

[aeiou] Coincide con un solo carácter, siempre que éste se encuentre dentro del conjunto especificado. En este caso, coincidiría con “a”, “e”, “i”, “o”, o “u”, pero no con otros caracteres.

[a-z0-9] Se pueden especificar rangos de caracteres utilizando el signo menos. En este caso, sería un solo carácter que debe ser una letra minúscula o un dígito.

[^A-Za-z] Cuando el primer carácter en la notación del conjunto es “^”, invierte la lógica. En este ejemplo, habría coincidencia con un solo carácter que no sea una letra mayúscula o una letra minúscula.

( ) Cuando se agregan paréntesis a una expresión regular, son ignorados para propósitos de encontrar coincidencias, pero permiten extraer un subconjunto determinado de la cadena en que se encuentra la coincidencia, en lugar de toda la cadena como cuando se utiliza findall().

\b Coincide con una cadena vacía, pero solo al comienzo o al final de una palabra.

\B Concide con una cadena vacía, pero no al comienzo o al final de una palabra.

\d Coincide con cualquier dígito decimal; equivalente al conjunto [0-9].

\D Coincide con cualquier carácter que no sea un dígito; equivalente al conjunto [^0-9].

Sección adicional para usuarios de Unix / Linux

El soporte para buscar archivos usando expresiones regulares viene incluido en el sistema operativo Unix desde la década de 1960, y está disponible en prácticamente todos los lenguajes de programación de una u otra forma.

De hecho, hay un programa de línea de comandos incluido en Unix llamado grep (Generalized Regular Expression Parser// Analizador Generalizado de Expresiones Regulares) que hace prácticamente lo mismo que los ejemplos que hemos dado en este capítulo que utilizan search(). Por tanto, si usas un sistema Macintosh o Linux, puedes probar los siguientes comandos en tu ventana de línea de comandos.

$ grep '^From:' mbox-short.txt
From: [email protected]
From: [email protected]
From: [email protected]
From: [email protected]

Esto ordena a grep mostrar las líneas que comienzan con la cadena “From:” en el archivo mbox-short.txt. Si experimentas un poco con el comando grep y lees su documentación, encontrarás algunas sutiles diferencias entre el soporte de expresiones regulares en Python y en grep. Por ejemplo, grep no reconoce el carácter de no espacio en blanco \S, por lo que deberás usar la notación de conjuntos [^ ], que es un poco más compleja, y que significa que encontrará una coincidencia con cualquier carácter que no sea un espacio en blanco.

Depuración

Python incluye una documentación simple y rudimentaria que puede ser de gran ayuda si necesitas revisar para encontrar el nombre exacto de algún método. Esta documentación puede revisarse en el intérprete de Python en modo interactivo.

Para mostrar el sistema de ayuda interactivo, se utiliza el comando help().

>>> help()

help> modules

Si sabes qué método quieres usar, puedes utilizar el comando dir() para encontrar los métodos que contiene el módulo, de la siguiente manera:

>>> import re
>>> dir(re)
[.. 'compile', 'copy_reg', 'error', 'escape', 'findall',
'finditer', 'match', 'purge', 'search', 'split', 'sre_compile',
'sre_parse', 'sub', 'subn', 'sys', 'template']

También puedes obtener una pequeña porción de la documentación de un método en particular usando el comando dir.

>>> help (re.search)
Help on function search in module re:

search(pattern, string, flags=0)
    Scan through string looking for a match to the pattern, returning
    a match object, or None if no match was found.
>>>

La documentación incluida no es muy exhaustiva, pero puede ser útil si estás con prisa o no tienes acceso a un navegador o motor de búsqueda.

Glosario

código frágil
Código que funciona cuando los datos se encuentran en un formato específico pero tiene a romperse si éste no se cumple. Lo llamamos “código frágil” porque se rompe con facilidad.
coincidencia ambiciosa
La idea de que los caracteres + y * en una expresión regular se expanden hacia afuera para coincidir con la mayor cadena posible.
grep
Un comando disponible en la mayoría de los sistemas Unix que busca en archivos de texto, buscando líneas que coincidan con expresiones regulares. El nombre del comando significa “Generalized Regular Expression Parser, o bien”Analizador Generalizado de Expresiones Regulares".
expresión regular
Un lenguaje para encontrar cadenas más complejas en una búsqueda. Una expresión regular puede contener caracteres especiales que indiquen que una búsqueda solo coincida al comienzo o al final de una línea, junto con muchas otras capacidades similares.
comodín
Un carácter especial que coincide con cualquier carácter. En expresiones regulares, el carácter comodín es el punto.

Ejercicios

Ejercicio uno: escribe un programa simple que simule la operación del comando grep en Unix. Debe pedir al usuario que ingrese una expresión regular y cuente el número de líneas que coincidan con ésta:

$ python grep.py
Ingresa una expresión regular: ^Author
mbox.txt tiene 1798 líneas que coinciden con ^Author

$ python grep.py
Ingresa una expresión regular: ^X-
mbox.txt tiene 14368 líneas que coinciden con ^X-

$ python grep.py
Ingresa una expresión regular: java$
mbox.txt tiene 4175 líneas que coinciden con java$

Ejercicio 2: escribe un programa que busque líneas en la forma:

New Revision: 39772

Extrae el número de cada línea usando una expresión regular y el método findall(). Registra el promedio de esos números e imprímelo.

Ingresa nombre de archivo: mbox.txt
38444.0323119

Ingresa nombre de archivo: mbox-short.txt
39756.9259259

Si encuentras un error en este libro, siéntete libre de enviarme una solución usando Github.