Expressões regulares III

Procurar URLs no meio do texto, em particular ligações http ou https, com expressões regulares, considerando nomes em Unicode, nomeadamente nomes de ficheiros, de domínios e de domínios de topo (TLD, top level domains). Além disso, os nomes dos TLD, que antes eram limitados a um lote pequeno e conhecido, agora nascem todas as semanas, e com tamanhos variáveis. Aparentemente, há um limite para esse tamanho, mas até quando?

Neste momento, o limite está definido nas RFC 1035, RFC 1123, e RFC 2181. Esse limite é de 63 carateres para cada segmento do nome de domínio, e 253 para o nome na totalidade, considerando já a representação textual – Punycode – no caso dos nomes que usem letras para além do ASCII ou, mais precisamente, para além do conjunto LDH (letras, dígitos, hífen), a-z, A-Z, 0-9 e o hífen.

Os URLs contêm também nomes de diretorias e de ficheiros, assim como parâmetros e variáveis para páginas dinâmicas. Construir um interpretador de URLs não é tarefa fácil. É preferível usar um já criado e testado e, eventualmente, melhorá-lo ou corrigi-lo.

Foi isso que eu fiz, e aqui fica o programa exemplo em Python com a expressão regular para caçar URLs:

#!/usr/bin/python
# -*- coding: utf-8 -*-
import re

line = "#lol avião https://gist.github.com/HenkPoley/8899766 com o tóino a bordo test@google.co.il, person@amazon.co.uk https://www.vinicius.atongadamirongadokabuletê/fotos/pauta.gif e outro http://www.pagina.移动/index.php e mais um outro link: https:///w3.游戏/ http://w3.pt e ainda https://stat.cool/index.php "

p = re.compile(r"""(?i)\b((?:https?:(?:/{1,3}|[\w0-9%])|[\w0-9.\-]+[.](?:\w{2,63})/)(?:[^\s()<>{}\[\]]+|\([^\s()]*?\([^\s()]+\)[^\s()]*?\)|\([^\s]+?\))+(?:\([^\s()]*?\([^\s()]+\)[^\s()]*?\)|\([^\s]+?\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’])|(?:\b(?<![@.])[\w0-9]+(?:[.\-][\w0-9]+)*[.](?:\w{2,63})\b/?(?!@)))""", re.U)

r = p.findall(line)
print r

O resultado da interpretação é o seguinte:

['https://gist.github.com/HenkPoley/8899766', 'https://www.vinicius.atongadamirongadokabulet\xc3\xaa/fotos/pauta.gif', 'http://www.pagina.\xe7\xa7\xbb\xe5\x8a\xa8/index.php', 'https:///w3.\xe6\xb8\xb8\xe6\x88\x8f/', 'http://w3.pt', 'https://stat.cool/index.php']

Links usados para obter este resultado, por ordem cronológica:
Python regular expression again – match url
An Improved Liberal, Accurate Regex Pattern for Matching URLs
gruber/Liberal Regex Pattern for Web URLs
winzig/Liberal Regex Pattern for URLs
Domain Name System

Expressões regulares II

Aparentemente, para encontrar hashtags num texto, mesmo que os carateres do texto estejam em Unicode, e considerando apenas carateres de texto, excluindo separadores como os espaços, sinais de pontuação, etc., podemos usar o código do exemplo seguinte:

#!/usr/bin/python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import re
import collections
line = "#fbf to me in Edinburgh, #fbf Scotland back #aço in November 2007. #cão #brexit #céu Most of you will be too young to remember #this, #brexit #brexit #brexit but back then, the entire UK was part of the European Union. History is fascinating."

p = re.compile(ur'(?i)(?<=\#)\w+',re.U)
r = p.findall(line)
print r

O resultado é:

[u'fbf', u'fbf', u'a\xe7o', u'c\xe3o', u'brexit', u'c\xe9u', u'this', u'brexit', u'brexit', u'brexit']

Expressões regulares

Para além das expressões regulares que eu usava com o FLEX, e também depois, com o PERL e o PHP, o Python tem umas expressões novas, que me parecem ter sido criadas pelo próprio Python, e que são bastante interessantes.

*?, +?, ??
Cria uma versão não gananciosa dos quantificadores *, +, ?. Neste caso, vai ser encontrada a expressão mínima, ao invés da expresão máxima.

{m,n}?
Da mesma forma que no caso anterior, aqui vai ser encontrada a expressão mínima de {m,n}. Por exemplo, para a string de 6 carateres 'aaaaaa', a{3,5} emparelha 5 carateres 'a', enquanto que a{3,5}? emparelha apenas 3 carateres.

(?...)
É uma notação de extensão. O caráter após o ? define o resto da sintaxe. Ver abaixo.

(?iLmsux)
As letras 'i', 'L', 'm', 's', 'u', 'x' correpondem às flags re.I (ignore case), re.L (locale dependent), re.M (multi-line), re.S (dot matches all), re.U (Unicode dependent), and re.X (verbose), para toda a expressão regular. Alternativamente, também se pode passar a flag na função re.compile().

(?:...)

Captura uma expressão, mas ignora-a no grupo de expressões capturadas
(?P<name>...)

Atribui um nome ao objeto capturado. Substitui o nome group. Na mesma expressão, o nome name é único. Por exemplo, (?P<quote>['"]).*?(?P=quote) captura qualquer string delimitada por aspas ou plicas.

(?P=name)
Equivalente ao anterior.

(?#...)
Comentário. Ignorado.

(?#...)
Emparelha, se ... emparelhar o próximo, mas não consome a string. É chamada uma lookahead assertion. Por exemplo, Isaac (?=Asimov) emparelha 'Isaac ' apenas se seguido de 'Asimov'.

(?!...)
Emparelha se não emparelhar o próximo. É uma negative lookahead assertion. Por exemplo, Isaac (?!Asimov) emparelha 'Isaac ' apenas se não for seguido de 'Asimov'.

(?<=...)
Emparelha se a posição atual na string for precedida por um emparelhamento de ... que termine na posição atual. É denominada uma positive lookbehind assertion. (?<=abc)def emparelha com abcdef, uma vez que o lookbehind volta atrás 3 carateres e verifica se o padrão mencionado emparelha.

>>> import re
>>> m = re.search('(?<=abc)def', 'abcdef') >>> m.group(0)
'def'

Outro exemplo, que procura uma palavra que segue um hífen.

>>> m = re.search('(?<=-)\w+', 'spam-egg') >>> m.group(0)
'egg'

(?<!...)

Emparelha, se a posição atual na string não for precedida por um .... É chamado um negative lookbehind assertion. É o oposto do anterior.

(?(id/name)yes-pattern|no-pattern)

Tenta emparelhar com yes-pattern se o grupo com o id ou o name existir, e com no-pattern se não existir. no-pattern é opcional e pode ser omitido. Por exemplo, (<)?(\w+@\w+(?:\.\w+)+)(?(1)>) é um padrão fraco para emparelhar emails, que emparelha '<user@host.com>' assim como 'user@host.com', masn não '<user@host.com'.

 

Python, MySQL e Unicode

Ao fim de algumas horas, lá consegui ler carateres Unicode de 4 octetos em utf32 e utf8 para uma base de dados MySQL, em Python.

O código dos carateres vinha, do ficheiro original de emoticons, em utf32. EU queria guardá-lo nesse formato, mas também convertê-lo para utf8.

Criei uma tabela para receber esses carateres, nos respetivos formatos:

CREATE TABLE uni (
	utf32 CHAR(1) CHARACTER SET utf32,
	utf8 CHAR(1) CHARACTER SET utf8mb4
);

Utilizei o CHARSET utf8mb4, ao invés de utf8 apenas, pois o utf8 usa, no máximo, 3 bytes por caráter, enquanto que o utf8mb4 usa até 4 bytes. O utf8 suporta apenas os carateres do Basic Multilingual Plane (BMP). (ver mais)

Depois, era necessário converter os carateres utf32 (unicode codepoints) para utf8. Para isso, usei uma expressão que encontrei no StackOverflow.

>>> code = '01F600'
>>> c = ''.join(['{0:x}'.format(ord(x)) for x in unichr(int(code, 16)).encode('utf-8')]).upper()
F09F9880

Gastei algumas horas até chegar a este resultado. Mas, com esta solução, é possível inserir códigos dos emojis na tabela.

MySQL e Python

Para usar MySQL em Python, no Linux Slackware, que é a distribuição de Linux que eu uso, tive que instalar o módulo MySQL-python. Para Windows, Mac, ou outras distribuições de Linux, encontrei as instruções seguintes no StackOverflow:

For Windows user, you can get an exe of MySQLdb.

For Linux, this is a casual package (python-mysqldb). (You can use sudo apt-get install python-mysqldb (for debian based distros), yum install mysql-python (for rpm-based), or dnf install python-mysql (for modern fedora distro) in command line to download.)

For Mac, you can install MySQLdb using Macport.

Para o Slackware, fui ao sítio do SlackBuilds e descarreguei o script de compilação, assim como o código fonte do MySQL-python, que é descarregado do sítio do Python. O script cria um pacote instalável.

Criei, então, um script em Python, com o código seguinte, para imprimir todo o conteúdo de uma tabela de uma base de dados:

!/usr/bin/python
import MySQLdb

db = MySQLdb.connect(host="localhost",
                     user="user",
                     passwd="pass",
                     db="livros")

cur = db.cursor()
cur.execute("SELECT * FROM livro")

for row in cur.fetchall():
    row = list(map(lambda x: str(x), row))
    print ", ".join(row)
    #print type(row)

db.close()

É aconselhável não ter as credenciais de acesso à base de dados – username e password – dentro do ficheiro de Python. A melhor solução é guardar esses dados num ficheiro noutra pasta, e depois importar esse ficheiro (ver mais informação aqui) sempre que for necessário. Eis como se importa um ficheiro em Python:

#!/usr/bin/python
import imp

dba = imp.load_source('module.dba', '/tmp/db/acessobd.py')

print dba.srv, dba.usr, dba.psw

Para mais informação e exemplos sobre o uso de MySQL em Python, nomeadamente, sobre a utilização de ORMs fica aqui o link de uma entrada no StackOverflow.

O último exemplo é uma inserção numa tabela da base de dados.

#!/usr/bin/python
# -*- coding: utf-8 -*-
import MySQLdb
import imp

dba = imp.load_source('module.dba', '/tmp/db/acessobd.py')

db = MySQLdb.connect(host="localhost",
			user=dba.usr,
			passwd=dba.psw,
			db="livros")

cur = db.cursor()

try:
        cur.execute("""INSERT INTO livro VALUES (NULL,%s,%s,%s)""",('Biblia', 'Moises', -2000))
        db.commit()
except:
        db.rollback()

db.close()

Listas

Considere-se a lista seguinte, assim como algumas operações de consulta da lista.

>>> myList = ["The", "earth", "revolves", "around", "sun"]
>>> myList
['The', 'earth', 'revolves', 'around', 'sun']

>>> myList[0]
'The'

>>> myList[4]
'sun'

>>> myList[5]
Traceback (most recent call last):
  File "", line 1, in 
IndexError: list index out of range

Um índice negativo consulta a lista a partir da extremidade final

>>> myList[-1]
'sun'

Podem inserir-se elementos em posições determinadas.

>>> myList.insert(0,"Yes")
>>> myList
['Yes', 'The', 'earth', 'revolves', 'around', 'sun']

Para adicionar elementos à lista, é possível usar a função append ou a função extend. A primeira adiciona apenas um elemento. A segunda, se o elemento for uma lista, estende-a com os elementos dessa lista.

>>> myList.append(["a", "true"])

>>> myList
['Yes', 'The', 'earth', 'revolves', 'around', 'sun', ['a', 'true']]

>>> len(myList)
7
>>> myList.extend(["statement", "for", "sure"])

>>> myList
['Yes', 'The', 'earth', 'revolves', 'around', 'sun', ['a', 'true'], 'statement', 'for', 'sure']

>>> len(myList)
10

Podem extrair-se elementos de uma lista com a função slice.

>>> myList[1:4]
['The', 'earth', 'revolves']

>>> myList[:4]
['Yes', 'The', 'earth', 'revolves']

>>> myList[4:]
['around', 'sun', ['a', 'true'], 'statement', 'for', 'sure']

>>> myList[:]
['Yes', 'The', 'earth', 'revolves', 'around', 'sun', ['a', 'true'], 'statement', 'for', 'sure']

Pesquisa em listas a partir do conteúdo:

>>> myList.index("revolves")
3

>>> myList.index("a")
Traceback (most recent call last):
  File "", line 1, in 
ValueError: 'a' is not in list

>>> myList.index(["a", "true"])
6

>>> "sun" in myList
True

Remover elementos:

>>> myList
['Yes', 'The', 'earth', 'revolves', 'around', 'sun', ['a', 'true'], 'statement', 'for', 'sure']

>>> myList.remove("Yes")
>>> myList
['The', 'earth', 'revolves', 'around', 'sun', ['a', 'true'], 'statement', 'for', 'sure']

>>> myList.remove("statement")
>>> myList
['The', 'earth', 'revolves', 'around', 'sun', ['a', 'true'], 'for', 'sure']

>>> myList.remove(["a", "true"])
>>> myList
['The', 'earth', 'revolves', 'around', 'sun', 'for', 'sure']

>>> myList.pop()
'sure'
>>> myList
['The', 'earth', 'revolves', 'around', 'sun', 'for']

Operadores de listas

>>> myList
['The', 'earth', 'revolves', 'around', 'sun', 'for']
>>> myList = myList + ["sure"]
>>> myList
['The', 'earth', 'revolves', 'around', 'sun', 'for', 'sure']

>>> myList += ["."]
>>> myList
['The', 'earth', 'revolves', 'around', 'sun', 'for', 'sure', '.']

>>> myList *= 2
>>> myList
['The', 'earth', 'revolves', 'around', 'sun', 'for', 'sure', '.', 'The', 'earth', 'revolves', 'around', 'sun', 'for', 'sure', '.']

	

Formatar datas

Precisei de mudar uma data do formato americano mm/dd/aaaa, para o formato do mysql aaaa-mm-dd.

Embora isto possa ser feito de muitas outras formas, algumas eventualmente mais eficientes, aqui vai uma solução que eu imaginei, com os conhecimentos adquiridos até agora.

import re

data = "5/23/2016"
p = re.compile("(\d+)/(\d+)/(\d+)")
r = p.search(data)
print "{0}-{1:02d}-{2:02d}".format(r.group(3), int(r.group(1)), int(r.group(2)))

Ficheiros csv

O código seguinte lê um ficheiro CSV e imprime cada campo numa linha

#!/usr/bin/python
import re

with open("b.csv") as f:
        linhas = f.readlines()

regex = re.compile(r"^\xef\xbb\xbf")
linhas = list(map(lambda x: regex.sub("", x).rstrip(), linhas))

for lin in linhas:
        campos = lin.split('","')
        campos[0] = campos[0][1:]
        tam = len(campos)
        tam2 = len(campos[tam-1])
        campos[tam-1] = campos[tam-1][:tam2-1]
        for i in campos:
                print i

Tal como no artigo anterior, remove o BOM do início das linhas. Usualmente, o BOM está só na primeira linha, mas no caso de se concatenarem ficheiros, pode aparecer em várias linhas do ficheiro. Remove também o \r\n do fim de cada linha do ficheiro.

Depois, parte cada linha pelo separador de campos “,”. Por fim, remove as aspas do início do primeiro campo e do fim do último campo.

Ficheiros

Para o meu projeto, preciso de ler ficheiros em Python. A primeira ação que preciso de executar é listar os ficheiros de uma pasta. Para isso vou usar o código seguinte:

import os
os.listdir("path")   # devolve uma lista

Para listar os ficheiros com extensão “.csv”, podemos usar o código seguinte:

import os
import re
d = os.listdir(".")
for i in d[:]:
    if re.match('.*\.csv$', i): 
        print i

E agora um programa que lê um ficheiro de texto para uma lista de linhas, retira o \r\n (rstrip) do fim das linhas e o BOM do início. No fim, imprime as linhas limpas.

#!/usr/bin/python
# -*- coding: utf-8 -*-

import re

with open("a.txt") as f:
    linhas = f.readlines()

regex = re.compile(r"^\xef\xbb\xbf")
linhas = list(map(lambda x: regex.sub("", x).rstrip(), linhas))

for i in linhas:
    print i

Instruções compostas

As instruções compostas do Python podem ser as instruções seguintes, definidas usando a sintaxe BNF:

  • if
    if_stmt ::=  "if" expression ":" suite
                 ( "elif" expression ":" suite )*
                 ["else" ":" suite]
    
  • while
    while_stmt ::=  "while" expression ":" suite
                    ["else" ":" suite]
    
  • for
    for_stmt ::=  "for" target_list "in" expression_list ":" suite
                  ["else" ":" suite]
    
  • try
    try_stmt  ::=  try1_stmt | try2_stmt
    try1_stmt ::=  "try" ":" suite
                   ("except" [expression [("as" | ",") identifier]] ":" suite)+
                   ["else" ":" suite]
                   ["finally" ":" suite]
    try2_stmt ::=  "try" ":" suite
                   "finally" ":" suite
    
  • with
    with_stmt ::=  "with" with_item ("," with_item)* ":" suite
    with_item ::=  expression ["as" target]
    
  • definição de funções
    decorated      ::=  decorators (classdef | funcdef)
    decorators     ::=  decorator+
    decorator      ::=  "@" dotted_name ["(" [argument_list [","]] ")"] NEWLINE
    funcdef        ::=  "def" funcname "(" [parameter_list] ")" ":" suite
    dotted_name    ::=  identifier ("." identifier)*
    parameter_list ::=  (defparameter ",")*
                        (  "*" identifier ["," "**" identifier]
                        | "**" identifier
                        | defparameter [","] )
    defparameter   ::=  parameter ["=" expression]
    sublist        ::=  parameter ("," parameter)* [","]
    parameter      ::=  identifier | "(" sublist ")"
    funcname       ::=  identifier
    
  • definição declasses
    classdef    ::=  "class" classname [inheritance] ":" suite
    inheritance ::=  "(" [expression_list] ")"
    classname   ::=  identifier
    

Exemplo do try – finally

>>> def f():
...     a = "Not a number"
...     try:
...         a = 1/0
...     finally:
...         return a
...
>>> f()
'Not a number'
>>>
>>> def g():
...     a = "Not a number"
...     try:
...         a = 10/3
...     finally:
...         return a
...
>>> g()
3

Exemplo de aplicação da instrução for

>>> a=range(10)
>>> a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> for x in a[:]:
...     if x > 3: a.remove(x)
...
>>> a
[0, 1, 2, 3]