Expressões regulares IV

A expressão regular mais trabalhosa, até agora, foi a seguinte, que serve para capturar menções, ou seja, nomes precedidos por @, mas que não são emails.

O mais difícil foi perceber que a expressão (?<=\@)\w+ não devolve o @ para o negative lookbehind anterior, e por isso é que tive que usar o @ também no negative lookbehind (?<!\w\@).

Eis o código:

#!/usr/bin/python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import re
import collections

line = "@mary call @john or send him an email @ john2@gmail.com also tell @mary that I cannot go. Regards @estêvão"

p = re.compile(ur'(?i)(?<!\w\@)((?<=\@)\w+)',re.U)
r = p.findall(line)
cnt = collections.Counter(r)

print cnt

for key, value in cnt.iteritems():
    print key, value

e o resultado, com contagem de ocorrências:

Counter({u'mary': 2, u'est\xeav\xe3o': 1, u'john': 1})
estêvão 1
john 1
mary 2
gmail 1

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 emojis, 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.