Expressões regulares (chamadas também de regex que vem de regular expressions) são uma linguagem que detecta padrões. Você expressa um padrão (pattern em inglês) que será aplicado a strings. É uma forma eficiente de encontrar padrões e extrair informações de textos. Nesse padrão, cada símbolo representa um tipo de informação e, nesse artigo, veremos os principais símbolos e seus significados com o auxílio de diversos exemplos.
O Python possui o módulo re para expressões regulares. A
documentação é bem detalhada e
ainda há esse texto com uma introdução mais passo a passo do
módulo.
O módulo possui três métodos que são muito utilizados e que serão nosso foco
aqui: match, search e findall.
Match #
O match procura pelo padrão fornecido desde o início da string. A assinatura é
re.match(padrão, string, flags=0). O retorno é None quando não encontra o
padrão. Quando encontra, retorna um Match object sobre o qual falaremos no
decorrer do artigo.
Vamos começar procurando o padrão “Fran” na string “Francisco Bustamante”, autor do site:
>>> import re
>>> re.match('Fran', 'Francisco Bustamante')
<re.Match object; span=(0, 4), match='Fran'>
>>> re.match('Fran', 'Bustamante, Francisco')
# repare que o último retorna None pois não encontra no início
O span=(0, 4) significa que o padrão foi encontrado entre os índices 0 e 4.
Lembre que a contagem em sequências Python começa em zero e que o último índice
é excluso.
Search #
O search procura pelo padrão fornecido ao longo da string. A assinatura é
re.search(padrão, string, flags=0). O retorno é None quando não encontra o
padrão. Quando encontra, retorna um Match object sobre o qual falaremos no
decorrer do artigo. Importante resaltar que retornará a primeira localização
onde o padrão foi encontrado.
>>> re.search('an', 'Francisco Bustamante')
<re.Match object; span=(2, 4), match='an'>
O padrão an aparece na string mais de uma vez, no entanto, o search retorna
em span apenas os índices da primeira localização.
Findall #
O findall procura pelo padrão fornecido ao longo da string. A assinatura é
re.findall(padrão, string, flags=0). O retorno é uma lista vazia quando não
encontra o padrão. Quando encontra, retorna uma lista com cada ocorrência.
Importante ressaltar que retornará todas as ocorrências encontradas.
>>> re.findall('an', 'Francisco Bustamante')
['an', 'an']
>>> re.findall('an', 'Chico')
[]
O poder do findall será melhor explorado com algumas ferramentas que
aprenderemos no decorrer do artigo.
Sinalizadores #
A assinatura dos métodos apresentados possuem um argumento flags, que pode ser traduzido como sinalizadores. Com sinalizadores podemos modificar alguns aspectos de como as expressões regulares funcionam. Veja a diferença de comportamento nos dois exemplos a seguir:
>>> re.match('fran', 'Francisco Bustamante')
>>> re.match('fran', 'Francisco Bustamante', re.IGNORECASE)
<re.Match object; span=(0, 4), match='Fran'>
No primeiro caso, o retorno foi None, de forma que o interpretador apenas
apresentou uma linha vazia. Isso porque o padrão foi passado com a letra inicial
minúscula e, na string, ela se apresenta maiúscula. O sinalizador
re.IGNORECASE, como o nome sugere, desconsidera diferenciação entre maiúsculo
e minúsculo.
A seguir, uma tabela com os principais sinalizadores do módulo re e seus
significados.
As letras após cada nome são as abreviações que podem ser utilizadas no lugar do
nome completo.
| Flag | Significado |
|---|---|
| ASCII, A | Considera caracteres de escape como \w, \b, \s e \d apenas em caracteres ASCII |
| DOTALL, S | Faz com que o metacaractere . encontre qualquer caractere, incluindo novas linhas |
| IGNORECASE, I | Faz combinações sem diferenciar maiúsculo de minúsculo |
| LOCALE, L | Faz uma correspondência considerando a localidade |
| MULTILINE, M | Correspondência multilinha, afetando ^ e $ |
| VERBOSE, X (de ‘extended’) | Habilita expressões regulares detalhadas, que podem ser organizadas de forma mais clara e compreensível |
Apareceram algumas palavras novas nessa tabela. Não se preocupe, elas serão explicadas no decorrer do artigo. Uma das palavras é metacaractere, nosso próximo tópico.
O poder das expressões regulares vem dos metacaracteres, caracteres que representam um conjunto de caracteres específicos, padrões gerais.
O metacaractere ponto (.)
#
O metacaractere . representa qualquer caractere, exceto quebra de linha. Vamos
utilizá-lo com o método match visto anteriormente:
>>> re.match('.', 'Francisco Bustamante')
<re.Match object; span=(0, 1), match='F'>
>>> re.match('.', '42')
<re.Match object; span=(0, 1), match='4'>
>>> re.match('.', ' Francisco Bustamante')
<re.Match object; span=(0, 1), match=' '>
Repare no último exemplo que havia um espaço no início da string e esse espaço foi reconhecido pelo metacaractere.
De acordo com a definição, o metacaractere . deve considerar caracteres de
controle exceto o \n, que
indica quebra de linha. Vejamos:
>>> re.match('.', '\t\t') # \t representa TAB
<re.Match object; span=(0, 1), match='\t'>
>>> re.match('.', '\n')
>>> print(re.match('.', '\n'))
None
Esse comportamento de ignorar \n pode ser modificado por um dos sinalizadores
que vimos anteriormente, o DOTALL:
>>> re.match('.', '\n', re.DOTALL)
<re.Match object; span=(0, 1), match='\n'>
Vimos anteriormente que o search busca o padrão no decorrer da string e
retorna a primeira posição onde encontra. Vejamos o comportamento com .:
>>> re.search('.', ' Francisco Bustamante')
<re.Match object; span=(0, 1), match=' '>
>>> re.search('.', 'Francisco Bustamante')
<re.Match object; span=(0, 1), match='F'>
>>> re.search('.', '\nFrancisco Bustamante')
<re.Match object; span=(1, 2), match='F'>
>>> re.search('.', '\nFrancisco Bustamante', re.DOTALL)
<re.Match object; span=(0, 1), match='\n'>
Os dois primeiros exemplos retornam a primeira posição, conforme esperado. No
terceiro exemplo, o caractere de controle \n é ignorado, retornando o primeiro
caractere logo após, a letra F. Esse comportamento é modificado pelo sinalizador
DOTALL no último exemplo.
Vimos anteriormente que o findall procura pelo padrão fornecido ao longo da
string, retornando uma lista com todas as ocorrências. Vamos combiná-lo com .:
>>> re.findall('.', 'Ciência\nProgramada')
['C', 'i', 'ê', 'n', 'c', 'i', 'a', 'P', 'r', 'o', 'g', 'r', 'a', 'm', 'a', 'd', 'a']
O caractere de controle \n foi ignorado e todos os demais foram retornados na
forma de lista.
Âncoras #
O símbolo ^ representa início de string e o símbolo $ representa final de
string. Vamos testar com o método findall:
>>> re.findall('^.', 'Ciência\nProgramada\nPython')
['C']
>>> re.findall('^.', 'Ciência\nProgramada\nPython', re.MULTILINE)
['C', 'P', 'P']
O padrão ^. significa procurar por qualquer caractere que não seja uma quebra
de linha no início da string. Assim, no primeiro caso, retorna a letra C. Mas
repare que a string passada possui quebras de linha. Logo, podemos passar o
sinalizador re.MULTILINE para que o padrão seja procurado em cada linha. Por
isso o segundo exemplo retorna uma lista com a primeira letra da string e,
também, com a primeira letra após o caractere de controle \n.
Podemos aplicar a mesma lógica para o padrão .$, que buscará no final da
string:
>>> re.findall('.$', 'Ciência\nProgramada\nPython')
['n']
>>> re.findall('.$', 'Ciência\nProgramada\nPython', re.MULTILINE)
['a', 'a', 'n']
Alguns casos limite que podemos analisar é quando temos uma string com apenas um caractere, ou vazia ou com uma quebra de linha:
>>> re.match('^.$', 'a')
<re.Match object; span=(0, 1), match='a'>
>>> re.match('^$', '') # o início ser igual ao fim, string vazia
<re.Match object; span=(0, 0), match=''>
>>> re.findall('^$', '\n', re.MULTILINE)
['', '']
O metacaractere . é muito abrangente, usualmente queremos ser um pouco mais
específicos.
Classes de caracteres #
Quando o padrão apresenta colchetes, estes declaram um classe de caracteres. Irá
se buscar cada caractere entre colchetes no texto da string. Vamos procurar por
vogais minúsculas na string Ciência Programada:
>>> re.findall('[aeiou]', 'Ciência Programada')
['i', 'i', 'a', 'o', 'a', 'a', 'a']
Observe que o ê não foi colocado na lista, afinal é diferente de e.
O símbolo ^ quando dentro de um classe de caractere significa negação. Logo,
se estamos buscando tudo menos vogais minúsculas:
>>> re.findall('[^aeiou]', 'Ciência Programada')
['C', 'ê', 'n', 'c', ' ', 'P', 'r', 'g', 'r', 'm', 'd']
É possível também definir ranges, faixas, de valores. Buscando de “a” até “f”:
>>> re.findall('[a-f]', 'Ciência Programada')
['c', 'a', 'a', 'a', 'd', 'a']
Podemos definir mais de uma faixa. Buscando de “a” até “f” e de “A” até “Z”:
>>> re.findall('[a-fA-Z]', 'Ciência Programada')
['C', 'c', 'a', 'P', 'a', 'a', 'd', 'a']
Um padrão bem comum é buscar por todas as letras e dígitos e por “_”, já que costumam ser os caracteres mais aceitos em campos de formulário online como e-mail, por exemplo:
>>> re.findall('[a-zA-Z0-9_]', 'Ciência Programada')
['C', 'i', 'n', 'c', 'i', 'a', 'P', 'r', 'o', 'g', 'r', 'a', 'm', 'a', 'd', 'a']
É uma sequência tão especial que há um atalho, o \w:
>>> re.findall('\w', 'Ciência Programada')
['C', 'i', 'ê', 'n', 'c', 'i', 'a', 'P', 'r', 'o', 'g', 'r', 'a', 'm', 'a', 'd', 'a']
Observe duas pequenas diferenças deste resultado com relação ao exemplo
anterior. Primeiro, não é necessário os colchetes para utilizar o \w. Segundo,
o caractere ê aparece no resultado. Ou seja, aqui se reconhece caracteres
unicode. Caso realmente queira apenas o
equivalente à classe [a-zA-Z0-9_], utilize o sinalizador re.ASCII:
>>> re.findall('\w', 'Ciência Programada', re.ASCII)
['C', 'i', 'n', 'c', 'i', 'a', 'P', 'r', 'o', '
g', 'r', 'a', 'm', 'a', 'd', 'a']
As principais sequências especiais definidas por padrão são:
\dequivalente a qualquer dígito unicode, o que inclui[0-9]\Dequivalente a negação de\d\sequivalente a caracteres de espaço em branco em unicode, o que inclui[ \t\n\r\f\v]\Sequivalente a negação de\s\wequivalente a caracteres que podem ser utilizados em textos em geral, o que inclui[a-zA-Z0-9_]\Wequivalente a negação de\w
Quando se usa o sinalizador re.ASCII, cada caso anterior fica restrito à
representação em colchete apresentada. Em unicode são mais abrangentes.
Raw strings #
Todas as sequências especiais utilizam o símbolo \. Isso é problemático em
Python, pois strings aceitam caracteres de controle na linguagem. Strings
literais avaliam o caractere após a barra invertida para verificar se é o caso
de um caractere de controle ou não. Vamos ver um exemplo:
>>> print('1\n2')
1
2
Quando queremos indicar que não é para considerar como um caractere de controle, utilizamos uma outra barra invertida para sinalizar escape:
>>> print('1\\n2') # escape da segunda barra invertida
1\n2
Esse comportamento relacionado a barras invertidas pode ser muito problemático
em alguns contextos. Por exemplo, na área científica se usa muito LaTeX para
produção de documentos. E os ambientes em LaTeX são delimitados com comandos que
utilizam \. Por exemplo, \begin{equation}...\end{equation} delimita um
ambiente para uma equação matemática. O comando \section indica o início de
uma seção em um documento. Vamos ver como o interpretador Python reconhece o
comando seguido de uma quebra de linha:
>>> '\section\n' # \s não tem significado em Python
'\\section\n'
>>> text = '\section\n' # LaTeX
>>> print(text)
\section
Repare que, como \s não tem significado para o interpretador Python,
automaticamente ele adiciona uma barra invertida para sinalizar que deve se
ignorar tal caractere. Ao usar print verificamos que aparece normalmente o
texto do comando e uma linha em branco.
Podemos verificar que caracteres de controle são considerados como um único
caractere verificando o tamanho da string armazenada na variável text:
>>> len(text)
9
Temos 8 caracteres em “\section” e o nono é o caractere de controle \n.
Teoricamente, deveria haver match ao se buscar por \\section na string
armazenada em text:
>>> print(re.match('\\section', text)) # \s é uma sequência especial em regex
None
Mas, como vimos na seção anterior, \s possui significado dentro do contexto do
módulo re. Assim, precisamos indicar que é para dar escape nas barras
invertidas:
>>> print(re.match('\\\\section', text))
<re.Match object; span=(0, 8), match='\\section'>
Bem-vindo ao que chamamos de a praga da barra invertida! Mas calma, há como
evitar esse monte de barras. Em Python, há o que chamamos de strings cruas ou
raw strings do inglês. Nesse tipo de strings, denotadas por um r antes das
aspas, as barras invertidas não são tratadas de nenhuma forma especial. Perceba
a diferença:
>>> len('\n')
1
>>> len(r'\n')
2
Na raw string, temos dois caracteres, o “" e o “n”, enquanto que na string normal (literal), há apenas um caractere que representa quebra de linha.
Voltando ao nosso exemplo, basta utilizar raw string no padrão:
>>> print(re.match(r'\\section', text)) # raw string, significa que não há caractere de controle
<re.Match object; span=(0, 8), match='\\section'>
Assim, uma dica muito importante: utilize raw strings quando houver barra invertida que não deve ser interpretada como sequência especial.
Uso de pipe #
O pipe | significa ou indicando alternativas no uso da expressão regular.
Veja exemplos:
>>> re.search('a|b', 'abc')
<re.Match object; span=(0, 1), match='a'>
>>> re.search('a|b', 'bcd')
<re.Match object; span=(0, 1), match='b'>
>>> re.search('a|b', 'cde')
Observe no primeiro exemplo que, mesmo existindo “b” na string, como o “a” foi
encontrado primeiro apenas a posição de “a” foi retornada. Com findall, ambos
são retornados:
>>> re.findall('b|a', 'abc')
['a', 'b']
Repetições #
Buscar repetições é um dos motivos mais comuns que levam ao uso de expressões regulares. Vamos verificar as diversas formas possíveis.
Quantidades especificas #
Vamos verificar o comportamento da busca pelo padrão \d{4} que busca qualquer
dígito quatro vezes.
>>> re.match(r'\d{4}', '1234') # string com 4 dígitos
<re.Match object; span=(0, 4), match='1234'>
>>> re.match(r'\d{4}', '123') # string com 3 dígitos
>>> re.match(r'\d{4}', '12345') # string com 5 dígitos
<re.Match object; span=(0, 4), match='1234'>
Nos exemplos, fica claro que, quando há menos de quatro dígitos, o retorno é
None. Nos demais casos, retorna os quatro primeiros dígitos. O mesmo ocorre
com o uso do método search:
>>> re.search(r'\d{4}', 'abc123def12345')
<re.Match object; span=(9, 13), match='1234'>
O primeiro seguimento 123 foi ignorado por ter menos de 4 dígitos. O segundo
seguimento, que possui 5 dígitos, foi considerado e retornou os quatro primeiros
dígitos.
Quantidade mínima e máxima #
O uso de vírgula indica que o valor a ser procurado é o mínimo. Ou seja, se
houver mais dígitos o comportamento será ganancioso (tradução usual de greed,
sendo também comum a tradução “guloso”) e retornará os demais dígitos além do
mínimo. Havendo menos dígitos que o mínimo, o retorno será None.
>>> re.match(r'\d{2,}', '12')
<re.Match object; span=(0, 2), match='12'>
>>> re.match(r'\d{2,}', '12345') # guloso ou ganancioso (greed)
<re.Match object; span=(0, 5), match='12345'>
>>> re.match(r'\d{2,}', '1')
Utilizar o símbolo ? após as chaves transforma o comportamento em preguiçoso,
sendo portanto tal símbolo um modificador de repetição.
>>> re.match(r'\d{2,}?', '12345') # preguiçoso, mínimo possível
<re.Match object; span=(0, 2), match='12'>
Um valor após a vírgula, indica valor máximo. As demais explicações anteriores seguem válidas.
>>> re.match(r'\d{2,4}', '12345')
<re.Match object; span=(0, 4), match='1234'>
>>> re.match(r'\d{2,4}', '123')
<re.Match object; span=(0, 3), match='123'>
>>> re.match(r'\d{2,4}', '12')
<re.Match object; span=(0, 2), match='12'>
>>> re.match(r'\d{2,4}', '1')
>>> re.match(r'\d{2,4}?', '12345') # ganancioso para preguiçoso
<re.Match object; span=(0, 2), match='12'>
0 ou 1 ocorrencia, elemento opcional #
A busca por um elemento opcional na string é, na realidade, um caso especial de quantidade mínima e máxima, com mínimo de zero e máximo de um.
>>> re.match(r'\d{0,1}', '12345')
<re.Match object; span=(0, 1), match='1'>
>>> re.match(r'\d{,1}', '12345') # 0 pode ser omitido
<re.Match object; span=(0, 1), match='1'>
O símbolo ? após uma expressão regular tem o mesmo efeito que {,1}. Logo:
>>> re.match(r'\d?', '12345')
<re.Match object; span=(0, 1), match='1'>
Mas já vimos que o mesmo símbolo transforma a busca de gananciosa em preguiçosa. Assim, a seguinte expressão retorna uma string vazia, pois o mínimo é de nenhuma (0) ocorrência:
>>> re.match(r'\d??', '12345')
<re.Match object; span=(0, 0), match=''>
Vamos por partes. O primeiro sinal de interrogação é um modificador de repetição
da expressão regular imediatamente anterior, buscando 0 ou 1 ocorrência de \d.
O segundo sinal de interrogação é um modificador do operador de repetição,
transformando-o em preguiçoso.
0 ou mais vezes #
Outro caso especial de mínimo e máximo. Também possui um símbolo especial, o
*. Observe os exemplos:
>>> re.match(r'\d{0,}', '12345')
<re.Match object; span=(0, 5), match='12345'>
>>> re.match(r'\d{,}', '12345') # 0 pode ser omitido
<re.Match object; span=(0, 5), match='12345'>
>>> re.match(r'\d*', '12345') # símbolo
<re.Match object; span=(0, 5), match='12345'>
>>> re.match(r'\d*?', '12345') # preguiçoso
<re.Match object; span=(0, 0), match=''>
>>> re.match(r'\d*', 'abc')
<re.Match object; span=(0, 0), match=''>
No último exemplo, retorna uma string vazia no match, pois o mínimo é de nenhuma ocorrência.
1 ou mais vezes #
Outro caso especial de mínimo e máximo. Também possui um símbolo especial, o
+. Observe os exemplos:
>>> re.match(r'\d{1,}', '12345')
<re.Match object; span=(0, 5), match='12345'>
>>> re.match(r'\d+', '12345') # + exige no mínimo uma ocorrência, sendo ganancioso
<re.Match object; span=(0, 5), match='12345'>
>>> re.match(r'\d+?', '12345') # transforma em preguiçoso
<re.Match object; span=(0, 1), match='1'>
>>> re.match(r'\d+', 'abc')
No último exemplo, retorna None, pois o mínimo é de uma ocorrência e não há
dígitos na string.
Entendendo a importância de controle de repetições #
Caso tudo tenha parecido muito abstrato até o momento, vamos começar a colocar algumas situações mais próximas ao real. Considere a string abaixo da qual gostaríamos de extrair todos os valores dos atributos.
>>> text = 'nome="Francisco" site="Ciência Programada"'
Como os valores estão entre aspas duplas, poderíamos imaginar num primeiro
momento o padrão r'".+"', já que . pegaria os caracteres com o modificador
+ indicando uma ou mais vezes. Mas veja o resultado:
>>> re.findall(r'".+"', text)
['"Francisco" site="Ciência Programada"']
Não funcionou pois colocamos qualquer coisa que está entre aspas com pelo menos uma ocorrência. A busca se estende da primeira aspa dupla até a última. Na realidade, queremos o que está entre cada par de aspas. Logo, devemos transformar a busca em preguiçosa:
>>> re.findall(r'".+?"', text)
['"Francisco"', '"Ciência Programada"']
Agora temos o que queríamos. Mas ainda não é uma boa forma de obter os valores. Observe o que ocorreria se fossem passados campos vazios:
>>> text = 'nome="" site=""'
>>> re.findall(r'".+?"', text)
['"" site="']
Retorna errado pois o + exige no mínimo uma ocorrência. Na realidade, queremos
0 ou mais ocorrências, o que podemos resolver utilizando *:
>>> re.findall(r'".*?"', text)
['""', '""']
Entendendo o Match Object #
Em diversos exemplos vimos que o resultado é um Match object. Vamos entender um pouco melhor esse objeto. Há quatro métodos importantes pro nível desse artigo:
>>> m = re.match(r'\d+', '12345')
>>> type(m)
re.Match
>>> m.group() # retorna a string em que foi feito match
'12345'
>>> m.start() # posição inicial do match
0
>>> m.end() # posição final do match
5
>>> m.span() # tupla com a posição inicial e final do match
(0, 5)
O método group será importante para a próxima seção.
Grupos de captura #
Por vezes a string analisada possui diversos campos dos quais se deseja extrair
dados. Podemos então definir grupos de captura. Os grupos são demarcados por
parênteses e podemos repetir o conteúdo de um grupo com um qualificador de
repetição, como *, +, ?, ou {m,n}. Por exemplo, (ab)* irá corresponder
a zero ou mais repetições de ab.
Considere a tag de HTML a seguir da qual queremos extrair o nome (input) e os
valores de type, id e name. Podemos criar um padrão, variável pattern no
código, seguindo o formato da string. Em cada grupo queremos pegar uma ou mais
ocorrências de caracteres e de forma preguiçosa, para não ocorrer os problemas
vistos na seção de controle de repetições:
>>> html = '<input type="text" id="id_cpf" name="cpf">'
>>> pattern = r'<(.+?) type="(.+?)" id="(.+?)" name="(.+?)"'
# qualquer caractere antes de um espaço para o nome da tag, idem entre aspas para os demais grupos
>>> m = re.match(pattern, html) # armazenando resultado do match em variável
>>> m
<re.Match object; span=(0, 41), match='<input type="text" id="id_cpf" name="cpf"'>
A variável m armazena o resultado, sendo um Match object. Por padrão,
apresenta o match completo realizado, mas podemos explorar mais detalhes do
objeto. O método groups apresenta todos grupos extraídos e o group permite
retornar grupos específicos:
>>> m.groups() # grupos de captura
('input', 'text', 'id_cpf', 'cpf')
>>> m.group(0) # match inteiro
'<input type="text" id="id_cpf" name="cpf"'
>>> m.group(1) # primeiro grupo
'input'
>>> m.group(2, 1, 3) # grupos específicos
('text', 'input', 'id_cpf')
Suponha agora que pode ocorrer mudança na ordem dos atributos da tag HTML:
>>> html1 = '<input type="text" id="id_cpf" name="cpf">'
>>> html2 = '<input id="id_cpf" name="cpf" type="text">'
Claramente, nosso padrão anterior não irá funcionar, pois dependia fortemente da
ordem dos atributos. Já vimos o metacaractere | que indica alternativa, algo
que será útil aqui pois queremos pegar cada atributo independente da ordem.
Outro conceito que utilizaremos é o de grupo de não captura, representado por
(?:...), substituindo as reticências por uma expressão regular. Para entender,
considere os exemplos a seguir:
>>> m = re.match('name="(.+?)"', 'name="Francisco"')
>>> m.groups()
('Francisco',)
>>> m = re.match('name="(?:.+?)"', 'name="Francisco"')
>>> m.groups()
()
No primeiro caso, utilizamos o padrão no grupo para obter o valor “Francisco” e no segundo excluímos esse grupo.
Podemos utilizar esse padrão alternadamente para obter cada grupo da tag HTML independente da ordem:
>>> pattern = r'<(.+?) (?:(?:type="
(.+?)"|id="(.+?)"|name="(.+?)") ?)*'
# o grupo de fora indica a presença ou não de espaço
>>> m = re.match(pattern, html1)
>>> m
<re.Match object; span=(0, 41), match='<input type="text" id="id_cpf" name="cpf"'>
>>> m.groups()
('input', 'text', 'id_cpf', 'cpf')
>>> m = re.match(pattern, html2)
>>> m
<re.Match object; span=(0, 41), match='<input id="id_cpf" name="cpf" type="text"'>
>>> m.groups()
('input', 'text', 'id_cpf', 'cpf')
Grupos nomeados #
Por fim, quando há vários grupos, é útil nomeá-los. O Python possui uma forma
específica para indicar nome de cada grupo: ?P<name>.
>>> pattern = r'<(?P<tag>.+?) (?:(?:type="(?P<type>.+?)"|id="(?P<id>.+?)"|name="(?P<name>.+?)") ?)*'
>>> m = re.match(pattern, html1)
>>> m.groups()
('input', 'text', 'id_cpf', 'cpf')
Quando se usa grupos nomeados, há um método muito útil, o groupdict, que
retorna o nome do grupo e o valor na forma de um dicionário:
>>> m.groupdict()
{'tag': 'input', 'type': 'text', 'id': 'id_cpf', 'name': 'cpf'}
>>> m = re.match(pattern, html2)
>>> m.groups()
('input', 'text', 'id_cpf', 'cpf')
>>> m.groupdict()
{'tag': 'input', 'type': 'text', 'id': 'id_cpf', 'name': 'cpf'}
Conclusão e um observação IMPORTANTE #
A observação importante é: não é porque algo é possível de resolver com expressões regulares que deve ser resolvido com expressões regulares.
Especificamente em Python, manipulações de string envolvendo substituições
costumam ser mais efetivas com métodos de strings como o replace. A própria
documentação
deixa isso claro.
Da mesma forma, por mais que tenha usado como exemplo o caso da tag HTML, há diversas discussões online sobre formas mais eficientes de extrair informações de HTML e porque regex não é, geralmente, a melhor opção. Veja aqui, aqui e aqui. Prepare-se para discussões acaloradas. E exercite o bom senso para saber quando é aceitável o uso e quando outra ferramenta deve ser utilizada.
Expressões regulares são úteis, mas podem ser muito difíceis de ler e debugar. Procure deixá-las o menor e mais específicas possível. É um assunto muito mais extenso que o apresentado aqui, tentei colocar o que considero um bom início e, obviamente, aquilo que está dentro do meu conhecimento. Certamente é uma ferramenta de conhecimento muito valioso.
Até a próxima!