Expressões regulares legíveis sem perder o poder?

77

Muitos programadores conhecem a alegria de criar uma expressão regular rápida, hoje em dia com a ajuda de algum serviço da Web, ou mais tradicionalmente em um prompt interativo, ou talvez escrevendo um pequeno script com a expressão regular em desenvolvimento e uma coleção de casos de teste . Em qualquer um dos casos, o processo é iterativo e bastante rápido: continue hackeando a cadeia de aparência enigmática até que ela corresponda e capture o que você deseja e rejeite o que você não deseja.

Para um resultado simples, pode ser algo como isto, como um regexp Java:

Pattern re = Pattern.compile(
  "^\\s*(?:(?:([\\d]+)\\s*:\\s*)?(?:([\\d]+)\\s*:\\s*))?([\\d]+)(?:\\s*[.,]\\s*([0-9]+))?\\s*$"
);

Muitos programadores também sabem o que é necessário para editar uma expressão regular ou apenas codificar em torno de uma expressão regular em uma base de código herdada. Com um pouco de edição para dividir, o regexp acima ainda é muito fácil de entender para qualquer pessoa razoavelmente familiarizada com o regexps, e um veterano do regexp deve ver imediatamente o que ele faz (responda no final do post, caso alguém queira fazer o exercício de descobrir por si mesmos).

No entanto, as coisas não precisam ficar muito mais complexo para um regexp para se tornar verdadeiramente coisa, e mesmo com a documentação diligente (que todo mundo só escrevo- claro faz para todos regexps complexos que escrevem ...), modificando as regexps torna-se um tarefa difícil. Também pode ser uma tarefa muito perigosa, se o regexp não for cuidadosamente testado por unidade (mas todos obviamente terão testes de unidade abrangentes para todos os seus regexps complexos, positivos e negativos ...).

Então, para encurtar a história, existe uma solução / alternativa gravação / leitura para expressões regulares sem perder seu poder? Como seria a regexp acima com uma abordagem alternativa? Qualquer idioma é bom, embora uma solução em vários idiomas seja melhor, na medida em que os regexps são em vários idiomas.


E então, o que o regexp anterior faz é o seguinte: analisa uma sequência de números em formato 1:2:3.4, capturando cada número, onde os espaços são permitidos e somente 3necessários.

hyde
fonte
2
coisa relacionada no SO: stackoverflow.com/a/143636/674039
wim
24
Ler / editar regexes é realmente trivial se você souber o que eles devem capturar. Você já deve ter ouvido falar desse recurso raramente usado na maioria dos idiomas chamado "comentários". Se você não colocar um acima de um regex complexo, explicando o que ele faz, pagará o preço mais tarde. Além disso, revisão de código.
TC1
2
Duas opções para limpar isso sem quebrá-lo em pedaços menores. Sua presença ou ausência varia de idioma para idioma. (1) expressões regulares de linha estendida, onde os espaços em branco na expressão regular são ignorados (a menos que escapem) e um formulário de comentário de linha única é adicionado, para que você possa dividi-lo em pedaços lógicos com indentação, espaçamento de linha e comentários. (2) grupos de captura nomeados, nos quais você pode dar um nome a cada parêntese, o que adiciona alguma autodocumentação e preenche automaticamente um hash de correspondências - muito melhor do que uma matriz de correspondências indexadas numericamente ou variáveis ​​$ N.
Ben Lee
3
Parte do problema é a própria linguagem regex e as más escolhas históricas em seu design, que são arrastadas como bagagem. Em uma linguagem sã, o agrupamento de parênteses é puramente um dispositivo sintático para modelar a árvore de análise. Mas nas implementações de regex voltando ao Unix, elas têm semântica: registradores de ligação a correspondências de subexpressão. Então você precisa de alguns suportes mais complicados e feios apenas para obter um agrupamento puro!
Kaz
2
Não é realmente uma resposta prática, mas pode ser útil mencionar que o poder da expressão regular é exatamente o de um autômato finito. Ou seja, as expressões regulares podem validar / analisar a mesma classe de seqüências de caracteres validadas e analisadas por autômatos finitos. Portanto, uma representação legível humana de um regex provavelmente deve ser capaz de criar rapidamente um gráfico, e acredito que a maioria das linguagens baseadas em texto é realmente ruim nisso; é por isso que usamos ferramentas visuais para essas coisas. Dê uma olhada em hackingoff.com/compilers/regular-expression-to-nfa-dfa para obter alguma inspiração.
damix911

Respostas:

80

Várias pessoas mencionaram a composição de partes menores, mas ninguém deu um exemplo ainda, então aqui está o meu:

string number = "(\\d+)";
string unit = "(?:" + number + "\\s*:\\s*)";
string optionalDecimal = "(?:\\s*[.,]\\s*" + number + ")?";

Pattern re = Pattern.compile(
  "^\\s*(?:" + unit + "?" + unit + ")?" + number + optionalDecimal + "\\s*$"
);

Não é o mais legível, mas acho que é mais claro que o original.

Além disso, C # tem o @operador que pode ser prefixado a uma corda, a fim de indicar que é para ser tomado literalmente (sem caracteres de escape), então numberseria@"([\d]+)";

Bobson
fonte
Só agora percebeu como tanto [\\d]+e [0-9]+deve ser apenas \\d+(bem, alguns podem achar [0-9]+mais legível). Não vou editar a pergunta, mas você pode querer corrigir esta resposta.
Hyde
@hyde - Boa captura. Tecnicamente, eles não são exatamente a mesma coisa - \dcorresponderão a qualquer coisa considerada um número, mesmo em outros sistemas de numeração (chinês, árabe, etc.), enquanto [0-9]corresponderão apenas aos dígitos padrão. Eu padronizei \\d, no entanto, e considerei o optionalDecimalpadrão.
Bobson
42

A chave para documentar a expressão regular é documentá-la. Com demasiada frequência, as pessoas lançam o que parece ser ruído de linha e deixam assim.

Dentro de perl, o /xoperador no final da expressão regular suprime o espaço em branco, permitindo documentar a expressão regular.

A expressão regular acima se tornaria:

$re = qr/
  ^\s*
  (?:
    (?:       
      ([\d]+)\s*:\s*
    )?
    (?:
      ([\d]+)\s*:\s*
    )
  )?
  ([\d]+)
  (?:
    \s*[.,]\s*([\d]+)
  )?
  \s*$
/x;

Sim, consome um pouco de espaço em branco vertical, embora seja possível reduzi-lo sem sacrificar muita legibilidade.

E então, o que o regexp anterior faz é: analisa uma sequência de números no formato 1: 2: 3.4, capturando cada número, onde os espaços são permitidos e apenas 3 são necessários.

Observando esta expressão regular, podemos ver como ela funciona (e não funciona). Nesse caso, esse regex corresponderá à string 1.

Abordagens semelhantes podem ser adotadas em outro idioma. A opção python re.VERBOSE funciona lá.

O Perl6 (o exemplo acima foi para o perl5) leva isso adiante com o conceito de regras que leva a estruturas ainda mais poderosas que o PCRE (fornece acesso a outras gramáticas (livres de contexto e sensíveis ao contexto) do que apenas regulares e estendidas).

Em Java (de onde este exemplo se baseia), pode-se usar a concatenação de cadeias para formar o regex.

Pattern re = Pattern.compile(
  "^\\s*"+
  "(?:"+
    "(?:"+
      "([\\d]+)\\s*:\\s*"+  // Capture group #1
    ")?"+
    "(?:"+
      "([\\d]+)\\s*:\\s*"+  // Capture group #2
    ")"+
  ")?"+ // First groups match 0 or 1 times
  "([\\d]+)"+ // Capture group #3
  "(?:\\s*[.,]\\s*([0-9]+))?"+ // Capture group #4 (0 or 1 times)
  "\\s*$"
);

É certo que isso cria muito mais "na string, possivelmente levando a alguma confusão lá, pode ser mais facilmente lido (especialmente com sintaxe destacada na maioria dos IDEs) e documentado.

A chave é reconhecer o poder e a natureza "escrever uma vez" nas quais expressões regulares frequentemente se enquadram. Escrever o código para evitar isso defensivamente, de modo que a expressão regular permaneça clara e compreensível, é fundamental. Formaizamos o código Java para maior clareza - as expressões regulares não são diferentes quando a linguagem oferece a opção de fazê-lo.


fonte
13
Há uma grande diferença entre "documentar" e "adicionar quebras de linha".
4
@ JonofAllTrades Tornar o código capaz de ser lido é o primeiro passo para qualquer coisa. A adição de quebras de linha também permite adicionar comentários para esse subconjunto do ER na mesma linha (algo mais difícil de fazer em uma única linha longa de texto de expressão regular).
2
@ JonofAllTrades, eu discordo bastante. "Documentar" e "adicionar quebras de linha" não são tão diferentes, pois ambos servem ao mesmo objetivo - facilitando a compreensão do código. E para código mal formatado, "adicionar quebras de linha" serve a esse propósito muito melhor do que a adição de documentação.
Ben Lee
2
Adicionar quebras de linha é um começo, mas representa cerca de 10% do trabalho. Outras respostas fornecem mais detalhes, o que é útil.
26

O modo "detalhado" oferecido por alguns idiomas e bibliotecas é uma das respostas para essas preocupações. Nesse modo, o espaço em branco na cadeia de caracteres regexp é removido (portanto, você precisa usar \s) e os comentários são possíveis. Aqui está um pequeno exemplo no Python que suporta isso por padrão:

email_regex = re.compile(r"""
    ([\w\.\+]+) # username (captured)
    @
    \w+         # minimal viable domain part
    (?:\.w+)    # rest of the domain, after first dot
""", re.VERBOSE)

Em qualquer idioma que não exija, implementar um tradutor do modo detalhado para o "normal" deve ser uma tarefa simples. Se você está preocupado com a legibilidade de seus regexps, provavelmente justificaria esse investimento de tempo com bastante facilidade.

Xion
fonte
15

Todo idioma que usa expressões regulares permite que você as componha a partir de blocos mais simples para facilitar a leitura e, com algo mais complicado que (ou tão complicado quanto) o seu exemplo, você definitivamente deve tirar proveito dessa opção. O problema específico com Java e muitas outras linguagens é que elas não tratam expressões regulares como cidadãos de "primeira classe", exigindo que elas se infiltrem na linguagem por meio de literais de strings. Isso significa muitas aspas e barras invertidas que não fazem parte da sintaxe regex e dificultam a leitura, e também significa que você não pode ficar muito mais legível do que isso sem definir efetivamente seu próprio mini-idioma e intérprete.

A melhor maneira prototípica de integrar expressões regulares era, obviamente, o Perl, com sua opção de espaço em branco e operadores de cotação de expressões regulares. O Perl 6 estende o conceito de criar expressões regulares de partes para gramáticas recursivas reais, o que é muito melhor usar que realmente não há comparação. O idioma pode ter perdido o barco da pontualidade, mas seu apoio à expressão regular era The Good Stuff (tm).

Kilian Foth
fonte
1
Por "blocos mais simples" mencionados no início da resposta, você quer dizer apenas concatenação de strings ou algo mais avançado?
Hyde
7
Eu quis dizer definir subexpressões como literais de cadeia mais curta, atribuí-las a variáveis ​​locais com nomes significativos e concatenar. Acho que os nomes são mais importantes para a legibilidade do que apenas a melhoria do layout.
precisa saber é o seguinte
11

Eu gosto de usar o Expresso: http://www.ultrapico.com/Expresso.htm

Este aplicativo gratuito possui os seguintes recursos que considero úteis ao longo do tempo:

  • Você pode simplesmente copiar e colar seu regex e o aplicativo irá analisá-lo para você
  • Após a gravação do seu regex, você pode testá-lo diretamente do aplicativo (o aplicativo fornecerá a lista de capturas, substituições ...)
  • Depois de testá-lo, ele gerará o código C # para implementá-lo (observe que o código conterá as explicações sobre sua regex).

Por exemplo, com o regex que você acabou de enviar, seria semelhante a: Tela de amostra com o regex fornecido inicialmente

Claro, experimentá-lo vale mais que mil palavras descrevendo-o. Observe também que estou de alguma forma relacionado ao editor deste aplicativo.

E. Jaep
fonte
4
você se importaria de explicar isso com mais detalhes - como e por que ele responde à pergunta? "Respostas só-Link" não são muito bem-vindos no Stack Câmbio
mosquito
5
@gnat Desculpe por isso. Você está absolutamente correto. Espero que minha resposta editada forneça mais informações.
E. Jaep
9

Para algumas coisas, pode ser útil usar apenas uma gramática como o BNF. Isso pode ser muito mais fácil de ler do que expressões regulares. Uma ferramenta como o GoldParser Builder pode converter a gramática em um analisador que faz o trabalho pesado para você.

As gramáticas BNF, EBNF etc. podem ser muito mais fáceis de ler e criar do que uma expressão regular complicada. O OURO é uma ferramenta para essas coisas.

O link wiki c2 abaixo tem uma lista de alternativas possíveis que podem ser pesquisadas no Google, com alguma discussão sobre elas incluída. É basicamente um link "consulte também" para completar minha recomendação de mecanismo gramatical:

Alternativas às expressões regulares

Tomando "alternativa" como "facilidade semanticamente equivalente com sintaxe diferente", existem pelo menos essas alternativas para / com RegularExpressions:

  • Expressões regulares básicas
  • Expressões regulares "estendidas"
  • Expressões regulares compatíveis com Perl
  • ... e muitas outras variantes ...
  • Sintaxe RE do estilo SNOBOL (SnobolLanguage, IconLanguage)
  • Sintaxe SRE (ER's como EssExpressions)
  • diferentes sintaxe do FSM
  • Gramáticas de interseção em estado finito (bastante expressivas)
  • ParsingExpressionGrammars, como em OMetaLanguage e LuaLanguage ( http://www.inf.puc-rio.br/~roberto/lpeg/lpeg.html )
  • O modo de análise do RebolLanguage
  • ProbabilityBasedParsing ...
Nick P
fonte
você se importaria de explicar mais sobre o que esse link faz e para que serve? "Respostas só-Link" não são muito bem-vindos no Stack Câmbio
mosquito
1
Bem-vindo aos programadores, Nick P. Por favor, ignore o voto negativo / r, mas leia a página na meta à qual o @gnat está vinculado.
precisa
@ Christoffer Lette Agradeço sua resposta. Tentará manter isso em mente em postagens futuras. O comentário de Paulo Scardine reflete a intenção de meus posts. As gramáticas BNF, EBNF etc. podem ser muito mais fáceis de ler e criar do que uma expressão regular complicada. O OURO é uma ferramenta para essas coisas. O link c2 possui uma lista de alternativas possíveis que podem ser pesquisadas no Google, com alguma discussão sobre elas incluída. Era basicamente um link "consulte também" para completar minha recomendação de mecanismo de gramática.
Nick P.
6

Esta é uma pergunta antiga e eu não vi nenhuma menção às Expressões Verbais, então pensei em adicionar essas informações aqui também para futuros candidatos. As expressões verbais foram projetadas especificamente para tornar o regex humano compreensível, sem a necessidade de aprender o significado do símbolo do regex. Veja o exemplo a seguir. Eu acho que isso faz melhor o que você está pedindo.

// Create an example of how to test for correctly formed URLs
var tester = VerEx()
    .startOfLine()
    .then('http')
    .maybe('s')
    .then('://')
    .maybe('www.')
    .anythingBut(' ')
    .endOfLine();

// Create an example URL
var testMe = 'https://www.google.com';

// Use RegExp object's native test() function
if (tester.test(testMe)) {
    alert('We have a correct URL '); // This output will fire}
} else {
    alert('The URL is incorrect');
}

console.log(tester); // Outputs the actual expression used: /^(http)(s)?(\:\/\/)(www\.)?([^\ ]*)$/

Este exemplo é para javascript, você pode encontrar esta biblioteca agora para muitas das linguagens de programação.

Parivar Saraff
fonte
2
Isso é incrível!
21417 Jeremy Thompson #
3

A maneira mais simples seria ainda usar o regex, mas construir sua expressão compondo expressões mais simples com nomes descritivos, por exemplo, http://www.martinfowler.com/bliki/ComposedRegex.html (e sim, isso é da string concat)

no entanto, como alternativa, você também pode usar uma biblioteca combinadora de analisadores, por exemplo, http://jparsec.codehaus.org/, que fornecerá um analisador decente recursivo completo. novamente o poder real aqui vem da composição (desta vez composição funcional).

jk.
fonte
3

Eu pensei que valeria a pena mencionar as expressões grok do logstash . O Grok se baseia na idéia de compor expressões longas de análise a partir das mais curtas. Ele permite testes convenientes desses componentes e vem pré-empacotado com mais de 100 padrões comumente usados . Além desses padrões, ele permite o uso de todas as expressões regulares de sintaxe.

O padrão acima expresso em grok é (eu testei no aplicativo depurador, mas poderia ter cometido um erro):

"(( *%{NUMBER:a} *:)? *%{NUMBER:b} *:)? *%{NUMBER:c} *(. *%{NUMBER:d} *)?"

As partes e espaços opcionais fazem com que pareça um pouco mais feia do que o habitual, mas tanto aqui como em outros casos, o uso do grok pode tornar a vida muito mais agradável.

yoniLavi
fonte
2

No F #, você tem o módulo FsVerbalExpressions . Ele permite que você componha Regexes a partir de expressões verbais, mas também possui alguns regexes pré-criados (como URL).

Um dos exemplos dessa sintaxe é o seguinte:

let groupName =  "GroupNumber"

VerbEx()
|> add "COD"
|> beginCaptureNamed groupName
|> any "0-9"
|> repeatPrevious 3
|> endCapture
|> then' "END"
|> capture "COD123END" groupName
|> printfn "%s"

// 123

Se você não estiver familiarizado com a sintaxe F #, groupName é a sequência "GroupNumber".

Em seguida, eles criam uma expressão verbal (VerbEx) que eles constroem como "COD (? <GroupNumber> [0-9] {3}) END". Em seguida, eles testam na cadeia "COD123END", onde obtêm o grupo de captura nomeado "GroupNumber". Isso resulta em 123.

Sinceramente, acho o regex normal muito mais fácil de compreender.

CodeMonkey
fonte
-2

Primeiro, entenda que o código que simplesmente funciona é um código incorreto. Um bom código também precisa relatar com precisão os erros encontrados.

Por exemplo, se você estiver escrevendo uma função para transferir dinheiro da conta de um usuário para a conta de outro usuário; você não retornaria apenas um booleano "funcionou ou falhou" porque isso não dá ao chamador nenhuma idéia do que deu errado e não permite que o chamador informe o usuário adequadamente. Em vez disso, você pode ter um conjunto de códigos de erro (ou um conjunto de exceções): não foi possível encontrar a conta de destino, fundos insuficientes na conta de origem, permissão negada, não pode se conectar ao banco de dados, muita carga (tente mais tarde), etc. .

Agora pense no seu exemplo "analise uma sequência de números no formato 1: 2: 3,4". Tudo o que a regex faz é relatar uma "aprovação / reprovação" que não permite que o feedback adequado seja apresentado ao usuário (se esse feedback é uma mensagem de erro em um log ou uma GUI interativa em que os erros são mostrados em vermelho como o tipos de usuário ou qualquer outra coisa). Que tipos de erros ele não consegue descrever adequadamente? Caractere incorreto no primeiro número, primeiro número muito grande, dois pontos ausentes após o primeiro número, etc.

Para converter "código incorreto que simplesmente funciona" em "código válido que fornece erros descritivos adequados", é necessário dividir a regex em muitas regexes menores (normalmente, regexes tão pequenas que é mais fácil fazê-lo sem regexes em primeiro lugar )

Tornar o código legível / sustentável é apenas uma conseqüência acidental de tornar o código bom.

Brendan
fonte
6
Provavelmente não é uma boa suposição. O meu é porque A) Isso não aborda a questão ( Como torná-lo legível?), B) A correspondência de expressão regular é aprovada / reprovada e, se você a decompor no ponto em que pode dizer exatamente por que ela falhou, você perca muito poder e velocidade e aumente a complexidade. C) Não há indicação da pergunta de que haja a possibilidade de a partida falhar - é simplesmente uma questão de tornar o Regex legível. Quando você controla os dados que entram e / ou os valida antes, pode assumir que são válidos.
Bobson
A) Quebrá-lo em pedaços menores torna-o mais legível (como conseqüência de torná-lo bom). C) Onde strings desconhecidas / não validadas inserem um software que um desenvolvedor sensato analisaria (com relatório de erros) nesse ponto e converteria os dados em um formulário que não precisa ser revisado - o regex não será necessário depois disso. B) é um absurdo que se aplica apenas ao código incorreto (consulte os pontos A e C).
Brendan
Indo do seu C: E se essa for a lógica de validação dele? O código do OP pode ser exatamente o que você está sugerindo - validando a entrada, relatando se não é válida e convertendo-a em um formulário utilizável (por meio das capturas). Tudo o que temos é a própria expressão. Como você sugeriria uma análise diferente de um regex? Se você adicionar algum código de exemplo que obterá o mesmo resultado, removerei meu voto negativo.
Bobson
Se for "C: Validando (com relatório de erros)", é um código incorreto, porque o relatório de erros é incorreto. Se falhar; foi porque a string era NULL, ou porque o primeiro número tinha muitos dígitos, ou porque o primeiro separador não era :? Imagine um compilador que tenha apenas uma mensagem de erro ("ERRO") que seja estúpida demais para dizer ao usuário qual é o problema. Agora imagine milhares de sites que são igualmente estúpidos e exibem (por exemplo) "Endereço de e-mail incorreto" e nada mais.
Brendan
Além disso, imagine um operador de suporte técnico semi-treinado recebendo um relatório de erro de um usuário completamente destreinado que diz: O software parou de funcionar - a última linha no log do software é "ERRO: Falha ao extrair o número da versão secundária da versão '1: 2-3.4 '(dois pontos esperados após o segundo número) "
Brendan