Dominando Expressões Regulares em Ruby: Da Sintaxe Básica aos Padrões Avançados

As expressões regulares são uma das ferramentas mais poderosas que temos no Ruby para trabalhar com strings. Embora possam parecer intimidantes no início, elas nos permitem realizar operações complexas de busca, extração e substituição de texto com uma elegância que seria impossível de alcançar com métodos convencionais. Neste tutorial completo, vamos explorar desde os conceitos fundamentais até técnicas avançadas de manipulação de padrões.

O Poder das Expressões Regulares

Uma expressão regular é um padrão que pode ser comparado com uma string. Esse padrão pode ser simples, como "a string deve conter a sequência de letras 'gato'", ou complexo, envolvendo múltiplas condições e estruturas. O que torna as expressões regulares verdadeiramente poderosas são as três operações fundamentais que elas nos permitem realizar:

  • Testar se uma string corresponde a um padrão
  • Extrair de uma string as seções que correspondem a todo ou parte de um padrão
  • Modificar a string, substituindo as partes que correspondem ao padrão

Criando Expressões Regulares

Ruby oferece várias maneiras de criar padrões de expressão regular. A mais comum é escrever o padrão entre barras normais, criando um literal de expressão regular:

# Padrão simples que procura pela palavra "gato"
padrao_simples = /gato/

# Alternativas para criar expressões regulares
padrao_classe = Regexp.new("gato")
padrao_delimitador = %r{gato}

# Útil quando o padrão contém barras normais
caminho_arquivo = %r{/usr/local/bin}

Correspondência de Padrões: Encontrando o que Procuramos

O operador =~ é nossa ferramenta principal para verificar se uma string corresponde a um padrão. Ele retorna a posição do caractere onde a correspondência começou, ou nil se não houver correspondência:

# Exemplos de correspondência
/gato/ =~ "cachorro e gato"    # => 12
/gato/ =~ "gatinho"             # => 0
/gato/ =~ "Gato"               # => nil (sensível a maiúsculas)

# Método match? para resultado booleano
/gato/.match?("cachorro e gato")  # => true
/gato/.match?("Gato")               # => false

Uma das aplicações práticas mais comuns é filtrar linhas de arquivo baseadas em padrões:

# Processa arquivo linha por linha
File.foreach("log.txt").with_index do |linha, indice|
  puts "#{indice}: #{linha}" if linha.match?(/erro/)
end

# Usando o operador de correspondência negativa
File.foreach("log.txt").with_index do |linha, indice|
  puts "#{indice}: #{linha}" if linha !~ /sucesso/
end

Substituição com Padrões

Os métodos sub e gsub nos permitem substituir texto baseado em padrões. A diferença é que sub substitui apenas a primeira ocorrência, enquanto gsub substitui todas:

texto = "Cachorro e Gato"
novo_texto = texto.sub(/Gato/, "Hamster")
puts novo_texto  # => "Cachorro e Hamster"

# Comparando sub e gsub
texto = "Cachorro e Gato"
texto1 = texto.sub(/a/, "*")
texto2 = texto.gsub(/a/, "*")

puts "Usando sub: #{texto1}"   # => "C*chorro e Gato"
puts "Usando gsub: #{texto2}"  # => "C*chorro e G*to"

Substituição com Blocos

Uma das funcionalidades mais poderosas é a capacidade de usar blocos com os métodos de substituição. O bloco recebe cada substring correspondente e retorna o valor de substituição:

texto = "raposa marrom veloz"

# Capitalizar primeira letra
resultado = texto.sub(/^./) { |correspondencia| correspondencia.upcase }
puts resultado  # => "Raposa marrom veloz"

# Capitalizar todas as vogais
resultado = texto.gsub(/[aeiou]/) { |vogal| vogal.upcase }
puts resultado  # => "rApOsA mArrOm vElOz"

Sintaxe Avançada de Padrões

Âncoras: Controlando Posições

As âncoras nos permitem especificar onde queremos que a correspondência ocorra na string. São fundamentais para criar padrões precisos:

texto = "este é\no momento"

# ^ corresponde ao início de uma linha
/^o/.match?(texto)    # => true (início da segunda linha)

# $ corresponde ao final de uma linha
/é$/.match?(texto)   # => true (final da primeira linha)

# \A corresponde apenas ao início da string inteira
/\Aeste/.match?(texto)  # => true

# \b corresponde a limites de palavra
frase = "seis horas"
/\bhoras/.match?(frase)  # => true
/\Bhora/.match?(frase)   # => true (dentro da palavra)

Classes de Caracteres

As classes de caracteres permitem especificar conjuntos de caracteres que podem corresponder a uma posição no padrão:

# Classes básicas
/[aeiou]/.match?("programação")  # => true (vogais)
/[0-9]/.match?("versão 3.2")      # => true (dígitos)
/[A-Za-z]/.match?("123abc")        # => true (letras)

# Classes negadas
/[^A-Z]/.match?("RUBY")           # => false (não há minúsculas)
/[^A-Z]/.match?("Ruby")           # => true (há minúsculas)

# Abreviações comuns
/\d/.match?("Custa R$ 12,50")  # => true (dígito)
/\s/.match?("espaço aqui")     # => true (espaço)
/\w/.match?("_variável_123")  # => true (caractere de palavra)

Repetição e Quantificadores

Os quantificadores controlam quantas vezes um padrão deve ser repetido. Eles são fundamentais para criar padrões flexíveis:

# Quantificadores básicos
/ab*/.match?("a")          # => true (* significa zero ou mais)
/ab*/.match?("abbbb")      # => true

/ab+/.match?("a")          # => false (+ significa um ou mais)
/ab+/.match?("ab")         # => true

/ab?/.match?("a")          # => true (? significa zero ou um)
/ab?/.match?("ab")         # => true

# Quantificadores específicos
/\d{3}/.match?("123")         # => true (exatamente 3 dígitos)
/\d{2,4}/.match?("12345")      # => true (2 a 4 dígitos)
/\d{3,}/.match?("12")         # => false (mínimo 3 dígitos)

Grupos e Captura

Os parênteses servem para agrupar parte do padrão e capturar os resultados para uso posterior. Esta é uma das funcionalidades mais poderosas das expressões regulares:

# Extração de dados estruturados
/(\d{2}):(\d{2})(..)/ =~ "14:30pm"
puts "Hora é #{$1}, minuto #{$2}, período #{$3}"
# => "Hora é 14, minuto 30, período pm"

# Usando MatchData (mais limpo)
dados = /(\d{2}):(\d{2})(..)/.match("14:30pm")
puts "Hora é #{dados[1]}, minuto #{dados[2]}"

# Grupos nomeados (mais legível)
/(?<hora>\d{2}):(?<minuto>\d{2})(?<periodo>..)/ =~ "14:30pm"
puts "Hora é #{hora}, minuto #{minuto}"

# Detecção de repetições
/(\w)\1/.match?("Mississippi")  # => true (letras duplicadas)

Caso Prático: Formatador de Nomes

Vamos criar um exemplo prático que demonstra o poder das expressões regulares. Imagine que precisamos normalizar nomes de cidades inseridos pelos usuários, independentemente de como foram digitados:

def formatar_cidade(nome)
  # Converte para minúsculas e capitaliza primeira letra de cada palavra
  nome.downcase.gsub(/\b\w/) { |primeira_letra| primeira_letra.upcase }
end

# Testando diferentes formatos de entrada
puts formatar_cidade("SÃO PAULO")      # => "São Paulo"
puts formatar_cidade("rio de janeiro")  # => "Rio De Janeiro"
puts formatar_cidade("bElO hOrIzOnTe")  # => "Belo Horizonte"

# Versão alternativa usando &:upcase
def formatar_cidade_v2(nome)
  nome.downcase.gsub(/\b\w/, &:upcase)
end

Opções e Modificadores de Expressão Regular

Ruby oferece várias opções que modificam como os padrões são interpretados. Essas opções podem ser especificadas após o delimitador final da expressão regular:

# i = case insensitive (insensível a maiúsculas)
/ruby/i.match?("RUBY")    # => true
/ruby/i.match?("Ruby")    # => true

# m = multiline mode (. corresponde a quebras de linha)
texto_multiplo = "linha1\nlinha2"
/linha1.linha2/.match?(texto_multiplo)   # => false
/linha1.linha2/m.match?(texto_multiplo)  # => true

# x = extended mode (permite espaços e comentários)
padrao_complexo = %r{
  (\w.*),        # nome da cidade seguido por vírgula
  \s                # um espaço
  ([A-Z][A-Z])      # sigla do estado (duas maiúsculas)
  \s                # um espaço
  (\d{5})         # CEP de 5 dígitos
}x

resultado = padrao_complexo.match("São Paulo, SP 01310")
puts "Cidade: #{resultado[1]}, Estado: #{resultado[2]}, CEP: #{resultado[3]}"

Substituição Avançada com Referências

Uma das funcionalidades mais poderosas é a capacidade de referenciar grupos capturados durante a substituição, permitindo reestruturar dados de forma elegante:

# Invertendo nome:sobrenome para sobrenome, nome
puts "joão:silva".sub(/(\w+):(\w+)/, '\2, \1')
# => "silva, joão"

# Descriptografando texto simples (troca pares de caracteres)
puts "txeto crpitogarfado".gsub(/(.)(.)/, '\2\1')
# => "texto criptografado"

# Usando grupos nomeados na substituição
resultado = "joão:silva".sub(
  /(?<primeiro>\w+):(?<ultimo>\w+)/, 
  '\k<ultimo>, \k<primeiro>'
)
puts resultado  # => "silva, joão"

# Substituição usando hash
dicionario = { "gato" => "felino", "cachorro" => "canino" }
dicionario.default = "animal"

puts "gato e cachorro".gsub(/\w+/, dicionario)
# => "felino animal canino"

Dicas Práticas e Depuração

Expressões regulares podem ser complexas de desenvolver e debugar. Aqui estão algumas estratégias que todo desenvolvedor Ruby deveria conhecer:

# Método auxiliar para visualizar correspondências
def mostrar_correspondencia(string, padrao)
  dados = padrao.match(string)
  if dados
    "#{dados.pre_match}->#{dados[0]}<-#{dados.post_match}"
  else
    "sem correspondência"
  end
end

# Testando diferentes padrões
puts mostrar_correspondencia('muito interessante', /t/)
# => "mui->t<-o interessante"

puts mostrar_correspondencia('João Silva', /lva/)
# => "João Si->lva<-"

# Quantificadores gananciosos vs. preguiçosos
texto = "A lua é feita de queijo"
puts mostrar_correspondencia(texto, /\s.*\s/)    # ganancioso
# => "A-> lua é feita de <-queijo"

puts mostrar_correspondencia(texto, /\s.*?\s/)   # preguiçoso
# => "A-> lua <-é feita de queijo"

Conclusão

As expressões regulares são uma ferramenta indispensável no arsenal de qualquer desenvolvedor Ruby. Embora possam parecer complexas inicialmente, o investimento em aprender sua sintaxe e aplicações compensa amplamente. Elas nos permitem resolver problemas de manipulação de texto de forma concisa e elegante que seria impossível de alcançar com métodos convencionais de string.

Lembre-se sempre de testar suas expressões regulares incrementalmente, usar o IRB para experimentar padrões, e considerar a legibilidade do código ao criar padrões complexos. O modo estendido (x) pode ser seu melhor amigo quando precisar criar expressões regulares que outros desenvolvedores também precisem entender.

Com essa base sólida, você está preparado para enfrentar qualquer desafio de manipulação de texto que o Ruby possa apresentar. As possibilidades são praticamente infinitas!

Comentários (0)

Nenhum comentário:

Postar um comentário