❝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.'
-
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 stringreplace()
. 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. -
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étodoreplace()
vê essas duas ocorrências e substitui cegamente as duas; enquanto isso, vejo meus endereços sendo destruídos. -
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 usars[:-6]
es[-6:].replace(...)
). Você gostaria de voltar em seis meses e depurar isso? Eu sei que não. -
É 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
. -
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çãore.sub()
, você pesquisa a strings
para a expressão regular'ROAD$'
e a substitui por'RD.'
. Corresponde aoROAD
no final da strings
, mas não corresponde aoROAD
que é parte da palavraBROAD
, porque está no meio des
.
^ 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'
-
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. -
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 verdader'\t'
é o caractere de barra invertida\
seguido pela letrat
. 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). -
*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 parare.sub()
acaba não substituindo nada, e você recebe a string original de volta, que não é o que você quer. -
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>
-
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 caracteresM
estivessem, o que não é o que você deseja. Você quer ter certeza de que os caracteresM
, se estiverem lá, estão no início da string.M?
opcionalmente, corresponde a um único caractereM
. Como isso é repetido três vezes, você está combinando de zero a três caracteresM
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 caracteresbM
. -
A essência do módulo
re
é a funçãosearch()
, 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()
retornaNone
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 desearch()
.'M'
corresponde a esta expressão regular, porque a primeira correspondências opcionais deM
foi encontrada e o segundo e terceiro caracteres opcionaisM
são ignorados. -
'MM'
corresponde porque o primeiro e o segundo caracteres opcionaisM
correspondem e o terceiroM
é ignorado. -
'MMM'
corresponde porque todos os três caracteresM
correspondem. -
'MMMM'
não corresponde. Todos os três caracteresM
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 quartoM
). Entãosearch()
retornaNone
. -
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 caracteresC
Os dois últimos padrões podem ser combinados:
-
um opcional
D
, seguido de zero a três caracteresC
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>
-
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
, eD?C?C?C?
(o que é um opcionalD
seguido por zero a três opcionais caracteresC
). 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. -
'MCM'
corresponde porque o primeiroM
corresponde, o segundo e o terceiro caracteresM
são ignorados e asCM
correspondências (portanto, os padrõesCD
eD?C?C?C?
nunca são considerados).MCM
é a representação numérica romana de1900
. -
'MD'
corresponde porque o primeiroM
corresponde, o segundo e o terceiro caracteresM
são ignorados e o padrãoD?C?C?C?
corresponde aD
(cada um dos três caracteresC
são opcionais e são ignorados).MD
é a representação em algarismo romano de 1500. -
'MMMCCC'
corresponde porque todos os três caracteresM
correspondem e o padrãoD?C?C?C?
correspondeCCC
(oD
é opcional e é ignorado).MMMCCC
é a representação numérica romana de3300
. -
'MCMC'
não corresponde. O primeiroM
corresponde, o segundo e o terceiro caracteresM
são ignorados eCM
corresponde, mas o$
não corresponde porque você ainda não está no final da string (ainda possui um caractereC
sem correspondência). OC
que não corresponder como parte do padrãoD?C?C?C?
, porque o mutuamente exclusivos padrãoCM
já tem correspondente. -
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ãoD?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') ④
>>>
-
Isso corresponde ao início da string e, em seguida, ao primeiro opcional
M
, mas não ao segundo e ao terceiroM
(mas está tudo bem porque eles são opcionais) e, em seguida, ao final da string. -
Isso corresponde ao início da string e, em seguida, ao primeiro e ao segundo opcionais
M
, mas não ao terceiroM
(mas está tudo bem porque é opcional) e, em seguida, ao final da string. -
Isso corresponde ao início da string, aos três opcionais
M
e ao final da string. -
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á umM
sem correspondência), portanto, o padrão não corresponde e retornaNone
.
>>> 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') ⑤
>>>
-
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 caracteresM
, você pode dizerM{1,3}
. -
Isso corresponde ao início da string, depois a um
M
entre os três possíveis e, a seguir, ao final da string. -
Isso corresponde ao início da string, depois a dois
M
dos três possíveis e, a seguir, ao final da string. -
Isso corresponde ao início da string, depois a três
M
dos três possíveis e, a seguir, ao final da string. -
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 caracteresM
antes do final da string, mas você tem quatro, portanto, o padrão não corresponde e retornaNone
.
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') ⑤
>>>
-
Isso coincide com o início da string, então o primeiro opcional
M
e, em seguidaCM
, em seguidaXL
, 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ê correspondeXL
, então você ignorar as escolhasXC
eL?X?X?X?
, e depois passar para o fim da string.MCMXL
é a representação numérica romana de1940
. -
Isso corresponde ao início da string, depois ao primeiro opcional
M
, entãoCM
, entãoL?X?X?X?
. DoL?X?X?X?
, ele corresponde aoL
e ignora todos os três caracteresX
opcionais. Em seguida, você se move para o final da string.MCML
é a representação numérica romana de1950
. -
Isso corresponde ao início da string, depois ao primeiro opcional e
M
, a seguirCM
, ao opcionalL
e ao primeiro opcionalX
, ignora o segundo e o terceiro opcionaisX
, a seguir, o final da string.MCMLX
é a representação numérica romana de1960
. -
Isso corresponde ao início da string, depois ao primeiro opcional
M
,CM
depois ao opcionalL
e aos três caracteres opcionaisX
e, por fim, ao final da string.MCMLXXX
é a representação numérica romana de1980
. -
Isso coincide com o início da string, depois com o primeiro
M
opcional, depois comCM
, depois com oL
opcional e todos os três caracteresX
opcionais e, a seguir, falha em coincidir com o final da string porque ainda há mais umX
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>
-
Isso corresponde ao início da string, depois a um dos três caracteres
M
possíveis e aD?C{0,3}
. Disto, ele corresponde aoD
opcional e zero dos três caracteresC
possíveis. Continuando, ele corresponde aL?X{0,3}
combinando oL
opcional e zero dos três caracteresX
possíveis. Em seguida, ele corresponde aV?I{0,3}
combinando oV
opcional e zero dos três caracteresI
possíveis e, finalmente, o final da string.MDLV
é a representação numeral romana de 1555. -
Isso corresponde ao início da string, depois a dois dos três caracteres
M
possíveis, aD?C{0,3}
com umD
e um dos três caracteresC
possíveis; entãoL?X{0,3}
com umL
e um dos três caracteresX
possíveis; entãoV?I{0,3}
com umV
e um dos três caracteresI
possíveis; então o fim da string.MMDCLXVI
é a representação numérica romana de 2666. -
Isso corresponde ao início da string, então três de três caracteres
M
, entãoD?C{0,3}
com umD
e três de três caracteresC
; entãoL?X{0,3}
com umL
e três dos três caracteres deX
; 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. -
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 aD?C{0,3}
ignorando oD
opcional e combinando zero de trêsC
, então casaL?X{0,3}
ignorando oL
opcional e combinando zero de trêsX
, então casaV?I{0,3}
pulando oV
opcional e combinando um de trêsI
. 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') ④
-
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ódulore
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. -
Isso corresponde ao início da string, depois a um dos três possíveis
M
, entãoCM
,L
e a três dos três possíveisX
, entãoIX
, ao final da string. -
Isso corresponde ao início da string, então três de três possíveis
M
, entãoD
e três de um possível trêsC
, entãoL
e três de um possível trêsX
, entãoV
e três de um possível trêsI
, então o final de a string. -
Isso não corresponde. Por quê? Como não tem o sinalizador
re.VERBOSE
, a funçãore.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'
-
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. -
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étodosearch()
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. - 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.
-
E é por isso que você nunca deve “encadear” os métodos
search()
egroups()
no código de produção. Se o métodosearch()
não retornar nenhuma correspondência, ele retornaráNone
, não um objeto de correspondência de expressão regular. ChamarNone.groups()
levanta uma exceção perfeitamente óbvia:None
não tem um métodogroups()
. (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') ④
>>>
- 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.
-
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. - 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.
- 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') ⑤
>>>
-
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. -
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. - É claro que os números de telefone separados por hífens também funcionam.
- 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?
- 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') ⑤
-
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. -
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. -
Outras variações também funcionam agora: pontos em vez de hífens e um espaço e um
x
antes da extensão. -
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. - 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') ④
-
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. -
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). -
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. -
É 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')
-
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í. - 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.
- Verificação de sanidade. Isso ainda funciona.
- 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', '')
- 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.
- 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 caracterex
opcional (em outras palavras, corresponde ax
zero ou uma vez).x*
corresponde ax
zero ou mais vezes.x+
correspondex
uma ou mais vezes.x{n,m}
corresponde a um caracterex
pelo menosn
vezes, mas não mais do quem
vezes.(a|b|c)
corresponde exatamente a um dea
,b
ouc
.(x)
em geral, é um grupo lembrado. Você pode obter o valor da correspondência usando o métodogroups()
do objeto retornado porre.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.
0 comentários:
Postar um comentário