5. La computación con expresiones regulares

Vamos a suponer que quieres analizar la cadena que hay abajo del Quijote de Cervantes:

1
2
3
4
C = ('La libertad, Sancho, es uno de los mas preciosos dones '
'que a los hombres dieron los cielos; con ella no pueden igualarse '
'los tesoros que encierran la tierra y el mar: por la libertad, '
'asi como por la honra, se puede y debe aventurar la vida.')

Por ejemplo, podrías extraer todas las palabras de tres letras. Hasta el momento, no sabes ninguna forma de hacerlo, aunque sea un paso fundamental de la computación lingüística.

5.1. re y findall()

Es tan fundamental que se ha inventado un lenguaje de programación entero para hacerlo. Las instrucciones de este lenguaje se llaman expresiones regulares y Python tiene un módulo para utilizarlas, llamado re. Re tiene varios métodos, pero con el que vas a empezar es con findall(), el cual debieras importar ahora:

>>> from re import findall

findall() examina una cadena buscando todos los casos de un patrón:

.. note:: findall(patrón, cadena de meta)

Le esencia del lenguaje de las expresiones regulares es permitirte diseñar un patrón adecuado para la tarea lingüísitica que tienes en mente.

Hay una dicotomía elemental en la elaboración de las expresiones regulares entre los que coinciden con una cadena cuya longitud no varia y los que coinciden con una cadena cuya longitud puede variar. Empiezo con aquellas, porque son más sencillas.

5.2. Como crear un patrón de longitud fija

5.2.1. Una cadena como una expresión regular

Una cadena en sí es una expresión regular y se puede buscar tal cual con findall(). Las instrucciones abajo tratan de coincidir con “los” y ” los ”:

1
2
3
4
>>> findall('los', C)
['los', 'los', 'los', 'los', 'los']
>>> findall(' los ', C)
[' los ', ' los ', ' los ', ' los ']

En línea 1, el patrón es 'los' y findall() retorna una lista de cinco casos. En línea 3, el patrón es ' los ' y findall() retorna una lista de cuatro casos. ¿Por qué hay discrepancia entre los dos? Si te fijas bien, la palabra cielos contiene el 'los' adicional de línea 2, así que para coincidir sólo con palabras, tendrás que rodear cada una con espacios como en 3.

5.2.2. El problema de casos que se solapan

Sin embargo, hay un problema enorme con contar con espacios para delimitar palabras, el cual se ilustra con el bloque siguiente:

1
2
3
>>> CC = ' los los los '
>>> findall(r' los ', CC)
[' los ', ' los ']

El patrón es 'los entre espacios. La cadena tiene tres casos, pero findall() sólo retorna dos, saltando el de en medio. ¿Por qué?

Voy a explicar lo que pasa por medio de un gráfico:

_images/5-Solapa1.png

El motor de la expresión regular empieza por la izquierda de la cadena y encuentra una coincidencia con el patrón en el primer espacio, que se termina en el segundo espacio, dado por la primera flecha verde. Pone la coincidencia en la lista de resultados y sigue buscando. Pero como ha ‘comido’ el segundo espacio ya como parte del primer caso, no coincide con el segundo 'los' para nada. Por eso tiene una flecha roja. Es sólo cuando llegue al tercer espacio que comienza un nuevo caso del patrón, que se cumple en el cuarto espacio, indicado por la segunda raya verde. En términos más generales, el ' los ' de en medio se solapa (“overlaps”) con el primero y el último y por lo tanto no es un objeto de la coincidencia.

La manera de arreglar el problema es rodear el segundo 'los' con un par adicional de espacios, para que el de medio deje de solapar con los otros dos:

1
2
3
>>> CC2 = ' los  los  los '
>>> findall(r' los ', CC2)
[' los ', ' los ', ' los ']

Esta versión de la cadena produce el resultado esperado, el cual se visualiza así:

_images/5-Solapa2.png

Afortunadamente, hay una solución fácil al problema de delimitar palabras.

5.2.3. \b coincide con el límite de una palabra

Es muy torpe indicar los límites de una palabra con espacios, así que voy a adelantar un truco de re y presentar el metacaracter de clase \b, que indica el límite de una palabra. El único inconveniente es que \b tiene un uso en Python como el retroceso (“backspace”), pero se puede avisarle a Python para que no lo trate así. El aviso consiste en anteponer r (“raw”) al patrón:

1
2
>>> findall(r'\blos\b', C)
>>> findall(r'\by\b', C)

Ahora bien, hay que tener claro en qué consiste una ‘palabra’ y dónde están sus límites.

En Python una ‘palabra’ se entiende como una cadena de caracteres alfanuméricos, es decir, de letras y dígitos. Y además, incluye el subrayado. Como verás abajo, este grupo de caracteres corresponde al metacaracter de clase \w, que abrevia “word”. Los límites de una palabra alfanumérica están al principio y final de la cadena, así como entre un caracter de palabra y uno que no lo es. El gráfico identifica los seis límites de palabra que tiene la cadena 'abc 123 _':

_images/5-LimPalabra.png

Puedes ponerlo a prueba con las expresiones regulares siguientes:

1
2
3
4
5
>>> C1 = 'abc 123 _ @'
>>> findall(r'\babc\b', C1)
>>> findall(r'\b123\b', C1)
>>> findall(r'\b_\b', C1)
>>> findall(r'\b@\b', C1)

Las instrucciones de 2 a 4 coinciden con el patrón; 5 no lo hace, porque '@' no es un carácter de palabra. Para más información, ve Word Boundaries.

5.2.4. | coincide con una cadena u otra

Recuerda que la tarea con que empezó el capítulo era encontrar todas las palabras de tres letras que tiene el texto. Hay seis, uno, los, mas, que, con, mar, por, asi. Re tiene un metacaracter que hace las veces de las comas en la lista, la barra vertical (“pipe”) |:

>>> findall(r'\buno\b|\blos\b|\bmas\b|\bque\b|\bcon\b|\bmar\b|\bpor\b|\basi\b', C)

Retorna un caso de uno, mas, con, mar y asi, dos casos de que y por y los cuatro casos de los.

5.2.5. () coincide con un grupo de caracteres

Cansa poner tantos \b, además de ser difícil de leer. Re provee el paréntesis () para agrupar caracteres. El patrón anterior se puede simplificar al siguiente:

>>> findall(r'\b(uno|los|mas|que|con|mar|por|asi)\b', C)

Es mucho más facíl de leer y escribir.

5.2.5.1. El paréntesis que no captura

Un problema frecuente es que se quiere retornar sólo una parte de patrón. Por ejemplo, supongo que queremos encontrar los sustantivos que terminan en -tad pero guardar sólo la raíz, o sea, la forma sin -tad. En el texto, hay libertad, dos veces. La instrucción abajo coincide con los dos casos:

>>> findall(r'\blibertad\b', C)

El problema es que retorna el patrón entero. Para suprimir el sufijo -tad, se pone la raíz entre paréntesis:

>>> findall(r'\b(liber)tad\b', C)

Ahora sólo retorna la raíz. Se dice que el paréntesis captura la cadena que tiene dentro. Para desactivar la capturación, se pone ?: después del paréntesis abierto:

>>> findall(r'\b(?:liber)tad\b', C)

5.2.6. [] coincide con un caracter de una gama y [^] con su contrario

Si la meta es encontrar todas la palabras de tres letras, el patrón anterior es muy específico. La lista uno, los, mas, que, con, mar, por, asi no son todas las palabras de tres letras posibles, sólo las quy hay por casualidad en el texto. Lo que queremos es una expresión regular que coincida con tres letras cualquieras. Re está a la altura de la tarea. Codifica ‘cualquier letra minúscula’ como [a-z], ‘cualquier letra mayúscula’ como [A-Z], y ‘cualquier dígito’ como [0-9]. Lo que necesitas son tres de los primeros:

>>> findall(r'\b[a-z][a-z][a-z]\b', C)

Una de las propiedades más útiles de los corchetes es que permiten una negación con el signo de intercalación (“caret”) ^. Por ejemplo, ¿qué coincide con tres caracteres que no son dígitos?:

>>> findall(r'\b[^0-9][^0-9][^0-9]\b', C)

Coincide con más que las palabras de tres letras, porque “no dígito” incluye el espacio y la puntuación.

La gama indicada con el guión (“dash”) no se restringe al principio o final de los dígitos o el alfabeto; puede abreviar cualquier sub-secuencia que conserva el orden natural. Como ejemplo, [m-z] puede coincidir o no con las últimas catorce letras del alfabeto:

1
2
>>> findall(r'\b[m-z][m-z][m-z]\b', C)
>>> findall(r'\b[^m-z][^m-z][^m-z]\b', C)

Se puede convertir una gama en una lista prescindiendo del guión:

>>> findall(r'\b[aceimlonqpsru][aceimlonqpsru][aceimlonqpsru]\b', C)

[aceimlonqpsru] es igual que intercalar una barra vertical entre cada letra, como en 'a|c|e|i|m|l|o|n|q|p|s|r|u', pero mucho más fácil de leer y escribir. Permite la negación también:

>>> findall(r'\b[^aceimlonqpsru][^aceimlonqpsru][^aceimlonqpsru]\b', C)

5.2.7. {} coincide con repeticiones de un carácter

En este momento, puede que te preguntes por qué tienes que darte el trabajo de escribir [a-z] tres veces. ¿No debiera haber una expresión regular que coincidiera con caracteres repetidas? Los dioses de las expresiones regulares se han apiado de ti y han creado el metacaracter de las llaves (“curly brackets”), {}, que rodean un número entero que fija las repeticiones con que coincidir:

>>> findall(r'\b[a-z]{3}\b', C)

Se puede introducir un segundo número entero para fijar un mínimo y un máximo de repeticiones con que coincidir, así:

Note

caracter{mínimo, máximo}

Un ejemplo sería:

>>> findall(r'\b[a-z]{3,5}\b', C)

Coincide con todas las palabras de tres a cinco letras minúsculas.

5.2.8. . coincide con cualquier caracter

El patrón '\b[a-z]{3}\b' es el más exacto para la tarea, pero re permite otro grado más de generalidad. El metacaracter ., el punto, coincide con cualquier carácter, hasta el espacio y la puntuación:

>>> findall(r'\b.{3}\b', C)

Este patrón ya no es adecuada, porque incluye secuencias de palabras de una o dos letras con dos o un espacios.

5.2.9. ^ y $ coinciden con el principio y final de una cadena

Una cadena tiene dos posiciones que siempre se pueden identificar, el principio y el final. Re las codifica con ^ and $:

>>> findall('^.|.$', C)

El primer carácter desde el principio de C es L y el último carácter antes del final es ..

5.2.10. \metacaracter coincide con el carácter y no el metacaracter

Suponte que tu patrón tiene que coincidir con un punto. Este es el único ejemplo posible con nuestro texto, pero no retorna sólo el punto:

>>> findall(r'.', C)

Lo que retorna son todos los caracteres, uno por uno.

Para coincidir con un punto, al punto hay que anteponerle un barra diagonal inversa para escapar su interpretación metacaracteral:

>>> findall(r'\.', C)

Esta convención vale por todos los demás metacaracteres también.

5.2.11. Resumen preliminar de los metacaracteres sencillos

Las expresiones regulares repasadas en esta sección se organizan en la tabla de Metacaracteres sencillos 1:

Metacaracteres sencillos 1
ejemplo coincide con nombre notas notación
a|b a o b disyunción   barra vertical
(ab) a y b agrupación sólo retorna lo que hay en (); (?:ab) da el resto paréntesis
[ab] a o b gama [a-z] minúscula, [A-Z] mayúscula, [0-9] dígitos corchetes
[^a] todos menos a negación   signo de intercalación
a{n} a n veces repetición a{min,max} min a max de a llaves
. un caracter punto no coincide con n punto
^a a al principio de C principio   signo de intercalación
a$ a al final de C final   signo de dólar
\meta el caracter escape   barra diagonal inversa

5.2.12. Resumen de la tarea

Para resumir, hemos repasado las cinco expresiones regulares que hay a continuación para efectuar la tarea de encontrar todas las palabras de tres letras que tiene el texto:

  1. '\buno\b|\blos\b|\bmas\b|\bque\b|\bcon\b|\bmar\b|\bpor\b|\basi\b'
  2. '\b(uno|los|mas|que|con|mar|por|asi)\b'
  3. '\b[a-z][a-z][a-z]\b'
  4. '\b[a-z]{3}\b'
  5. '\b.{3}\b'

La tercera y la cuarta son las más exactas. Las primeras dos son muy específicas porque fallan con un texto nuevo que tuviera una palabra de tres letras nueva. La última es muy general porque no fallan con cadenas de tres caracteres que no son palabras.

5.2.13. Práctica de la coincidencia de longitud fija

¿Qué instrucción de findall() coincide con …

  1. las palabras de 5 letras minúsculas que terminan en ‘s’ en C?
  2. las palabras de 6 letras que empiezan con una letra mayúscula en C?
  3. las palabras de 2 letras minúsculas que empiezan o terminan con ‘s’ en C?
  4. las palabras de 6 letras minúsculas que tienen ‘r’ como cuarta letra en C?
  5. las palabras de 3 letras minúsculas que queden después de quitar el sujifo ‘es’ en C?
  6. las palabras de 4 a 7 letras minúsculas que terminan con ‘s’ en C?
  7. las palabras de 6 letras que empiezan con mayúscula en C?

Las respuestas se encuentran en Respuestas a las prácticas con expresiones regulares, pero trata de contestar cada pregunta por tu cuenta antes de mirarlas.

5.3. Como crear un patrón de longitud variable

Hasta el momento, has tenido que fijar la longitud del patrón. Esta sección te libra de esa restricción.

5.3.1. {,} coincide con una repetición ilimitada de un carácter

Las llaves comparten con el corte la posibilidad de suprimir uno de los dos números enteros para empezar el procesamiento en una secuencia de cero o continuarlo hasta la secuencia más larga:

1
2
3
4
>>> findall(r'\b[a-z]{0}\b', C)
>>> findall(r'\b[a-z]{,2}\b', C)
>>> findall(r'\b[a-z]{2,}\b', C)
>>> findall(r'\b[a-z]{,}\b', C)

El primer ejemplo retorna una cadena nula por cada pareja de límites de palabra, como si el patrón fuera \b\b, que es lo que resulta cuando el número de letras minúsculas es 0. Línea 2 retorna todas las palabras de una o dos letras minúsculas, además de todas las cadenas nulas de línea 1 por la opción de 0. Línea 3 retorna todas las palabras con 2 o más letras minúsculas. Línea 4 muestra la posibilidad de dejar los dos argumentos en blanco, lo cual retorna todas la palabras minúsculas, además de las cadenas nulas.

5.3.2. * y + coinciden con una repetición ilimitada de un carácter

Re provee dos meta-caracteres que son equivalentes a {0,} y {1,}, + y *. Por ejemplo, para encontrar las palabras minúsculas que terminan con ‘es’:

1
2
3
4
5
6
7
8
>>> findall(r'\b[a-z]{0,}es\b', C)
['es', 'dones', 'hombres']
>>> findall(r'\b[a-z]*es\b', C)
['es', 'dones', 'hombres']
>>> findall(r'\b[a-z]{1,}es\b', C)
['dones', 'hombres']
>>> findall(r'\b[a-z]+es\b', C)
['dones', 'hombres']

Línea 1 coincide con palabras que terminan en es y empiezan con 0 o más letras minúsculas tal como lo exige [a-z]{0,}. Línea 3 coincide con lo mismo. Por lo tanto, el significado de [a-z]* tiene que ser “0 o más letras minúsculas”, igual que [a-z]{,}. Línea 5, en cambio, coincide con palabras que terminan en es y empiezan con 1 o más letras minúsculas como exigido por [a-z]{1,}. Por lo tanto, el significado de [a-z]+ tiene que ser “1 o más letras minúsculas”, igual que [a-z]{1,}. A la estrella * se le conoce como la estrella de Kleene en la informática y la lógica matemática, ver Clausura de Kleene en Wikipida.

5.3.3. ? coincide con un caracter opcional

El texto tiene dos formas de poder, puede y pueden. Si se considera puede como básica, se puede buscar las dos con una disyunción como en línea 1, pero es mucho más claro señalar n como opcional con ? como en línea 2. La opcionalidad es también una repetición de cero a una vez, como en línea 3:

1
2
3
>>> findall(r'\b(puede|pueden)\b', C)
>>> findall(r'\bpueden?\b', C)
>>> findall(r'\bpueden{0,1}\b', C)

A mi modo de ver, la segunda con el signo de interrogación es la más fácil de entender.

Si más de un carácter es opcional, hay que encerrarlos todos en paréntesis que no capturan:

>>> findall(r'\b(?:el)?la\b', C)

5.3.4. ? desactiva la codicia de *, + y ?

No querrías confiarle tu cartera a *, +, ?, porque son codiciosos, o sea, toman todo lo que pueden. Piensa en qué expresión regular coincidiría con la materia entre comas en C. A primera vista, serían las cadenas ‘, Sancho,’ y ‘, asi como por la honra,’, extraídas con la instrucción siguiente:

1
2
>>> findall(r',.*,', C)
[', Sancho, es uno de los mas preciosos dones que a los hombres dieron los cielos; con ella no pueden igualarse los tesoros que encierran la tierra y el mar: por la libertad, asi como por la honra,']

El resultado dista mucho de lo deseado: incluye todo el texto entre la primera y la tercera coma. Es así porque el motor de la expresión regular sigue buscando casos del patrón hasta encontrar el más grande. El proceso se ilustra en el diagrama siguiente:

_images/5-Codicia.png

La raya 1 indica que el motor encuentra un caso del patrón, pero no se para ahí. Sigue por raya 2 hasta encontrar la tercera coma, que también podría formar parte del patrón. Pero tampoco se para ahí. Sigue por raya 3 hasta encontrar la cuarta coma, que también podría formar parte del patrón. Ahora cuando sigue por raya 4, encuentra el final de la cadena y retrocede hasta el última punto, 4. Toda la materia entre 1 y 4 coincide con el patrón de estar entre dos comas y eso es lo que devuelve.

Es lo que nos pasa por confiar la expresión regular a un metacaracter codicioso como .. Se apaga la codicia de un metacaracter posponiéndole ?,:

1
2
3
>>> findall(r',.*?,', C)
>>> findall(r',.+?,', C)
[', Sancho,', ', asi como por la honra,']

? restringe el motor de la expresión regular a la secuencia de *, +, ? más pequeña del patrón, como en este dibujo:

_images/5-Pereza.png

Esta se llama coincidencia perezosa.

Apagar la codicia de ? supone escriber dos signos de interrogación, ??, de lo cual no sé ningún ejemplo.

5.3.5. Resumen final de los metacaracteres sencillos

Las expresiones regulares repasadas en esta sección se agregan a la tabla anterior de Metacaracteres sencillos 2:

Metacaracteres sencillos 2
ejemplo coincide con nombre notas notación
a|b a o b disyunción   barra vertical
(ab) a y b agrupación sólo retorna lo que hay en (); (?:ab) da el resto paréntesis
[ab] a o b gama [a-z] minúscula, [A-Z] mayúscula, [0-9] dígitos corchetes
[^a] todos menos a negación   signo de intercalación
a{n} a n veces repetición a{min,max} min a max de a llaves
. un caracter punto no coincide con n punto
^a a al principio de C principio   signo de intercalación
a$ a al final de C final   signo de dólar
a* cero o más de a cero o más a*? + perezoso estrella (de Kleene)
a+ una o más de a uno o más a+? * perezoso signo de más
a? con o sin a opcionalidad a?? ? perezoso signo de interrogación
\meta el caracter escape   barra diagonal inversa

5.3.6. Práctica con la coincidencia de longitud variable

Ya sabes bastante como para replantear algunos de los problemas de coincidencia de longitud fija de una forma más eficaz y ensayar algunos nuevos. ¿Qué instrucción de findall() coincide con …

  1. las palabras minúsculas que terminan en ‘n’ en C?
  2. las palabras que empiezan con una letra mayúscula en C?
  3. las palabras que empiezan con ‘l’ o ‘L’ en C?
  4. las palabras minúsculas que empiezan o terminan con ‘s’ en C?
  5. las palabras minúsculas que tienen ‘r’ o ‘rr’ que no está al principio o al final en C?
  6. las letras minúsculas que queden después de quitar el sujifo ‘s’ o ‘es’ en C?
  7. las palabras que no terminan con vocal (aeiou) en C?
  8. los infinitivos de ‘a’, o sea, los palabras minúsculas que terminan en ‘ar’ en C? (se tolera un error)
  9. todas las palabras en C?
  10. la puntuación de C?

Las respuestas se encuentran en Respuestas a las prácticas con expresiones regulares, pero trata de contestar cada pregunta por tu cuenta antes de mirarlas.

5.4. Como crear un patrón con metacaracteres de clase

Casi has terminado con los metacaracteres de las expresiones regulares. Sólo quedan por ver algunos que abrevian clases enteras de metacaracteres, para que puedas escribir todavía menos.

5.4.1. \w and \W coinciden con un caracter alfanumérico o su contrario

Puedes pensar que escribir constantemente [a-zA-Z0-9] para coincidir con los caracteres que no son puntuación sería cansado. Los desarrolladores de re están de acuerdo y han definido un metacharacter de clase que abrevia esta gama, \w. Tiene una pequeña rareza en que incluye el subrayado _. Si recuerdas las limitaciones en el nombre de variable de Python, te vendrá a mente que empiezan con una letra seguida de una combinación de letras, dígitos o subrayados. El subrayado reemplaza el espacio y hace un nombre más fácil de leer. Por eso se incluye en la clase de las letras y números, o sea, la clase alfanumérica. Re define \W como la negación de esta clase, lo cual abarca todos los caracteres no alfanuméricos.

\w provee la solución más corta y exacta al problema de identificar las palabras de tres letras en el texto:

1
2
3
>>> findall(r'\b\w{3}\b', C)
>>> findall(r'\b\W{2}\b', C)
[', ', ', ', '; ', ': ', ', ', ', ']

La segunda instrucción coincide con las secuencias de dos caracteres no alfanuméricos, que son un signo de puntuación seguido de espacio.

5.4.2. \d and \D coinciden con un dígito o su contrario

A los desarrolladores de re también les ha parecido cómodo tener un abreviatura de la gama de los dígitos [0-9] en \d, cuyo negación es \D. he aquí una pequeña muestra:

1
2
3
4
5
>>> C2 = 'a1b2?3_4'
>>> findall(r'\d', C2)
['1', '2', '3', '4']
>>> findall(r'\D', C2)
['a', 'b', '?', '_']

5.4.3. El espacio en blanco

5.4.3.1. Los caracteres invisibles \t, \v, \n, \r, \f

Hay un grupo de metacaracteres que no se ven en la pantalla de la computadora sino que mandan el cursor a cierto lugares. Que yo sepa, se han heredado de las máquinas de teletipo por medio de las máquinas de escribir. Algunos son obsoletos o en desuso, o se han adaptado a otros fines. Los saco a relucir por razones de exhaustividad.

El espacio horizontal que deja un tabulador se codifica con \t. Le corresponde un tabulador vertical que salta renglones para rellenar un formulario, \v, que ha caído en desuso. [1]

Hay tres metacaracteres de clase para terminar un renglón. El retorno de carro \r desplaza el cursor al final del renglón. El salto de línea, \n, termina el renglón y desplaza el cursor al principio del renglón siguiente. La alimentación de página \f desplaza el cursor a la página siguiente. [2]

El texto con el formato de C no tiene ningún salto de línea:

>>> findall(r'\n', C)

Pero si se reformatea así, tiene tres:

1
2
3
4
5
6
C3 = '''La libertad, Sancho, es uno de los mas preciosos dones
que a los hombres dieron los cielos; con ella no pueden igualarse
los tesoros que encierran la tierra y el mar: por la libertad,
asi como por la honra, se puede y debe aventurar la vida.'''

>>> findall(r'\n', C3)

5.4.3.2. \s and \S coinciden con un espacio en blanco o su contrario

Conviene agregar a esta grupo el espacio, ya que es otro tipo de carácter escondido que normalmente no necesitas. En conjunto, [ \t\v\n\r\f] se llaman espacio en blanco. Como son invisibles, no te das cuenta de que están presentes. Pero sí están y una expresión regular que no los tenga en cuenta tendrá un resultado inesperado. Re facilita tratar con ellos con un metacaracter de clase que los abarca, \s. Su contario es \S, como puedes adivinar. Estas instrucciones los aplica al texto:

1
2
>>> findall(r'\s', C)
>>> findall(r'\S', C)

¿Te das cuentas que la puntuación no es espacio en blanco y por lo tanto coincide en línea 2 junta con las letras?

5.4.4. \b and \B coinciden con un limite de palabra o su contrario

Ya he tenido lugar de mencionar el límite de palabra, \b. Su opuesto es el perfectamente previsible \B, aunque no sé para qué sirve.

5.4.5. \A and \Z coinciden con el principio o final de una cadena

Porque re necesita un metacaracter de clase que coincide con el principio o final de una cadena, \A y \Z, cuando ya tiene el sumamente útil ^ and $ se me escapa, pero ahí lo tienes. Supongo que algunos prefieren trabajar con los metacaracteres de clase.

5.4.6. Resumen de los metacaracteres de clase

Los metacaracteres presentados en esta sección se ordenan en la tabla de Metacaracteres de clase:

Metacaracteres de clase
metacaracter abrevia nombre notas
\w [a-zA-Z0-9_] alphanumérico w de “word”
\W [^a-zA-Z0-9_]   no es alphanumérico
\d [0-9] dígito  
\D [^0-9]   no es dígito
\t   tabulador horizontal  
\v   tabulador vertical  
\r   retorno de carro  
\n   salto de línea  
\f   alimentación de página  
\s [ \t\v\n\r\f] espacio en blanco s de “space”
\S [^ \t\v\n\r\f]   no es espacio en blanco
\b   límite de palabra b de “boundary”
\B     no es límite de palabra
\A ^ principio de cadena  
\Z $ final de cadena  

5.4.7. Práctica con los metacaracteres de clase

¿Qué instrucción de findall() coincide con …

  1. todas las palabras de C?
  2. el nombre de usuario de 'howard@tulane.edu‘?
  3. el servidor de 'howard@tulane.edu‘?
  4. tanto el nombre de usuario como el servidor de 'howard@tulane.edu‘?
  5. el servidor de ‘http://tulane.edu‘?
  6. la dirección de calle de ‘La Casa Blanca, 1600 Pennsylvania Avenue NW, Washington, DC 20500’?
  7. la ciudad de ‘La Casa Blanca, 1600 Pennsylvania Avenue NW, Washington, DC 20500’?
  8. el estado de ‘La Casa Blanca, 1600 Pennsylvania Avenue NW, Washington, DC 20500’?
  9. el código postal de ‘La Casa Blanca, 1600 Pennsylvania Avenue NW, Washington, DC 20500’?
  10. el prefijo de ‘504-862-3417’?
  11. el resto de ‘504-862-3417’?

Las respuestas se encuentran en Respuestas a las prácticas con expresiones regulares, pero trata de contestar cada pregunta por tu cuenta antes de mirarlas.

5.5. Como crear un patrón con caracteres de Unicode

Para aprender como el módulo de re de las expresiones regulares maneja los caracteres que no son de ASCII, vas a volver a la cita de Don Quijote, pero ahora con sus dos acentos:

1
2
3
4
C = ('La libertad, Sancho, es uno de los más preciosos dones '
'que a los hombres dieron los cielos; con ella no pueden igualarse '
'los tesoros que encierran la tierra y el mar: por la libertad, '
'así como por la honra, se puede y debe aventurar la vida.')

Se pasa a Unicode:

1
2
3
>>> U = C.decode('utf8')
>>> U
u'La libertad, Sancho, es uno de los m\xe1s preciosos dones que a los hombres dieron los cielos; con ella no pueden igualarse los tesoros que encierran la tierra y el mar: por la libertad, as\xed como por la honra, se puede y debe aventurar la vida.'

5.5.1. Como activar la coincidencia de caracteres no ASCII con UNICODE

Como antes, quieres deseñar una expresión regular que coincide con las palabras de tres letras. Mira con detenimiento las cuatro instrucciones de findall():

1
2
3
4
5
6
>>> from re import findall, UNICODE
>>> findall(r'\b[a-z]{3}\b', U)
>>> findall(r'\b[a-z]{3}\b', U, UNICODE)
>>> findall(r'\b\w{3}\b', U)
>>> findall(r'\b\w{3}\b', U, UNICODE)
[u'uno', u'los', u'm\xe1s', u'que', u'los', u'los', u'con', u'los', u'que', u'mar', u'por', u'as\xed', u'por']

Las instrucciones de 2 y 3 se valen de la gama alfabética [a-z], pero no coinciden con las palabras acentuadas. Las instrucciones de 4 y 5 se valen del metacarácter de clase \w, pero sólo la 5 que tiene la bandera de UNICODE coincide con las palabras acentuadas.

La gama alfabética [a-zA-Z] se restringe a ASCII, lo cual la hace inútil para el texto. El metacarácter alfanumérica de clase \w, en cambio, es sensible a su contexto y por lo tanto puede adaptarse a Unicode – si es que la bandera de UNICODE lo avisa.

5.5.2. Como traducir entre cadenas y números de Unicode con ord() y unichar()

A todos los caracteres de Unicode les corresponde un número, parecido a un índice. Python dispone de un par de métodos para traducir entre un carácter y su número, ejemplificado abajo con ñ:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> C = 'ñ'
>>> U = C.decode('utf8')
>>> U
u'\xf1'
>>> ord(U)
241
>>> unichr(241)
u'\xf1'
>>> print unichr(241).encode('utf8')
ñ

En 5, ord() retorna el número de ñ en Unicode. En 6, unichar() retorna la secuencia de escape de carácter del número. Línea 9 se vale de unichar() para codificar la cadena de Unicode a UTF-8.

Saber el número de un carácter te proporciona una manera útil de filtrar los caracteres no ASCII de un texto, porque todos ellos son más grandes de 128, aunque no sabes todavía la técnica computacional para hacerlo.

5.6. Will the best regex please stand up?

5.6.1. Under-fitting vs. over-fitting

This challenge of finding the regular expression that is just right may remind you of the story of Goldilocks and the three bears, in which Goldilocks tried to find the bowl of porridge that was neither too hot nor too cold. Statisticians have their own version of Goldilocks, which evaluates how well a statistical analysis fits the data that it is applied to. An analysis that over-fits the data is too specific, in that it excludes data points from a larger data set that should be included. Conversely, an analysis that under-fits the data is too general, in that it includes data points from a larger data set that should be excluded. In our example, the first two regular expressions over-fit the data set (at should be included), while the last two under-fit it (19 should be excluded). [3]

5.6.2. False positives and false negatives

Statistical test theory provides an alternative way of conceptualizing the problem, which I unfortunately can’t figure out how to tie in to Goldilocks. Though it is usually illustrated in terms of medical tests, I believe that explaining it in terms of legal ‘tests’ is easier to understand. Imagine that a person is charged with a crime and goes through a trial. If she is guilty and the verdict is guilty, the trial has produced a true positive data point: a guilty person is found guilty. Conversely, if she is innocent and the verdict is not guilty, the trial has produced a true negative data point: a not-guilty person is found not guilty.

We expect that an accurate test only produces true positives and true negatives, but there are two more logical possibilities that leave room for a test to be nearly accurate. One is for an innocent person to be found guilty. This is called a false positive data point, because the accused should have failed the test but instead passed it. Alternatively, if a guilty person is found innocent, the legal test has produced a false negative data point, because the accused should have passed the test but instead failed it. [4]

All four possibilities are summarized in Four outcomes of a trial:

Four outcomes of a trial
  true false
positive guilty found guilty innocent found guilty
negative innocent found not guilty guilty found not guilty

It is commonly held that the American legal system is designed to err on the side of false negatives, in the sense that it is better for the guilty to be judged not guilty once in a while than for the innocent to be judged guilty even once.

5.6.3. Summary of the two sorts of regex evaluation

The table Four outcomes of a regular expression attempts to bring the two ways of evaluating a regular expression together, along with some examples. Each cell states how the string indicated is fit by the regular expression indicated, which has been shorn of blank space. The location of the cell within the table shows how the evaluation is classified as a statistical test.

Four outcomes of a regular expression
  true false
positive evaluation of ‘to’ by [a-z]{2} results in good fit evaluation of ‘at’ by (?:to|be|it|as) results in under-fit
negative evaluation of ‘the’ by [a-z]{2} results in bad fit evaluation of ‘19’ by .{2} results in over-fit

5.6.4. Which is faster?

5.7. Resumen

Una gran parte de este capítulo va dirigida a diseñar una expresión regular que coincide con las palabras de tres letras que tiene el texto. Se ha demostrado como se puede transformar la expresión específica de 1 a la expresión general de 6:

  1. '\buno\b|\blos\b|\bmas\b|\bque\b|\bcon\b|\bmar\b|\bpor\b|\basi\b'
  2. '\b(uno|los|mas|que|con|mar|por|asi)\b'
  3. '\b[a-z][a-z][a-z]\b'
  4. '\b[a-z]{3}\b'
  5. '\b\w{3}\b'
  6. '\b.{3}\b'

5.8. Further practice

5.8.1. Inductive vs. deductive reasoning

What is Sherlock Holmes known for? For amazing leaps of deduction, you might say. The Wikipedia entry on Holmesian deduction provides a helpful quote from “A Scandal in Bohemia”, in which Holmes tells Watson that he had gotten very wet lately and that he had “a most clumsy and careless servant girl”. When Watson demands to know how Holmes could have made such a detailed and accurate guess, Holmes explains:

It is simplicity itself ... My eyes tell me that on the inside of your left shoe, just where the firelight strikes it, the leather is scored by six almost parallel cuts. Obviously they have been caused by someone who has very carelessly scraped round the edges of the sole in order to remove crusted mud from it. Hence, you see, my double deduction that you had been out in vile weather, and that you had a particularly malignant boot-slitting specimen of the London slavey.

You may have noticed that I characterize Holmes’ ratiocination as guessing, but he himself – as well as Wikipedia – calls it deduction. I was trying to be polite, as you can surmise by trying to categorize the example in the terms of the table of Two types of reasoning:

Two types of reasoning
  induction deduction
premise 1 Sherlock is a grandfather. All men are mortal.
premise 2 Sherlock is bald. Sherlock is a man.
conclusion All grandfathers are bald. Sherlock is mortal
characterization specific > general general > specific
process bottom-up top-down

In the example, Holmes starts with a very specific observation and works backwards to a general cause. Yet Two types of reasoning classifies this as induction, not deduction. So to the extent that the example is representative of Holmesian reasoning, it is almost exclusively inductive. Yet you can appreciate why Holmes would want to say that it is deductive. If the premises are true in deduction, then the conclusion is guaranteed to be true, too. Not so in induction: even true premises can lead to a false conclusion.

So what does this have to do with practicing regular expressions, you might ask. Well, imagine what the relationship is between a regular expression and the substrings that it matches. The regular expression is a general statement; the substrings that it matches are specific instances. So you can do two types of exercises, a deductive one in which you are given a regular expression and asked to infer the substrings it matches, and an inductive one in which you are given a bunch of substrings are asked to induce a regular expression that they will match.

5.8.1.1. Deductive

Restate these with character classes. Match in S …

  1. the words that end in ‘t’.
  2. the words that start with a capital letter.
  3. the words that start with ‘th’ in any case.
  4. the words whose fourth letter can be ‘v’ or ‘n’. (two ways)
  5. the words that are at least five letters long.
  6. all the punctuation (which includes the newline character ‘\n’).

The answers are found at Respuestas a las prácticas con expresiones regulares, but try to do them all before looking at the answers.

Explain:

1
2
3
4
5
6
re.findall('[^aeiou]{3}', S3+'_'+S4)
re.findall('a|e|i|o|u{2}', S3)
re.findall('[^aeiou]{2,}', S4)
re.findall('[^aeiou]{,3}', S4)
re.findall('[^aeiou]{2,2}', S4)
re.findall('fish(es)? ', S6)

5.8.1.2. Inductive

5.9. Further reading

Python’s documentation of regular expressions is found at 7.2. re — Regular expression operations. The most thorough on-line tutorial I have found is at regular-expressions.info. Questions about regular expressions are answered at Stack Overflow under the tag regex.

Footnotes

[1]Wikipedia has a rudimentary page on over-fitting but none on under-fitting. A search for either term turns up some introductory material from the literature on machine learning.
[2]Wikipedia’s explanation of statistical hypothesis testing puts it in the more general framework of Type I and type II errors.
[3]See the discussion at Stack Overflow on vertical tabs.
[4]See the discussion at Stack Overflow on terminating lines.

Last edited: February 10, 2015

Table Of Contents

Previous topic

4. La computación con cadenas de Unicode

Next topic

6. Documentos digitales como cadenas

This Page