2.35. Noções básicas de padrões

Uma expressão regular é uma pequena linguagem para descrever strings pela forma em vez do seu conteúdo exato. Use-a quando uma string corresponde se e só se seguir um padrão que pode descrever mas não enumerar – «uma sequência de dígitos seguida de uma unidade», «uma linha que começa com ERROR e termina com um número», «qualquer uma destas extensões de ficheiro, em qualquer ordem, com prefixos v opcionais».

Recorra a re apenas quando um método de string simples não chegar.

Cada um destes é mais rápido, mais fácil de ler e mais difícil de errar do que a expressão regular equivalente. Use expressões regulares quando a forma da string é importante e a substring exata não.

2.35.1. As quatro coisas que vai usar

O módulo re do MicroPython expõe quatro coisas:

  • re.compile() – transformar uma string de padrão num objeto de padrão compilado que pode reutilizar.

  • re.match() – tentar o padrão no início de uma string. O padrão está ancorado na posição 0.

  • re.search() – tentar o padrão em qualquer lugar de uma string. Devolve a primeira correspondência.

  • re.sub() – encontrar cada correspondência e substituí-la.

Omissões notáveis em comparação com o CPython: sem re.findall, sem re.finditer, sem re.split ao nível do módulo (os padrões compilados têm um método split em vez disso), sem re.fullmatch, sem constantes de sinalizadores como re.IGNORECASE. Quando precisasse de uma dessas no CPython, construa o equivalente a partir de re.search() num ciclo.

2.35.2. Um primeiro padrão

O padrão r'\d+' corresponde a um ou mais dígitos:

>>> import re
>>> m = re.search(r'\d+', 'sensor reading 42 ok')
>>> m.group(0)
'42'

Algumas coisas a notar:

  • O padrão é escrito como uma string raw (r'...') para que a barra invertida em \d chegue ao re em vez de ser processada como um escape de string Python. Use sempre strings raw para padrões de expressões regulares.

  • re.search() devolve um objeto de correspondência em caso de sucesso e None em caso de falha. Verifique sempre antes de chamar match.group().

  • m.group(0) é o texto completo que o padrão correspondeu. Os grupos 1, 2, … aparecem mais tarde, quando o padrão contiver parênteses de captura.

O mesmo padrão com re.match() devolve None porque a string não começa com um dígito:

>>> re.match(r'\d+', 'sensor reading 42 ok') is None
True
>>> re.match(r'\d+', '42 readings')
<match num=1>

2.35.3. As partes de um padrão

Os padrões mais úteis são construídos a partir de um pequeno conjunto de partes. As que funcionam no MicroPython:

Caracteres literais – qualquer carácter que não seja especial corresponde a si próprio. hello corresponde a hello.

Caracteres especiais. ^ $ * + ? { } [ ] \ | ( ) têm todos significados abaixo. Para corresponder a um deles literalmente, escape-o com uma barra invertida: \. corresponde a um ponto literal.

Classes de caracteres – abreviaturas para conjuntos de caracteres comuns:

  • \d – qualquer dígito 0-9

  • \D – qualquer não-dígito

  • \s – qualquer carácter de espaço em branco (espaço, tabulação, nova linha)

  • \S – qualquer não-espaço em branco

  • \w – qualquer carácter «de palavra»: letras, dígitos, sublinhado

  • \W – qualquer carácter que não seja de palavra

  • . – qualquer carácter exceto nova linha

Quantificadores – quantas vezes a parte anterior deve corresponder:

  • * – zero ou mais (ganancioso)

  • + – um ou mais (ganancioso)

  • ? – zero ou um

  • {n} – exatamente n

  • {m,n} – entre m e n (inclusive)

Combinando: \d{3}-\d{4} corresponde a três dígitos, um traço, quatro dígitos. \s+ corresponde a um ou mais caracteres de espaço em branco. hello.*world corresponde a hello, qualquer coisa (incluindo nada), depois world.

Nota

Ganancioso significa que o quantificador consome o máximo da entrada possível enquanto o resto do padrão ainda corresponde. Contra hello x world y world, o .* em hello.*world corresponde à sequência mais longa que ainda deixa um world no final – captura x world y, não o mais curto x. O mesmo é verdade para + e a forma de intervalo {m,n}: o motor escolhe a correspondência mais longa possível, recuando apenas se o resto do padrão falhar.

2.35.4. Substituição

re.sub() encontra cada correspondência e substitui-a por uma string. A substituição pode referenciar grupos capturados através de \1, \2, … (abordado com o resto da sintaxe de grupos mais adiante). Sem grupos, re.sub é uma substituição direta de localização e substituição numa expressão regular:

>>> re.sub(r'\s+', ' ', 'too    many   spaces')
'too many spaces'

>>> re.sub(r'\d+', 'N', 'log 12, log 345, log 6')
'log N, log N, log N'

O terceiro argumento é a string na qual operar; o resultado é uma nova string com cada correspondência substituída.

2.35.5. Divisão – apenas num padrão compilado

Não existe re.split ao nível do módulo. Para dividir com uma expressão regular, compile o padrão primeiro e chame o seu método split

>>> sep = re.compile(r'\s*,\s*')
>>> sep.split('a , b,c ,  d')
['a', 'b', 'c', 'd']

O segundo argumento opcional limita o número de divisões:

>>> sep.split('a, b, c, d', 2)
['a', 'b', 'c, d']

2.35.6. Compilar para reutilização

Se o mesmo padrão for executado muitas vezes – dentro de um ciclo ou numa função frequentemente chamada – compile-o uma vez e reutilize o objeto compilado:

digit_run = re.compile(r'\d+')

def first_number(line):
    m = digit_run.search(line)
    return int(m.group(0)) if m else None

Chamar pattern.match() e pattern.search() num objeto compilado é o mesmo que as funções ao nível do módulo, mas evita o custo de recompilação em cada chamada.

2.35.7. Padrões que não correspondem a nada

Três padrões em particular surpreendem os programadores:

  • .* corresponde à string vazia. re.search(r'.*', s).group(0) devolve '' em qualquer entrada.

  • Um padrão com um carácter especial não escapado é um erro de sintaxe. re.compile(r'cost: $5') lança ValueError porque $ significa «fim da string». Use r'cost: \$5'.

  • O ponto . não corresponde a uma nova linha. Para corresponder em novas linhas, escreva o padrão para as tratar explicitamente com [\s\S] ou processe uma linha de cada vez.

Com estas partes, um padrão pode corresponder a quase qualquer fatia de texto de forma fixa. Extrair dados estruturados da correspondência requer grupos de captura.