2 de agosto de 2018

Repasando python. Expresiones regulares. Resumen 2

https://docs.python.org/2/howto/regex.html
Generador de código: http://hilite.me/

Funciones a nivel de módulo - Module-Levels functions

No es necesario crear patrones. El módulo re también posee las funciones de alto nivel match(), search(), findall(), sub() y otras. Toman los mismos argumentos que los métodos de los patrones, con la expresión regular (RE) como primer argumento y la cadena a nalizar como segundo, devolviendo un objeto match o None.

>>> print re.match(r'From\s+', 'Fromage amk')
None
>>> re.match(r'From\s+', 'From amk Thu May 14 19:12:10 1998')  
<_sre.SRE_Match object at 0x...>

= = = = = = = = =

Banderas - compilation flags.

Las banderas indican modificaciones de cómo vamos a trabajar con las expresiones regulares. Entre ellas tenemos:

DOTALL, S --> Coincide cualquier carácter, incluso caracteres de líneas nuevas.

I , IGNORECASE --> Representa la coincidencia tipo case-insensitive, es decir, dará coincidencia de letras independientemente si son o no mayúsculas o minúsculas.

L, LOCALE --> hace que \w, \W, \b, and \B dependan del modo local actual. "Locales" son una caracterśitica de la biblioteca de C que intenta ayudar a escribir programas que tienen  en cuenta las diferencias entre varias lenguas. Por ejemplo, \w representa la clase [A-Za-z] pero no tiene en cuenta la "Ñ" o la "ñ". Si el sistema está bien configurado, y el Locale en español está  seleccionado, al activar la bandera LOCALE, el sistema considerará que la Ñ entra dentro de la clase [A-Z]. Activar esta bandera ralentiza algo el programa. 

M, MULTILINE --> ^ produce una coincidencia al principio de la cadena y $ al final de la misma e 

inmediatamente antes de una nueva línea (si existe)al final de la cadena. Cuando esta bandera está 

activa, ^ coincide al principio de la cadena y al principio de cada línea dentro de la cadena. De forma

idéntica, $ coincide al final de la cadena y al final de cada línea (inmediatamente antes del carácter línea nueva).

U,UNICODE --> Hacen a las clases \w, \W, \b, \B, \d, \D, \s y \S depender de la base de datos UNICODE.

X, VERBOSE --> Te permite escribir RE más fáciles de leer. Si lo activo, se ignoran los espacios en 

blanco, excepto cuando forman parte de la misma clase o están antecedidos por \. También permite  escribir comentarios con #

charref = re.compile(r"""
 &[#]                # Start of a numeric entity reference
 (
     0[0-7]+         # Octal form
   | [0-9]+          # Decimal form
   | x[0-9a-fA-F]+   # Hexadecimal form
 )
 ;                   # Trailing semicolon
""", re.VERBOSE)

charref = re.compile("&#(0[0-7]+"
                     "|[0-9]+"
                     "|x[0-9a-fA-F]+);")

Con y sin la bandera re.VERBOSE

= = = = = = = = = 

Más metacaracteres

| --> Operador OR.
^ --> Al principio. A menos que la bandera MULTILINE esté activa, se aplica al principio de la cadena.
Por ejemplo, buscar la palabra From al principio de la cadena:

>>> print re.search('^From', 'From Here to Eternity')
<_sre.SRE_Match object at 0x...>
>>> print re.search('^From', 'Reciting From Memory')
None

$ --> Al final. O justo antes de un carácter de nueva línea.

>>> print re.search('}$', '{block}')
<_sre.SRE_Match object at 0x...>
>>> print re.search('}$', '{block} ')
None
>>> print re.search('}$', '{block}\n')
<_sre.SRE_Match object at 0x...>

\A --> al principio de la cadena como ^ , pero si estoy en el modo multilínea, \A sólo lo hará al principio de la cadena y no al principio de cada línea.
\Z --> Sólo al final de la cadena.
\b --> "Contorno de palabra" Sólo se aplica al final o al principio de una palabra, si está está entre espacios en blanco o signos de puntuación.

Ejemplo:


>>> p = re.compile(r'\bclass\b')
>>> print p.search('no class at all')
<_sre.SRE_Match object at 0x...>
>>> print p.search('the declassified algorithm')
None
>>> print p.search('one subclass is')
None

\b hay que usarlo como literal. Si no puede haber confusión con el carácter BACKSPACE.

\B--> Opuesto a \b

= = = = = = = = = = = 

Agrupando - Grouping

A veces necesitamos más información que sólo si una expresión regular coincide o no. A veces necesitamos que una expresión regular detecte si hay varios grupos de caracteres en una cadena que cumplan determinadas condiciones. Por ejemplo, en la cabecera de un mensaje con el estándar RFC-822, debemos determinar si existen varios grupos separados con nombre:valor.

From: author@example.com
User-Agent: Thunderbird 1.5.0.9 (X11/20061227)
MIME-Version: 1.0
To: editor@example.com

Construimos grupos con los metacaracteres ( y ) , los paréntesis. Y a estos grupos se les puede aplicar los metacaracteres *, + , ? y {m,n}

Por ejemplo, coincidencia del grupo ab repetido...

>>> p = re.compile('(ab)*')
>>> print p.match('ababababab').span()
(0, 10)

Los grupos están indexados. El índice se puede pasar como argumento a group(), start(), end(), and span() y el índice cero, siempre que se cree un grupo, siempre existe. Por lo tanto, el grupo 0 siempre existe y coincide con el resultado de la expresión regular RE. Los métodos de los objetos match siempre tienen cero por índice por defecto.


>>> p = re.compile('(a)b')
>>> m = p.match('ab')
>>> m.group()
'ab'
>>> m.group(0)
'ab'

Los subgrupos se numeran de izquierda a derecha, desde el uno hacia arriba. Para saber el múmero de un subgrupo simplemente cuenta el número de paréntesis de apertura de izquierda a derecha:


>>> p = re.compile('(a(b)c)d')
>>> m = p.match('abcd')
>>> m.group(0)
'abcd'
>>> m.group(1)
'abc'
>>> m.group(2)
'b'

Para que devuelva una lista con todos los grupos: m.groups()
Para que devuelva una lista con grupos específicos: m.group(2,1,3)



Detectar palabras repetidas



>>> p = re.compile(r'\b(\w+)\s+\1\b')
>>> p.search('Paris in the the spring').group()
'the the'

Usamos texto raw para escribir solamente un \ .  La expresión significa:

  • (\w+) grupo de más de 1 carácter alfanumérico. Es el grupo 1
  • \s+ ...seguido de 1 o más espacios en blanco
  • \1 ...seguido del grupo número 1 (las llaman referencias traseras o backreferences)
  • Entre \b y \b, o sea, palabra completa

= = = = = = = = =

Grupos con nombre y no capturados - Non-capturing and Named Groups 

Construir expresiones regulares puede consistir en crear muchos grupos. Bien para destacar subcadenas, o bien para estructurar la expresión regular. A veces es difícil seguir la numeración de los subgrupos. Hay dos características que nos ayudan en este seguimiento; ambas usan una sintaxis común para la extensión de expresiones regulares.

Si en la sintaxis uso (?...) , ? no tiene nada que repetir. Se aprovecha esta circunstancia para indicar (en Perl o Python) que si tenemos (?=foo) 

Si quiero un grupo no-capturado, uso la sintaxis ?: como en el ejemplo...


>>> m = re.match("([abc])+", "abc")
>>> m.groups()
('c',)
>>> m = re.match("(?:[abc])+", "abc")
>>> m.groups()
()

Si quiero un grupo con nombre, necesito la sintaxis (?P<name>...). Por ejemplo:


>>> p = re.compile(r'(?P<word>\b\w+\b)')
>>> m = p.search( '(((( Lots of punctuation )))' )
>>> m.group('word')
'Lots'
>>> m.group(1)
'Lots'

He creado el grupo llamado "palabra", "word".

Y las referencias anteriores que se hacían con números \1  ahora se pueden referenciar con la sintaxis (?P=name)


La expresión regular para encontrar palabras dobles, \b(\w+)\s+\1\b se puede escribir como \b(?P<word>\w+)\s+(?P=word)\b:



>>> p = re.compile(r'\b(?P<word>\w+)\s+(?P=word)\b')
>>> p.search('Paris in the the spring').group()
'the the'

= = = = = = = = = 

Aseveraciones hacia adelante - lookahead assertions

Forma positiva (?=...) y forma negativa (?!...). Tiene éxito si la expresión regular que contiene coincide (positiva) ( o no coincide - negativa) en la posición actual, y falla en caso contrario. 

Por ejemplo, la expresión .*[.].*$ coincidirá con cualquier nombre de fichero punto extensión. "Punto" representa cualquier carácter que se repetirá 0 o más veces más un punto (clase punto [.]) más, visto desde el final de la expresión, repetirá 0 o más veces un carácter .*

Intentemos ahora varios intentos de encontrar ficheros con extensión que no sea bat. Por ejemplo:

.*[.][^b].*$  intento para que el primer carácter no sea "b". Pero claro, también excluye otros ficheros, como prueba.bar.

.*[.]([^b]..|.[^a].|..[^t])$ en esta expresión se intenta que no acepte una extensión cuyo primer carácter es b, cuyo segundo es a y cuyo tercero es t. De acuerdo, no aceptará un fichero bat. Pero tampoco uno del tipo prueba.cf con tan sólo dos caracteres. No sirve.

.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$ Este patrón incluyendo ? toma los segundos caracteres y tercero como opcionales. Vale, lo consigue. Pero es poco legible. Y lo que es peor, si quiero excluir los ficheros con extensión bat y exe, se complicaría aún más.

Pero usando una aseveración negativa: .*[.](?!bat$)[^.]*$  que significa: si no coincide el patrón bat, sigue evaluando. Y llegará a que desde el final encontrará cualesquiera caracteres que no sean puntos.

Si coincide el patrón bat, da una coincidencia negativa y falla. y ahora para incluir la extensión exe ya es fácil: .*[.](?!bat$|exe$)[^.]*$

= = = = = =

Cortando cadenas

.split(string[, maxsplit=0]) --> corta por las coincidencias del patrón. Si maxsplit no es cero, corta las veces que explicita. 


>>> p = re.compile(r'\W+')
>>> p.split('This is a test, short and sweet, of split().')
['This', 'is', 'a', 'test', 'short', 'and', 'sweet', 'of', 'split', '']
>>> p.split('This is a test, short and sweet, of split().', 3)
['This', 'is', 'a', 'test, short and sweet, of split().']

Si quiero además que aparezca el delimitador, pongo el patrón entre paréntesis.


>>> p = re.compile(r'\W+')
>>> p2 = re.compile(r'(\W+)') ### Delimitador entre paréntesis.
>>> p.split('This... is a test.')
['This', 'is', 'a', 'test', '']
>>> p2.split('This... is a test.')
['This', '... ', 'is', ' ', 'a', ' ', 'test', '.', '']

Y como función a nivel de módulo:


>>> re.split('[\W]+', 'Words, words, words.')
['Words', 'words', 'words', '']
>>> re.split('([\W]+)', 'Words, words, words.')
['Words', ', ', 'words', ', ', 'words', '.', '']
>>> re.split('[\W]+', 'Words, words, words.', 1)
['Words', 'words, words.']

= = = = = = = = =

Búsqueda y reemplazo

.sub(reemplazo, cadena[, cuenta=0]) --> retorna una cadena que se obtiene sustituyendo el reemplazo en la cadena, según coincida o no con el patrón. Lo hace las veces que se especifique en "cuenta".

>>> p = re.compile('(blue|white|red)')
>>> p.sub('colour', 'blue socks and red shoes')
'colour socks and colour shoes'
>>> p.sub('colour', 'blue socks and red shoes', count=1)
'colour socks and red shoes'

.subn hace lo mismo, pero retorna una tupla con la cadena nueva y las veces que ha realizado el reemplazo.

>>> p = re.compile('(blue|white|red)')
>>> p.subn('colour', 'blue socks and red shoes')
('colour socks and colour shoes', 2)
>>> p.subn('colour', 'no colours at all')
('no colours at all', 0)

Si el reemplazo es una cadena, cualquier carácter \ en ella se procesa. \n se convierte en una nueva línea, \r en retorno de carro y así sucesivamente. Las referencias traseras, como \1, se reemplazan con la correspondiente subcadena correspondiente al grupo de la expresión regular.

En este ejemplo se hace coincidir la palabra section por una cadena cerrada entre llaves y cambiamos section a subsection.


>>> p = re.compile('section{ ( [^}]* ) }', re.VERBOSE)
>>> p.sub(r'subsection{\1}','section{First} section{second}')
'subsection{First} subsection{second}'

También hay una sintaxis para referirse a un grupo con nombre en los reemplazos. Todas son equivalentes. Cuidado con la sintaxis \g<1>0 porque puede interpretarse como la '10'


>>> p = re.compile('section{ (?P<name> [^}]* ) }', re.VERBOSE)
>>> p.sub(r'subsection{\1}','section{First}')
'subsection{First}'
>>> p.sub(r'subsection{\g<1>}','section{First}')
'subsection{First}'
>>> p.sub(r'subsection{\g<name>}','section{First}')
'subsection{First}'

El reemplazo también puede ser una función, el resultado de ella. Se le pasa a la función el objeto match correspondiente. El siguiente ejemplo encuentra números decimales y los sustituye por hexadecimales:


>>> def hexrepl(match):
...     "Return the hex string for a decimal number"
...     value = int(match.group())
...     return hex(value)
...
>>> p = re.compile(r'\d+')
>>> p.sub(hexrepl, 'Call 65490 for printing, 49152 for user code.')
'Call 0xffd2 for printing, 0xc000 for user code.'

Si utilizamos funciones a nivel de módulo re.sub() , el primer parámetro es el patrón.

= = = = = = = = = = =

Algunos problemas

  • Algunas veces el empleo del módulo re está desaconsejado porque es más lento que métodos de cadenas ad hoc. Por ejemplo, si simplemente quiero sustituir una palabra por otra mejor usar el método replace(). O translate() para borrar un carácter o modificarlo por otro.
  • Recordar que match() dará las coincidencias si empieza desde cero, si no hay que usar search().
  • Greedy or not greedy (ávaro o no ávaro): el uso del carácter * es avaricioso. De tal forma que encuentra hasta la última instancia del mismo. Esto a veces da resultados no deseados. Para evitarlo, se usa caracteres no avariciosos como ?
  • # AVARICIOSO
    >>> s = '<html><head><title>Title</title>'
    >>> len(s)
    32
    >>> print re.match('<.*>', s).span()
    (0, 32)
    >>> print re.match('<.*>', s).group()
    <html><head><title>Title</title>
    
    # NO AVARICIOSO
    >>> print re.match('<.*?>', s).group()
    <html>
    
  • Es mejor usar , por mor de la claridad, la bandera re.VERBOSE


No hay comentarios:

Publicar un comentario