2.35. Conceitos básicos de padrões

Uma expressão regular é uma pequena linguagem para descrever strings pela forma, em vez de pelo seu conteúdo exato. Use-a quando uma string corresponde se-e-somente-se ela segue um padrão que você consegue descrever, mas não consegue 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 arquivo, em qualquer ordem, com prefixos v opcionais”.

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

Cada um desses é mais rápido, mais fácil de ler e mais difícil de errar do que a regex equivalente. Use regex quando a forma da string importa e a substring exata não.

2.35.1. As quatro coisas que você vai usar

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

  • re.compile() – transforma uma string de padrão em um objeto de padrão compilado que você pode reutilizar.

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

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

  • re.sub() – encontra cada correspondência e a substitui.

Omissões notáveis em relação ao CPython: sem re.findall, sem re.finditer, sem re.split em nível de módulo (padrões compilados têm um método split em vez disso), sem re.fullmatch, sem constantes de flag como re.IGNORECASE. Onde você recorreria a uma dessas no CPython, construa o equivalente a partir de re.search() em um laço.

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 observar:

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

  • re.search() retorna um objeto de correspondência em caso de sucesso e None em caso de falha. Sempre verifique 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 contém parênteses de captura.

O mesmo padrão com re.match() retorna 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

A maioria dos padrões úteis é construída a partir de um pequeno conjunto de partes. As que funcionam no MicroPython:

Caracteres literais – qualquer caractere que não seja especial corresponde a si mesmo. hello corresponde a hello.

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

Classes de caracteres – abreviações para conjuntos de caracteres comuns:

  • \d – qualquer dígito 0-9

  • \D – qualquer não dígito

  • \s – qualquer caractere de espaço em branco (espaço, tabulação, quebra de linha)

  • \S – qualquer caractere que não seja espaço em branco

  • \w – qualquer caractere de “palavra”: letras, dígitos, underscore

  • \W – qualquer caractere que não seja de palavra

  • . – qualquer caractere, exceto quebra de 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 e quatro dígitos. \s+ corresponde a um ou mais caracteres de espaço em branco. hello.*world corresponde a hello, qualquer coisa (inclusive nada) e então world.

Nota

Ganancioso significa que o quantificador consome a maior parte possível da entrada, contanto que o restante do padrão ainda corresponda. Contra hello x world y world, o .* em hello.*world corresponde à maior sequência que ainda deixa um world no fim – ele captura x world y, e não o mais curto x. O mesmo vale para + e para a forma de intervalo {m,n}: o motor pega a maior correspondência que conseguir e só recua se o restante do padrão falhar.

2.35.4. Substituição

re.sub() encontra cada correspondência e a substitui por uma string. A substituição pode referenciar grupos capturados por meio de \1, \2, … (abordado mais adiante junto com o restante da sintaxe de grupos). Sem grupos, re.sub é uma busca-e-substituição direta sobre uma regex:

>>> 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 sobre a qual operar; o resultado é uma nova string com cada correspondência substituída.

2.35.5. Divisão – apenas em um padrão compilado

Não há re.split em nível de módulo. Para dividir com base em uma regex, compile o padrão primeiro e chame 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. Compilando para reutilização

Se o mesmo padrão for executado muitas vezes – dentro de um laço ou em uma função crítica de desempenho – 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() em um objeto compilado é o mesmo que as funções em nível de módulo, mas evita o custo da recompilação a cada chamada.

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

Três padrões em particular pegam os desenvolvedores de surpresa:

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

  • Um padrão com um caractere especial sem escape é um erro de sintaxe. re.compile(r'cost: $5') levanta ValueError porque $ significa “fim da string”. Use r'cost: \$5'.

  • O ponto . não corresponde a uma quebra de linha. Para corresponder através de quebras de linha, escreva o padrão para tratá-las explicitamente com [\s\S] ou forneça uma linha por vez.

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