segunda-feira, 10 de maio de 2021

Expressões regulares em python

❝Algumas pessoas, quando confrontadas com um problema, pensam “Eu sei, usarei expressões regulares”. Agora eles tem dois problemas.❞
- Jamie Zawinski

Mergulho

Tirar um pequeno trecho de um grande bloco de texto é um desafio. Em Python, strings têm métodos para pesquisa e substituição: index(), find(), split(), count(), replace(). Mas esses métodos são limitados aos casos mais simples. Por exemplo, o método index() procura uma única substring codificada e a pesquisa sempre diferencia maiúsculas de minúsculas. Para fazer pesquisas que não diferenciam maiúsculas de minúsculas de uma string s, você deve chamar s.lower() ou s.upper() e certificar-se de que suas strings de pesquisa são as maiúsculas e minúsculas apropriadas. Os métodos replace() e split() têm as mesmas limitações.

Se seu objetivo pode ser alcançado com métodos de string, você deve usá-los. Eles são rápidos, simples e fáceis de ler, e há muito a ser dito sobre um código rápido, simples e legível. Mas se você estiver usando várias funções de string diferentes com instruções if para lidar com casos especiais, ou se estiver encadeando chamadas para split() e join() para fatiar e dividir suas strings, pode ser necessário passar para as expressões regulares.

As expressões regulares são uma forma poderosa e (principalmente) padronizada de pesquisar, substituir e analisar texto com padrões complexos de caracteres. Embora a sintaxe da expressão regular seja restrita e diferente do código normal, o resultado pode acabar sendo mais legível do que uma solução enrolada à mão que usa uma longa cadeia de funções de string. Existem até maneiras de incorporar comentários em expressões regulares, para que você possa incluir documentação refinada neles.

Observação

Se você usou expressões regulares em outras linguagens (como Perl, JavaScript ou PHP), a sintaxe do Python será muito familiar. Leia o resumo do módulo re para obter uma visão geral das funções disponíveis e seus argumentos.

Estudo de caso: Nº de endereços de rua

Esta série de exemplos foi inspirada por um problema da vida real que tive em meu trabalho diário há vários anos, quando precisei limpar e padronizar endereços de rua exportados de um sistema legado antes de importá-los para um sistema mais novo. (Veja, eu não apenas invento essas coisas; elas são realmente úteis). Este exemplo mostra como abordei o problema.

>>> s = '100 NORTH MAIN ROAD'
>>> s.replace('ROAD', 'RD.')                ①
'100 NORTH MAIN RD.'
>>> s = '100 NORTH BROAD ROAD'
>>> s.replace('ROAD', 'RD.')                ②
'100 NORTH BRD. RD.'
>>> s[:-4] + s[-4:].replace('ROAD', 'RD.')  ③
'100 NORTH BROAD RD.'
>>> import re                               ④
>>> re.sub('ROAD$', 'RD.', s)               ⑤
'100 NORTH BROAD RD.'
  1. Meu objetivo é padronizar um endereço de rua para que 'ROAD' seja sempre abreviado como 'RD.'. À primeira vista, pensei que era simples o suficiente para que pudesse usar apenas o método string replace(). Afinal, todos os dados já estavam em maiúsculas, portanto, as incompatibilidades de maiúsculas e minúsculas não seriam um problema. E a string de pesquisa, 'ROAD', era uma constante. E neste exemplo aparentemente simples, s.replace() realmente funciona.
  2. A vida, infelizmente, está cheia de contra-exemplos, e eu rapidamente descobri este. O problema aqui é que 'ROAD' aparece duas vezes no endereço, uma vez como parte do nome da rua 'BROAD' e outra como sua própria palavra. O método replace() vê essas duas ocorrências e substitui cegamente as duas; enquanto isso, vejo meus endereços sendo destruídos.
  3. Para resolver o problema de endereços com mais de uma substring 'ROAD', você poderia recorrer a algo assim: apenas pesquise e substitua 'ROAD' nos últimos quatro caracteres do endereço (s[-4:]), e deixe a string sozinha (s[:-4]). Mas você pode ver que isso já está ficando complicado. Por exemplo, o padrão depende do comprimento da string que você está substituindo. (Se você estivesse substituindo 'STREET' com 'ST.', você precisaria usar s[:-6] e s[-6:].replace(...)). Você gostaria de voltar em seis meses e depurar isso? Eu sei que não.
  4. É hora de passar para as expressões regulares. Em Python, todas as funcionalidades relacionadas às expressões regulares estão contidas no módulo re.
  5. Dê uma olhada no primeiro parâmetro: 'ROAD$'. Esta é uma expressão regular simples que corresponde 'ROAD' apenas quando ocorre no final de uma string. O $ significa “fim da cadeia.” (Há um caractere correspondente, o circunflexo ^, que significa “início da string”). Usando a função re.sub(), você pesquisa a string s para a expressão regular 'ROAD$' e a substitui por 'RD.'. Corresponde ao ROAD no final da string s, mas não corresponde ao ROAD que é parte da palavra BROAD, porque está no meio de s.

^ corresponde ao início de uma string. $ corresponde ao final de uma string.

Continuando com minha história de limpeza de endereços, logo descobri que o exemplo anterior, correspondendo 'ROAD' no final do endereço, não era bom o suficiente, porque nem todos os endereços incluíam uma designação de rua. Alguns endereços simplesmente terminavam com o nome da rua. Eu fugia com isso na maioria das vezes, mas se o nome da rua fosse 'BROAD', a expressão regular corresponderia 'ROAD' ao final da string como parte da palavra 'BROAD', o que não é o que eu queria.

>>> s = '100 BROAD'
>>> re.sub('ROAD$', 'RD.', s)
'100 BRD.'
>>> re.sub('\\bROAD$', 'RD.', s)   ①
'100 BROAD'
>>> re.sub(r'\bROAD$', 'RD.', s)   ②
'100 BROAD'
>>> s = '100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD$', 'RD.', s)   ③
'100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD\b', 'RD.', s)  ④
'100 BROAD RD. APT 3'
  1. O que eu realmente queria era encontrar 'ROAD' quando estava no final da string e era sua própria palavra (e não uma parte de alguma palavra maior). Para expressar isso em uma expressão regular, você usa \b, que significa "um limite de palavra deve ocorrer bem aqui." Em Python, isso é complicado pelo fato de que o próprio caractere '\' em uma string deve ser ignorado. Isso às vezes é chamado de praga da barra invertida e é um dos motivos pelos quais as expressões regulares são mais fáceis em Perl do que em Python. Por outro lado, o Perl mistura expressões regulares com outra sintaxe, portanto, se você tiver um bug, pode ser difícil dizer se é um bug na sintaxe ou na sua expressão regular.
  2. Para contornar a praga da barra invertida, você pode usar o que é chamado de string bruta, prefixando a string com a letra r. Isso diz ao Python que nada nesta string deve ser ignorado; '\t' é um caractere de tabulação, mas na verdade r'\t' é o caractere de barra invertida \ seguido pela letra t. Eu recomendo sempre usar strings brutas ao lidar com expressões regulares; caso contrário, as coisas ficam muito confusas muito rapidamente (e as expressões regulares já são confusas o suficiente).
  3. *suspiro* Infelizmente, logo encontrei mais casos que contradiziam minha lógica. Nesse caso, o endereço da rua continha a palavra 'ROAD' como uma palavra inteira sozinha, mas não era no final, porque o endereço tinha um número de apartamento após a designação da rua. Como 'ROAD' não está bem no final da string, não corresponde, então toda a chamada para re.sub() acaba não substituindo nada, e você recebe a string original de volta, que não é o que você quer.
  4. Para resolver este problema, removi o caractere $ e adicionei outro \b. Agora, a expressão regular diz “corresponde 'ROAD' quando é uma palavra inteira sozinha em qualquer lugar da string”, seja no final, no início ou em algum lugar no meio.

Estudo de caso: números romanos

Você provavelmente já viu algarismos romanos, mesmo que não os tenha reconhecido. Você pode tê-los visto em direitos autorais de filmes antigos e programas de televisão (“Copyright MCMXLVI” em vez de “Copyright 1946”), ou nas paredes de dedicação de bibliotecas ou universidades (“estabelecido MDCCCLXXXVIII” em vez de “estabelecido 1888”). Você também pode tê-los visto em esboços e referências bibliográficas. É um sistema de representação de números que realmente data do antigo Império Romano (daí o nome).

Em algarismos romanos, existem sete caracteres que são repetidos e combinados de várias maneiras para representar números.

  • I = 1
  • V = 5
  • X = 10
  • L = 50
  • C = 100
  • D = 500
  • M = 1000

A seguir estão algumas regras gerais para a construção de algarismos romanos:

  • Às vezes, os caracteres são aditivos. I é 1, II é 2 e III é 3. VI é 6 (literalmente, “5 e 1”), VII é 7 e VIII é 8.
  • Os caracteres dezenas (I, X, C, e M) podem ser repetidos até três vezes. Em 4, você precisa subtrair do próximo caractere cincos mais alto. Você não pode representar 4 como IIII; em vez disso, é representado como IV (“1 menor que 5”). 40 é escrito como XL (“10 menos que 50”), 41 como XLI, 42 como XLII, 43 como XLIII e então 44 como XLIV (“10 menos que 50, então 1 menos que 5”).
  • Às vezes, os caracteres são... o oposto de aditivos. Ao colocar certos caracteres antes de outros, você subtrai do valor final. Por exemplo, em 9, você precisa subtrair do próximo caractere de dezenas mais alto: 8 é VIII, mas 9 é IX (“1 menor que 10”), não VIIII (já que o caractere I não pode ser repetido quatro vezes). 90 é XC, 900 é CM.
  • Os cinco caracteres não podem ser repetidos. 10 é sempre representado como X, nunca como VV. 100 é sempre C, nunca LL.
  • Os algarismos romanos são lidos da esquerda para a direita, portanto, a ordem dos caracteres é muito importante. DC é 600; CD é um número completamente diferente ( 400, “100 menor que 500”). CI é 101; IC não é nem mesmo um algarismo romano válido (porque você não pode subtrair 1 diretamente de 100; você precisaria escrevê-lo como XCIX “10menor que 100, então 1 menor que 10”).

Verificando Milhares

O que seria necessário para validar que uma string arbitrária é um numeral romano válido? Vamos pegar um dígito de cada vez. Como os algarismos romanos são sempre escritos da ordem superior para a inferior, vamos começar com a mais alta: a casa dos milhares. Para números 1000 e superiores, os milhares são representados por uma série de caracteres M.

>>> import re
>>> pattern = '^M?M?M?$'        ①
>>> re.search(pattern, 'M')     ②
<_sre.SRE_Match object at 0106FB58>
>>> re.search(pattern, 'MM')    ③
<_sre.SRE_Match object at 0106C290>
>>> re.search(pattern, 'MMM')   ④
<_sre.SRE_Match object at 0106AA38>
>>> re.search(pattern, 'MMMM')  ⑤
>>> re.search(pattern, '')      ⑥
<_sre.SRE_Match object at 0106F4A8>
  1. Esse padrão tem três partes. ^ corresponde ao que segue apenas no início da string. Se isso não fosse especificado, o padrão corresponderia independentemente de onde os caracteres M estivessem, o que não é o que você deseja. Você quer ter certeza de que os caracteres M, se estiverem lá, estão no início da string. M? opcionalmente, corresponde a um único caractere M. Como isso é repetido três vezes, você está combinando de zero a três caracteres M em uma linha. E $ corresponde ao final da string. Quando combinado com o caractere ^ no início, isso significa que o padrão deve corresponder a toda a string, sem outros caracteres antes ou depois dos caracteresb M.
  2. A essência do módulo re é a função search(), que usa uma expressão regular (padrão) e uma string ('M') para tentar fazer a correspondência com a expressão regular. Se uma correspondência for encontrada, search() retorna um objeto que possui vários métodos para descrever a correspondência; se nenhuma correspondência for encontrada, search() retorna None o valor nulo do Python. No momento, tudo o que você se importa é se o padrão corresponde, o que você pode dizer apenas olhando para o valor de retorno de search(). 'M' corresponde a esta expressão regular, porque a primeira correspondências opcionais de M foi encontrada e o segundo e terceiro caracteres opcionais M são ignorados.
  3. 'MM' corresponde porque o primeiro e o segundo caracteres opcionais M correspondem e o terceiro M é ignorado.
  4. 'MMM' corresponde porque todos os três caracteres M correspondem.
  5. 'MMMM' não corresponde. Todos os três caracteres M combinam, mas então a expressão regular insiste no final da string (por causa do caractere $), e a string ainda não termina (por causa do quarto M). Então search() retorna None.
  6. Curiosamente, uma string vazia também corresponde a essa expressão regular, uma vez que todos os caracteres M são opcionais.

Verificando Centenas

? torna um padrão opcional.

A casa das centenas é mais difícil do que a dos milhares, porque existem várias maneiras mutuamente exclusivas de expressá-la, dependendo de seu valor.

  • 100 = C
  • 200 = CC
  • 300 = CCC
  • 400 = CD
  • 500 = D
  • 600 = DC
  • 700 = DCC
  • 800 = DCCC
  • 900 = CM

Portanto, existem quatro padrões possíveis:

  • CM
  • CD
  • De zero a três caracteres C (zero se a casa das centenas for 0)
  • D, seguido de zero a três caracteres C

Os dois últimos padrões podem ser combinados:

  • um opcional D, seguido de zero a três caracteres C

Este exemplo mostra como validar a casa das centenas de um algarismo romano.

>>> import re
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)$'  ①
>>> re.search(pattern, 'MCM')             ②
<_sre.SRE_Match object at 01070390>
>>> re.search(pattern, 'MD')              ③
<_sre.SRE_Match object at 01073A50>
>>> re.search(pattern, 'MMMCCC')          ④
<_sre.SRE_Match object at 010748A8>
>>> re.search(pattern, 'MCMC')            ⑤
>>> re.search(pattern, '')                ⑥
<_sre.SRE_Match object at 01071D98>
  1. Este padrão começa igual ao anterior, verificando o início da string (^) e depois a casa dos milhares (M?M?M?). Em seguida, ele tem a nova parte, entre parêntesis, que define um conjunto de três padrões mutuamente exclusivos, separados por barras verticais: CM, CD, e D?C?C?C? (o que é um opcional D seguido por zero a três opcionais caracteres C). O analisador de expressão regular verifica cada um desses padrões em ordem (da esquerda para a direita), pega o primeiro que corresponde e ignora o resto.
  2. 'MCM' corresponde porque o primeiro M corresponde, o segundo e o terceiro caracteres M são ignorados e as CMcorrespondências (portanto, os padrões CDe D?C?C?C?nunca são considerados). MCMé a representação numérica romana de 1900.
  3. 'MD' corresponde porque o primeiro M corresponde, o segundo e o terceiro caracteres M são ignorados e o padrão D?C?C?C? corresponde a D (cada um dos três caracteres C são opcionais e são ignorados). MD é a representação em algarismo romano de 1500.
  4. 'MMMCCC' corresponde porque todos os três caracteres M correspondem e o padrão D?C?C?C? corresponde CCC (o D é opcional e é ignorado). MMMCCC é a representação numérica romana de 3300.
  5. 'MCMC' não corresponde. O primeiro Mcorresponde, o segundo e o terceiro caracteres M são ignorados e CM corresponde, mas o $ não corresponde porque você ainda não está no final da string (ainda possui um caractere C sem correspondência). O C que não corresponder como parte do padrão D?C?C?C?, porque o mutuamente exclusivos padrão CM já tem correspondente.
  6. Curiosamente, uma string vazia ainda corresponde a esse padrão, porque todos os caracteres M são opcionais e ignorados, e a string vazia corresponde ao padrão D?C?C?C? em que todos os caracteres são opcionais e ignorados.

Uau! Vê com que rapidez as expressões regulares podem ficar desagradáveis? E você cobriu apenas os milhares e centenas de casas de algarismos romanos. Mas se você seguiu tudo isso, as casas das dezenas e unidades são fáceis, porque são exatamente o mesmo padrão. Mas vamos examinar outra maneira de expressar o padrão.

Usando a sintaxe {n,m}

{1,4} corresponde entre 1 e 4 ocorrências de um padrão.

Na seção anterior, você estava lidando com um padrão em que o mesmo caractere poderia ser repetido até três vezes. Existe outra maneira de expressar isso em expressões regulares, que algumas pessoas consideram mais legíveis. Primeiro, olhe para o método que já usamos no exemplo anterior.

>>> import re
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'M')     ①
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, 'MM')    ②
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMM')   ③
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, 'MMMM')  ④
>>>
  1. Isso corresponde ao início da string e, em seguida, ao primeiro opcional M, mas não ao segundo e ao terceiro M (mas está tudo bem porque eles são opcionais) e, em seguida, ao final da string.
  2. Isso corresponde ao início da string e, em seguida, ao primeiro e ao segundo opcionais M, mas não ao terceiro M (mas está tudo bem porque é opcional) e, em seguida, ao final da string.
  3. Isso corresponde ao início da string, aos três opcionais M e ao final da string.
  4. Isso corresponde ao início da string e, em seguida, a todos os três opcionais M, mas não corresponde ao final da string (porque ainda há um M sem correspondência), portanto, o padrão não corresponde e retorna None.
>>> pattern = '^M{0,3}$'        ①
>>> re.search(pattern, 'M')     ②
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MM')    ③
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, 'MMM')   ④
<_sre.SRE_Match object at 0x008EEDA8>
>>> re.search(pattern, 'MMMM')  ⑤
>>>
  1. Este padrão diz: "Combine o início da string, em qualquer lugar de zero a três caracteres M e, a seguir, no final da string." O 0 e o 3 podem ser quaisquer números; se quiser combinar pelo menos um, mas não mais do que três caracteres M, você pode dizer M{1,3}.
  2. Isso corresponde ao início da string, depois a um M entre os três possíveis e, a seguir, ao final da string.
  3. Isso corresponde ao início da string, depois a dois M dos três possíveis e, a seguir, ao final da string.
  4. Isso corresponde ao início da string, depois a três M dos três possíveis e, a seguir, ao final da string.
  5. Isso corresponde ao início da string, depois a três M dos três possíveis, mas não corresponde ao final da string. A expressão regular permite até três caracteres M antes do final da string, mas você tem quatro, portanto, o padrão não corresponde e retorna None.

Verificando Dezenas e Uns

Agora, vamos expandir a expressão regular do numeral romano para cobrir a casa das dezenas e unidades. Este exemplo mostra a verificação de dezenas.

>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$'
>>> re.search(pattern, 'MCMXL')     ①
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCML')      ②
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLX')     ③
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXX')   ④
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXXX')  ⑤
>>> 
  1. Isso coincide com o início da string, então o primeiro opcional M e, em seguida CM, em seguida XL, em seguida, no final da string. Lembre-se de que a sintaxe (A|B|C) significa “corresponder exatamente a um de A, B ou C”. Você corresponde XL, então você ignorar as escolhas XC e L?X?X?X?, e depois passar para o fim da string. MCMXL é a representação numérica romana de 1940.
  2. Isso corresponde ao início da string, depois ao primeiro opcional M, então CM, então L?X?X?X?. Do L?X?X?X?, ele corresponde ao L e ignora todos os três caracteres X opcionais. Em seguida, você se move para o final da string. MCML é a representação numérica romana de 1950.
  3. Isso corresponde ao início da string, depois ao primeiro opcional e M, a seguir CM, ao opcional L e ao primeiro opcional X, ignora o segundo e o terceiro opcionais X, a seguir, o final da string. MCMLX é a representação numérica romana de 1960.
  4. Isso corresponde ao início da string, depois ao primeiro opcional M, CM depois ao opcional L e aos três caracteres opcionais X e, por fim, ao final da string. MCMLXXX é a representação numérica romana de 1980.
  5. Isso coincide com o início da string, depois com o primeiro M opcional, depois com CM, depois com o L opcional e todos os três caracteres X opcionais e, a seguir, falha em coincidir com o final da string porque ainda há mais um X não contabilizado. Portanto, todo o padrão falha em corresponder e retorna Nenhum. MCMLXXXX não é um numeral romano válido.

(A|B) corresponde ao padrão A ou ao padrão B, mas não aos dois.

A expressão para a casa de uns segue o mesmo padrão. Vou poupar você dos detalhes e mostrar o resultado final.

>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'

Então, como é usar essa sintaxe {n,m} alternativa? Este exemplo mostra a nova sintaxe.

>>> pattern = '^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'
>>> re.search(pattern, 'MDLV')              ①
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMDCLXVI')          ②
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMMDCCCLXXXVIII')   ③
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'I')                 ④
<_sre.SRE_Match object at 0x008EEB48>
  1. Isso corresponde ao início da string, depois a um dos três caracteres M possíveis e a D?C{0,3}. Disto, ele corresponde ao D opcional e zero dos três caracteres C possíveis. Continuando, ele corresponde a L?X{0,3} combinando o L opcional e zero dos três caracteres X possíveis. Em seguida, ele corresponde a V?I{0,3} combinando o V opcional e zero dos três caracteres I possíveis e, finalmente, o final da string. MDLV é a representação numeral romana de 1555.
  2. Isso corresponde ao início da string, depois a dois dos três caracteres M possíveis, a D?C{0,3} com um D e um dos três caracteres C possíveis; então L?X{0,3} com um L e um dos três caracteres X possíveis; então V?I{0,3} com um V e um dos três caracteres I possíveis; então o fim da string. MMDCLXVI é a representação numérica romana de 2666.
  3. Isso corresponde ao início da string, então três de três caracteres M, então D?C{0,3} com um D e três de três caracteres C; então L?X{0,3} com um L e três dos três caracteres de X; então V? I {0,3} com um V e três dos três caracteres I; então o fim da string. MMMDCCCLXXXVIII é a representação em algarismo romano de 3888 e é o algarismo romano mais longo que você pode escrever sem sintaxe estendida.
  4. Observe de perto. (Sinto-me um mágico. “Observem atentamente, crianças, vou tirar um coelho da cartola”). Isso corresponde ao início da string, depois a zero em três M, depois corresponde a D?C{0,3} ignorando o D opcional e combinando zero de três C, então casa L?X{0,3} ignorando o L opcional e combinando zero de três X, então casa V?I{0,3} pulando o V opcional e combinando um de três I. Em seguida, o fim da string. Uau.

Se você acompanhou tudo isso e entendeu na primeira tentativa, está se saindo melhor do que eu. Agora imagine tentar entender as expressões regulares de outra pessoa, no meio de uma função crítica de um grande programa. Ou até imagine voltar às suas próprias expressões regulares alguns meses depois. Eu fiz isso, e não é uma visão bonita.

Agora vamos explorar uma sintaxe alternativa que pode ajudar a manter suas expressões sustentáveis.

Expressões regulares detalhadas

Até agora, você lidou apenas com o que chamarei de expressões regulares “compactas”. Como você viu, eles são difíceis de ler e, mesmo que você descubra o que um deles faz, não há garantia de que será capaz de entendê-los seis meses depois. O que você realmente precisa é de documentação embutida.

Python permite que você faça isso com algo chamado expressões regulares detalhadas. Uma expressão regular detalhada é diferente de uma expressão regular compacta de duas maneiras:

  • O espaço em branco é ignorado. Espaços, tabulações e retornos de carro não são correspondidos como espaços, tabulações e retornos de carro. Eles não são compatíveis de forma alguma. (Se quiser corresponder a um espaço em uma expressão regular detalhada, você precisará escapar dele colocando uma barra invertida na frente dele).
  • Os comentários são ignorados. Um comentário em uma expressão regular detalhada é como um comentário no código Python: ele começa com um caractere # e vai até o final da linha. Neste caso, é um comentário dentro de uma string de várias linhas em vez de dentro do seu código-fonte, mas funciona da mesma maneira.

Isso ficará mais claro com um exemplo. Vamos revisitar a expressão regular compacta com a qual você está trabalhando e torná-la uma expressão regular detalhada. Este exemplo mostra como.

>>> pattern = '''
    ^                   # beginning of string
    M{0,3}              # thousands - 0 to 3 Ms
    (CM|CD|D?C{0,3})    # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 Cs),
                        #            or 500-800 (D, followed by 0 to 3 Cs)
    (XC|XL|L?X{0,3})    # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 Xs),
                        #        or 50-80 (L, followed by 0 to 3 Xs)
    (IX|IV|V?I{0,3})    # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 Is),
                        #        or 5-8 (V, followed by 0 to 3 Is)
    $                   # end of string
    '''
>>> re.search(pattern, 'M', re.VERBOSE)                 ①
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXXIX', re.VERBOSE)         ②
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMMDCCCLXXXVIII', re.VERBOSE)   ③
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'M')                             ④
  1. A coisa mais importante a se lembrar ao usar expressões regulares verbosas é que você precisa passar um argumento extra ao trabalhar com elas: re.VERBOSE é uma constante definida no módulo re que sinaliza que o padrão deve ser tratado como uma expressão regular detalhada. Como você pode ver, esse padrão tem um pouco de espaço em branco (todos eles são ignorados) e vários comentários (todos eles são ignorados). Depois de ignorar o espaço em branco e os comentários, esta é exatamente a mesma expressão regular que você viu na seção anterior, mas é muito mais legível.
  2. Isso corresponde ao início da string, depois a um dos três possíveis M, então CM, L e a três dos três possíveis X, então IX, ao final da string.
  3. Isso corresponde ao início da string, então três de três possíveis M, então D e três de um possível três C, então L e três de um possível três X, então V e três de um possível três I, então o final de a string.
  4. Isso não corresponde. Por quê? Como não tem o sinalizador re.VERBOSE, a função re.search está tratando o padrão como uma expressão regular compacta, com espaços em branco significativos e marcas de hash literais. Python não consegue detectar automaticamente se uma expressão regular é prolixa ou não. Python assume que toda expressão regular é compacta, a menos que você declare explicitamente que ela é detalhada.

Estudo de caso: análise de números de telefone

\d corresponde a qualquer dígito numérico (0–9). \D corresponde a qualquer coisa, exceto dígitos.

Até agora, você se concentrou em combinar padrões inteiros. Ou o padrão corresponde ou não. Mas as expressões regulares são muito mais poderosas do que isso. Quando uma expressão regular faz jogo, você pode escolher partes específicas do mesmo. Você pode descobrir o que combinou onde.

Este exemplo veio de outro problema do mundo real que encontrei, novamente em um trabalho do dia anterior. O problema: analisar um número de telefone americano. O cliente queria poder inserir o número de forma livre (em um único campo), mas depois queria armazenar o código de área, tronco, número e, opcionalmente, um ramal separadamente no banco de dados da empresa. Eu vasculhei a web e encontrei muitos exemplos de expressões regulares que pretendiam fazer isso, mas nenhuma delas era permissiva o suficiente.

Aqui estão os números de telefone de que eu precisava para aceitar:

  • 800-555-1212
  • 800 555 1212
  • 800.555.1212
  • (800) 555-1212
  • 1-800-555-1212
  • 800-555-1212-1234
  • 800-555-1212x1234
  • 800-555-1212 ext. 1234
  • work 1-(800) 555.1212 #1234

Bastante variedade! Em cada um desses casos, preciso saber se o código de área era 800, o tronco era 555 e o restante do número de telefone era 1212. Para quem tem uma extensão, preciso saber se a extensão era 1234.

Vamos trabalhar no desenvolvimento de uma solução para análise de número de telefone. Este exemplo mostra a primeira etapa.

>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})$')  ①
>>> phonePattern.search('800-555-1212').groups()             ②
('800', '555', '1212')
>>> phonePattern.search('800-555-1212-1234')                 ③
>>> phonePattern.search('800-555-1212-1234').groups()        ④
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'groups'
  1. Sempre leia as expressões regulares da esquerda para a direita. Este corresponde ao início da string, e então (\d{3}). O que é \d{3}? Bem, \d significa “qualquer dígito numérico” (0 a 9). O {3} no meio “corresponde exatamente a três dígitos numéricos”; é uma variação da sintaxe {n,m} que você viu antes. Colocar tudo entre parênteses significa “combine exatamente três dígitos numéricos, e então lembre-os como um grupo que eu posso pedir mais tarde ”. Em seguida, combine um hífen literal. Em seguida, combine outro grupo de exatamente três dígitos. Em seguida, outro hífen literal. Em seguida, outro grupo de exatamente quatro dígitos. Em seguida, combine o final da string.
  2. Para obter acesso aos grupos que o analisador de expressão regular lembrou ao longo do caminho, use o método groups() no objeto que o método search() retorna. Ele retornará uma tupla de quantos grupos foram definidos na expressão regular. Nesse caso, você definiu três grupos, um com três dígitos, um com três dígitos e um com quatro dígitos.
  3. Essa expressão regular não é a resposta final, porque não trata um número de telefone com um ramal no final. Para isso, você precisará expandir a expressão regular.
  4. E é por isso que você nunca deve “encadear” os métodos search() e groups() no código de produção. Se o método search() não retornar nenhuma correspondência, ele retornará None, não um objeto de correspondência de expressão regular. Chamar None.groups() levanta uma exceção perfeitamente óbvia: None não tem um método groups(). (Claro, é um pouco menos óbvio quando você obtém essa exceção nas profundezas do seu código. Sim, falo por experiência própria aqui).
>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})-(\d+)$')  ①
>>> phonePattern.search('800-555-1212-1234').groups()              ②
('800', '555', '1212', '1234')
>>> phonePattern.search('800 555 1212 1234')                       ③
>>> 
>>> phonePattern.search('800-555-1212')                            ④
>>>
  1. Esta expressão regular é quase idêntica à anterior. Assim como antes, você corresponde ao início da string, a um grupo lembrado de três dígitos, a um hífen, a um grupo de três dígitos lembrado, a um hífen e a um grupo de quatro dígitos lembrado. A novidade é que você combina outro hífen e um grupo lembrado de um ou mais dígitos e, em seguida, o final da string.
  2. O método groups() agora retorna uma tupla de quatro elementos, uma vez que a expressão regular agora define quatro grupos a serem lembrados.
  3. Infelizmente, essa expressão regular também não é a resposta final, porque presume que as diferentes partes do número de telefone estão separadas por hífens. E se eles estiverem separados por espaços, vírgulas ou pontos? Você precisa de uma solução mais geral para combinar vários tipos diferentes de separadores.
  4. Ops! Essa expressão regular não apenas não faz tudo o que você deseja, como também é um retrocesso, porque agora você não pode analisar números de telefone sem um ramal. Não era isso que você queria; se a extensão estiver lá, você quer saber o que é, mas se não estiver, você ainda quer saber quais são as diferentes partes do número principal.

O próximo exemplo mostra a expressão regular para lidar com separadores entre as diferentes partes do número de telefone.

>>> phonePattern = re.compile(r'^(\d{3})\D+(\d{3})\D+(\d{4})\D+(\d+)$')  ①
>>> phonePattern.search('800 555 1212 1234').groups()  ②
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212-1234').groups()  ③
('800', '555', '1212', '1234')
>>> phonePattern.search('80055512121234')              ④
>>> 
>>> phonePattern.search('800-555-1212')                ⑤
>>> 
  1. Segure seu chapéu. Você está combinando o início da string, um grupo de três dígitos, então \D+. Que raio é aquilo? Bem, \D corresponde a qualquer caractere, exceto um dígito numérico e + significa “1 ou mais”. Portanto, \D+ corresponde a um ou mais caracteres que não são dígitos. Isso é o que você está usando em vez de um hífen literal, para tentar combinar diferentes separadores.
  2. Usando \D+ em vez de - significa, agora você pode combinar números de telefone onde as partes são separadas por espaços em vez de hifens.
  3. É claro que os números de telefone separados por hífens também funcionam.
  4. Infelizmente, essa ainda não é a resposta final, porque pressupõe que haja um separador. E se o número de telefone for inserido sem nenhum espaço ou hífen?
  5. Ups! Isso ainda não resolveu o problema de exigir extensões. Agora você tem dois problemas, mas pode resolvê-los com a mesma técnica.

O próximo exemplo mostra a expressão regular para lidar com números de telefone sem separadores.

>>> phonePattern = re.compile(r'^(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  ①
>>> phonePattern.search('80055512121234').groups()      ②
('800', '555', '1212', '1234')
>>> phonePattern.search('800.555.1212 x1234').groups()  ③
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()        ④
('800', '555', '1212', '')
>>> phonePattern.search('(800)5551212 x1234')           ⑤
  1. A única alteração que você fez desde a última etapa foi alterar todos os + para *. Em vez de \D+ entre as partes do número de telefone, agora você combina em \D*. Lembra que + significa “1 ou mais”? Bem, *significa “zero ou mais”. Portanto, agora você deve ser capaz de analisar números de telefone mesmo quando não há nenhum caractere separador.
  2. Eis que realmente funciona. Por quê? Você combinou o início da string, depois um grupo lembrado de três dígitos (800), depois zero caracteres não numéricos, um grupo lembrado de três dígitos (555), depois zero caracteres não numéricos e um grupo lembrado de quatro dígitos (1212), em seguida, zero caracteres não numéricos, um grupo lembrado de um número arbitrário de dígitos (1234) e, em seguida, o final da string.
  3. Outras variações também funcionam agora: pontos em vez de hífens e um espaço e um x antes da extensão.
  4. Finalmente, você resolveu o outro problema antigo: as extensões são opcionais novamente. Se nenhuma extensão for encontrada, o método groups() ainda retorna uma tupla de quatro elementos, mas o quarto elemento é apenas uma string vazia.
  5. Odeio ser o portador de más notícias, mas você ainda não acabou. Qual é o problema aqui? Há um caractere extra antes do código de área, mas a expressão regular assume que o código de área é a primeira coisa no início da string. Não tem problema, você pode usar a mesma técnica de “zero ou mais caracteres não numéricos” para pular os caracteres iniciais antes do código de área.

O próximo exemplo mostra como lidar com os caracteres principais em números de telefone.

>>> phonePattern = re.compile(r'^\D*(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  ①
>>> phonePattern.search('(800)5551212 ext. 1234').groups()                  ②
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()                            ③
('800', '555', '1212', '')
>>> phonePattern.search('work 1-(800) 555.1212 #1234')                      ④
  1. Este é o mesmo que no exemplo anterior, exceto que agora você está combinando \D* zero ou mais caracteres não numéricos antes do primeiro grupo lembrado (o código de área). Observe que você não está se lembrando desses caracteres não numéricos (eles não estão entre parênteses). Se você encontrá-los, simplesmente os pule e comece a lembrar o código de área sempre que chegar a ele.
  2. Você pode analisar o número de telefone com êxito, mesmo com o primeiro parêntese esquerdo antes do código de área. (O parêntese direito após o código de área já foi tratado; é tratado como um separador não numérico e correspondido pelo \D* após o primeiro grupo lembrado).
  3. Apenas uma verificação de sanidade para ter certeza de que você não quebrou nada que funcionava antes. Como os caracteres iniciais são totalmente opcionais, isso corresponde ao início da string, depois a zero caracteres não numéricos, a um grupo lembrado de três dígitos (800), a um caractere não numérico (o hífen) e a um grupo de três lembrado dígitos (555), um caractere não numérico (o hífen), um grupo lembrado de quatro dígitos (1212), zero caracteres não numéricos, um grupo lembrado de zero dígitos e o final da string.
  4. É aqui que as expressões regulares me fazem querer arrancar os olhos com um objeto rombudo. Por que este número de telefone não corresponde? Porque existe um 1 antes do código de área, mas você presumiu que todos os caracteres iniciais antes do código de área eram caracteres não numéricos (\D*). Aargh.

Vamos voltar por um segundo. Até agora, todas as expressões regulares corresponderam desde o início da string. Mas agora você vê que pode haver uma quantidade indeterminada de coisas no início da string que você deseja ignorar. Em vez de tentar combinar tudo apenas para pular, vamos adotar uma abordagem diferente: não combine explicitamente o início da string. Essa abordagem é mostrada no próximo exemplo.

>>> phonePattern = re.compile(r'(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  ①
>>> phonePattern.search('work 1-(800) 555.1212 #1234').groups()         ②
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()                        ③
('800', '555', '1212', '')
>>> phonePattern.search('80055512121234').groups()                      ④
('800', '555', '1212', '1234')
  1. Observe a falta de ^ nesta expressão regular. Você não está mais correspondendo ao início da string. Não há nada que diga que você precisa combinar toda a entrada com sua expressão regular. O mecanismo de expressão regular fará o trabalho árduo de descobrir onde a string de entrada começa a corresponder e partir daí.
  2. Agora você pode analisar com êxito um número de telefone que inclui caracteres e um dígito à esquerda, além de qualquer número de qualquer tipo de separador em torno de cada parte do número de telefone.
  3. Verificação de sanidade. Isso ainda funciona.
  4. Isso ainda funciona também.

Vê com que rapidez uma expressão regular pode ficar fora de controle? Dê uma olhada rápida em qualquer uma das iterações anteriores. Você pode dizer a diferença entre um e o outro?

Enquanto você ainda entende a resposta final (e é a resposta final; se você descobriu um caso que não resolve, não quero saber sobre isso), vamos escrevê-la como uma expressão regular detalhada, antes você esquece por que fez as escolhas que fez.

>>> phonePattern = re.compile(r'''
        # don't match beginning of string, number can start anywhere
(\d{3})     # area code is 3 digits (e.g. '800')
\D*         # optional separator is any number of non-digits
(\d{3})     # trunk is 3 digits (e.g. '555')
\D*         # optional separator
(\d{4})     # rest of number is 4 digits (e.g. '1212')
\D*         # optional separator
(\d*)       # extension is optional and can be any number of digits
$           # end of string
''', re.VERBOSE)
>>> phonePattern.search('work 1-(800) 555.1212 #1234').groups()  ①
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212')                          ②
('800', '555', '1212', '')
  1. Além de estar espalhada por várias linhas, esta é exatamente a mesma expressão regular da última etapa, portanto, não é surpresa que analise as mesmas entradas.
  2. Verificação final de sanidade. Sim, ainda funciona. Você Terminou.

Resumo

Esta é apenas a ponta do iceberg do que as expressões regulares podem fazer. Em outras palavras, mesmo que você esteja completamente dominado por eles agora, acredite em mim, você não viu nada ainda.

Agora você deve estar familiarizado com as seguintes técnicas:

  • ^ corresponde ao início de uma string.
  • $ corresponde ao final de uma string.
  • \b corresponde a um limite de palavra.
  • \d corresponde a qualquer dígito numérico.
  • \D corresponde a qualquer caractere não numérico.
  • x? corresponde a um caractere x opcional (em outras palavras, corresponde a x zero ou uma vez).
  • x* corresponde a x zero ou mais vezes.
  • x+ corresponde x uma ou mais vezes.
  • x{n,m} corresponde a um caractere x pelo menos n vezes, mas não mais do que m vezes.
  • (a|b|c) corresponde exatamente a um de a, b ou c.
  • (x) em geral, é um grupo lembrado. Você pode obter o valor da correspondência usando o método groups() do objeto retornado por re.search.

As expressões regulares são extremamente poderosas, mas não são a solução correta para todos os problemas. Você deve aprender o suficiente sobre eles para saber quando são apropriados, quando resolverão seus problemas e quando causarão mais problemas do que soluções.

Esse artigo é uma tradução de um capítulo do livro "Dive Into Python 3" escrito por Mark Pilgrim. Você pode ler o livro desde o início em português clicando aqui.

Traduzido por Acervo Lima. O original pode ser acessado aqui.

Licença

0 comentários:

Postar um comentário