2.36. Grupos e âncoras

Um padrão pode fazer mais do que dizer “esta string corresponde” – ele pode separar os trechos correspondidos e entregar cada um à aplicação por nome. Parênteses ao redor de parte de um padrão o tornam um grupo de captura; o objeto de correspondência então expõe cada grupo como uma substring separada.

2.36.1. Grupos de captura

Envolva qualquer parte de um padrão em (...) para capturar o que ele correspondeu:

>>> import re
>>> m = re.search(r'temp (\d+) at (\d+)s', 'temp 42 at 137s ok')
>>> m.group(0)
'temp 42 at 137s'
>>> m.group(1)
'42'
>>> m.group(2)
'137'
  • O grupo 0 é sempre a correspondência inteira.

  • Os grupos 1, 2, … são as substrings capturadas, numeradas da esquerda para a direita pelo parêntese de abertura.

  • Chamar match.group() com um índice além do último grupo levanta IndexError.

Um padrão comum é “corresponder a uma estrutura conhecida e capturar as partes variáveis como inteiros”:

def parse_temp(line):
    m = re.search(r'temp (\d+) at (\d+)s', line)
    if not m:
        return None
    return int(m.group(1)), int(m.group(2))

2.36.2. Grupos sem captura

Os parênteses também agrupam uma subexpressão para que um quantificador possa se aplicar ao grupo inteiro. Esse é o único propósito do agrupamento em r'(ab)+' – “um ou mais ab“. O fato de ab aparecer como grupo 1 é um efeito colateral.

Para agrupar sem capturar, use (?:...)

>>> re.search(r'(?:ab)+', 'xababy').group(0)
'abab'

Grupos sem captura mantêm os números dos grupos organizados quando um padrão usa agrupamento por estrutura, mas não se importa em extrair cada trecho.

2.36.3. Âncoras

Âncoras não correspondem a um caractere – elas correspondem a uma posição.

  • ^ – início da string.

  • $ – fim da string.

As âncoras são o que faz re.match() e re.search() se comportarem de forma diferente. re.match(p, s) é o mesmo que re.search('^' + p, s): força o padrão a começar na posição 0. Adicionar $ ao fim de um padrão então faz com que o padrão corresponda à string inteira e a nada mais:

>>> re.search(r'^\d+$', '12345')
<match num=1>
>>> re.search(r'^\d+$', '12345 ok') is None
True

^ e $ no re do MicroPython sempre significam o início e o fim da string inteira passada para re.search(). Não há flag re.MULTILINE para fazê-los corresponder a cada quebra de linha embutida, e $ também não corresponde à posição antes de um \n final – ele tem que ser o fim absoluto da entrada. Para obter comportamento por linha, divida a entrada nas quebras de linha primeiro e execute o padrão em cada linha.

2.36.4. Conjuntos de caracteres

Colchetes definem um conjunto explícito de caracteres. A correspondência consome exatamente um caractere do conjunto.

  • [abc] – um dentre a, b, c.

  • [a-z] – um caractere no intervalo a-z (inclusive).

  • [a-zA-Z0-9] – letras ou dígitos. Três intervalos combinados.

  • [^abc]não um dentre a, b, c. O ^ só nega quando é o primeiro caractere dentro dos colchetes.

Exemplos:

>>> re.search(r'[A-F0-9]{6}', 'colour #1a2b3c rest').group(0)
'1A2B3C'
>>> re.search(r'[A-F0-9]{6}', 'colour #1a2b3c rest') is None
True

A primeira chamada retorna None na prática porque o texto literal está em minúsculas. O re do MicroPython não possui flag re.IGNORECASE – para corresponder sem diferenciar maiúsculas de minúsculas, escreva ambas as formas dentro do conjunto:

>>> re.search(r'[A-Fa-f0-9]{6}', 'colour #1a2b3c rest').group(0)
'1a2b3c'

Os atalhos de classe (\d, \s, \w e suas formas negadas) também podem ser usados dentro de [...]: [\w-] é “caracteres de palavra ou um traço literal”.

2.36.5. Quantificadores gananciosos vs preguiçosos

Os quantificadores *, +, ? e {m,n} são gananciosos por padrão – eles correspondem a tantos caracteres quanto o restante do padrão ainda permitir. Frequentemente é exatamente isso que se deseja; às vezes não é:

>>> re.search(r'<(.+)>', 'a <b> <c> d').group(1)
'b> <c'

O .+ ganancioso capturou tudo até o último >. Acrescentar ? torna o quantificador preguiçoso – ele corresponde ao mínimo possível:

>>> re.search(r'<(.+?)>', 'a <b> <c> d').group(1)
'b'

A forma preguiçosa para no primeiro >. Quantificadores preguiçosos surgem constantemente ao extrair delimitadores balanceados de uma string.

2.36.6. Retrorreferências na substituição

re.sub() pode referenciar grupos capturados na string de substituição por meio de \1, \2, … A substituição reescreve cada correspondência usando os trechos capturados:

>>> re.sub(r'(\d+)\.(\d+)', r'\2.\1', 'swap 12.34 and 5.6')
'swap 34.12 and 6.5'

Cada correspondência captura dois números, e a substituição os troca. \g<1> é uma sintaxe alternativa para a mesma coisa – útil quando o próximo caractere na substituição é um dígito (r'\g<1>0' para acrescentar um zero literal ao grupo 1 em vez de ler “grupo 10”).

2.36.7. O que não está disponível

Um lembrete do que o re do MicroPython não suporta, caso um padrão do CPython venha parar aqui e te surpreenda:

  • Lookahead (?=...) e lookbehind (?<=...) – não implementados.

  • Grupos nomeados (?P<name>...) e retrorreferências nomeadas (?P=name) – não implementados.

  • Constantes de flag como re.IGNORECASE, re.MULTILINE, re.DOTALL – não respeitadas. Construa você mesmo o conjunto que não diferencia maiúsculas de minúsculas ou divida a entrada previamente.

  • Os métodos match.groups(), match.span(), match.start() e match.end() dependem de um nível de ROM que nenhuma placa OpenMV distribuída habilita. Código que dependa deles não será executado na cam.

Com padrões, grupos e âncoras, o conjunto de ferramentas de regex na cam é pequeno o suficiente para ser aprendido de uma só vez e rico o suficiente para fazer tudo, exceto análise sensível ao contexto.