3 pontos por GN⁺ 2024-03-21 | 1 comentários | Compartilhar no WhatsApp

Entendendo o comportamento do caractere "$" em expressões regulares no Python

  • Ao usar o módulo re do Python, é conhecido que ^ significa "início da string" e $ significa "fim da string".
  • Porém, $ nem sempre significa apenas "fim da string", e seu comportamento pode variar conforme a plataforma.
  • No Python, quando o modo multilinha está desativado, o caractere $ pode corresponder ao fim da string ou antes do caractere de nova linha no final da string.

Diferença entre corresponder ao fim da string e ao caractere de nova linha

  • Com o modo multilinha desativado, no Python, para corresponder ao fim da string sem caractere de nova linha, não basta usar apenas $.
  • É possível usar \z e \Z para corresponder ao fim da string.
  • No Python, ao usar re.MULTILINE, $ corresponde ao fim da string e ao fim de cada linha (logo antes do caractere de nova linha).

Comparação do comportamento de expressões regulares em várias plataformas

  • Por meio de uma tabela que compara se há correspondência para o padrão em "cat\n" em várias plataformas, mostra-se que, se for permitido corresponder incluindo o caractere de nova linha, usar $ no modo multilinha funciona de forma consistente.
  • Para corresponder sem incluir o caractere de nova linha, deve-se usar \z em todas as plataformas, exceto Python e ECMAScript; em Python e ECMAScript, deve-se usar respectivamente \Z ou $ sem modo multilinha.

Opinião do GN⁺

  • Este artigo pode alertar desenvolvedores que usam expressões regulares sobre o comportamento inesperado do caractere $ no Python.
  • Expressões regulares são muito poderosas no processamento de strings, mas é enfatizado que é preciso cuidado, pois podem apresentar comportamentos diferentes conforme a plataforma.
  • Desenvolvedores precisam estar cientes dessas diferenças e realizar testes adicionais para evitar problemas de compatibilidade ao desenvolver aplicações multiplataforma.
  • Outras bibliotecas de expressões regulares que oferecem funcionalidades semelhantes incluem java.util.regex do Java e System.Text.RegularExpressions do .NET, e também é necessário entender e usar corretamente as diferenças de comportamento de cada plataforma.
  • Ao introduzir nova sintaxe ou novos comportamentos em expressões regulares, é preciso considerar a compatibilidade com o código existente, o impacto em desempenho e a curva de aprendizado da equipe, avaliando bem os benefícios e custos dessas mudanças.

1 comentários

 
GN⁺ 2024-03-21
Comentário do Hacker News
  • Pessoas familiarizadas com expressões regulares sabem que ^ significa "início da string" e $ significa "fim da string". Mas, pessoalmente, penso neles como "início da linha" e "fim da linha". Na maioria dos casos, como lidamos com texto uma linha de cada vez, o resultado é o mesmo, mas a perspectiva com que penso nesses operadores não muda. Provavelmente porque tive meu primeiro contato com expressões regulares através do grep e costumo pensar na entrada principalmente em termos de "linhas".

    • Expressões regulares POSIX e expressões regulares de Python são diferentes. Em geral, é preciso consultar a documentação da implementação de regex que você usa, e a sintaxe não é universal.
    • Segundo o Capítulo 9 do POSIX, expressões regulares normalmente estão relacionadas ao processamento de texto e operam sobre strings terminadas em NUL, que indica o fim da string. Alguns utilitários limitam o processamento a unidades de linha. $ pode corresponder ao fim da string ou ao fim da linha, e isso é definido pelo utilitário (ou modo). A maioria dos utilitários comuns (grep, sed, awk, Python etc.) trata isso, por padrão, como fim da linha.
    • Não existe uma única sintaxe de expressão regular universal. Sem saber a linguagem e as opções em uso, não dá para ler ou escrever regex com confiança.
  • Esta é uma oportunidade perfeita para apresentar Robert Elder. Ele produz conteúdo no YouTube e em blog, tem uma série sobre expressões regulares e se aprofunda bastante nas diferenças de comportamento entre várias ferramentas.

    • O conteúdo mais recente dele também é ótimo: https://www.youtube.com/watch?v=ys7yUyyQA-Y
    • Ele tem muito conteúdo que pode interessar aos usuários do HN, por exemplo sobre a realidade e as dificuldades da consultoria.
  • Expressões regulares foram uma das primeiras coisas que realmente internalizei quando comecei a aprender Perl. (Perl ainda ocupa um lugar especial no meu coração por causa do livro "Camel")

    • Hoje, a informação mais importante é saber que as implementações diferem, e criar o hábito de buscar a referência daquilo com que você está trabalhando.
    • Por exemplo, regex no Emacs usa "\s_-" (ou algo que você vê na tela sem consultar a referência), em vez de "\w", como classe de caracteres, mas o Emacs tem a melhor documentação e facilidade de descoberta.
    • Alguns utilitários exigem escape de parênteses e outros não. Às vezes esse comportamento é configurável, e às vezes não.
    • Já passei por todas as fases de confusão, irritação e negação, e agora simplesmente aceitei. O conceito é o mesmo em todo lugar, mas a variação muda.
  • Já consigo ouvir maus gerentes de contratação adicionando "como se faz match do fim da string em uma expressão regular?" à lista de perguntas do tipo "ha! você não conhece essa pegadinha!".

  • É estranho deixar Perl de fora da lista quando o assunto é expressões regulares.

    • A descrição de $ na documentação perlre: faz correspondência com o fim da string (ou antes da quebra de linha no fim da string; ou, com /m, antes de qualquer quebra de linha)
  • Raku (anteriormente Perl 6) escolheu ^ e $ para representar o início e o fim da string, e introduziu ^^ e $$ para representar o início e o fim da linha. O modo multilinha não está disponível nem é necessário.

    • Uma das vantagens de uma reformulação/reimplementação completa é poder aprender com o fato de que o comportamento anterior surpreendia as pessoas.
  • Alguém realmente acha que expressões regulares foram padronizadas? Mudar para um novo contexto é sempre um processo de reaprendizado.

  • Há uma confusão entre string e linha. Uma string é uma sequência de caracteres, e uma linha pode ser duas coisas diferentes. Se você considerar a quebra de linha como terminador de linha, então uma linha é uma sequência de caracteres que não são quebra de linha, incluindo a própria quebra de linha. Sem quebra de linha, não é uma linha completa. É isso que o POSIX usa. Se você considerar a quebra de linha como separador de linha, então uma linha é uma sequência de caracteres sem quebra de linha. Em ambos os casos, o conteúdo da linha termina antes da quebra de linha, seja porque a quebra de linha encerra a linha, seja porque a separa da próxima linha.

    • O significado de ^ e $ é baseado em linhas — esteja você em modo de linha única ou multilinha. Para significados baseados em string — ao lidar com arquivos, você pode até pensar no arquivo inteiro — use \A e \Z ou o equivalente.
  • Isso levou a alguns bugs sérios em apps baseados em Ruby. Sempre uso \A\z.