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

75

Muitos programadores conhecem a alegria de criar uma expressão regular rápida, geralmente com a ajuda de algum serviço da Web ou mais tradicionalmente no prompt interativo, ou talvez escrevendo um pequeno script com a expressão regular em desenvolvimento e uma coleção dos casos de teste. Em qualquer um dos casos, o processo é iterativo e bastante rápido: mantenha o hacking na seqüência de caracteres oculta até que ela corresponda e capture o que você quer e rejeite o que você não deseja.

Para um caso simples, o resultado pode ser algo assim, 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 conhecem a dificuldade de editar uma expressão regular ou apenas codificar em torno de uma expressão regular em uma base de código legado. Com um pouco de edição para dividir, acima de regexp ainda é muito fácil de compreender para qualquer pessoa razoavelmente familiarizada com regexps, e um veterano de regexp deve ver imediatamente o que ele faz (responda no final do post, caso alguém queira o exercício de descobrir por si próprios).

No entanto, as coisas não precisam ser muito mais complexas para que um regexp se torne verdadeiramente uma coisa só de escrita, e mesmo com documentação diligente (o que todos é claro faz para todas as regex complexas que escrevem ...), modificar os regexps torna-se uma tarefa assustadora. Pode ser uma tarefa muito perigosa também, se o regexp não for cuidadosamente testado em unidade (mas todos é claro tem testes unitários abrangentes para todas as expressões regulares, positivas e negativas ...).

Então, para encurtar a história, existe uma solução / alternativa de leitura / escrita para expressões regulares sem perder seu poder? Como seria a expressão regular acima com uma abordagem alternativa? Qualquer linguagem é boa, embora uma solução multilíngue seja a melhor, na medida em que regexps são multi-idiomas.

E então, o que o regexp anterior faz é isto: analisar uma cadeia de números no formato 1:2:3.4 , capturando cada número, onde espaços são permitidos e somente 3 é necessário.

    
por hyde 15.04.2013 / 14:44
fonte

11 respostas

80

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

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 parece que é mais claro que o original.

Além disso, o C # possui o operador @ que pode ser adicionado a uma string para indicar que ele deve ser usado literalmente (sem caracteres de escape), portanto number seria @"([\d]+)";

    
por 15.04.2013 / 17:04
fonte
42

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

Dentro de perl , o operador /x no final da expressão regular suprime os espaços em branco, permitindo que um documente 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, é um pouco desgastante de espaços em branco verticais, embora se possa encurtá-lo sem sacrificar muita legibilidade.

And then, what the earlier regexp does is this: parse a string of numbers in format 1:2:3.4, capturing each number, where spaces are allowed and only 3 is required.

Olhando para essa expressão regular, é possível ver como ela funciona (e não funciona). Nesse caso, essa regex corresponderá à string 1 .

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

O Perl6 (o exemplo acima foi para o perl5) leva isso adiante com o conceito das regras , o que leva a ainda mais poderosas estruturas do que o PCRE (ele fornece acesso a outras gramáticas (contexto livre e sensível ao contexto) do que apenas as regulares e regulares regulares).

Em Java (onde este exemplo é extraído), pode-se usar a concatenação de strings 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*$"
);

É verdade que isso cria muito mais " na string, o que possivelmente causa alguma confusão, pode ser mais facilmente lido (especialmente com realce de sintaxe na maioria dos IDEs) e documentado.

A chave é reconhecer o poder e a natureza "escrever uma vez" que as expressões regulares geralmente entram. Escrever o código para evitar isso defensivamente, de modo que a expressão regular permaneça clara e compreensível, é fundamental. Formamos 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.

    
por 15.04.2013 / 16:54
fonte
26

O modo "detalhado" oferecido por alguns idiomas e bibliotecas é uma das respostas para essas preocupações. Neste modo, os espaços em branco na string regexp são removidos (então você precisa usar \s ) e os comentários são possíveis. Aqui está um pequeno exemplo em 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 seja, implementar um tradutor de modo detalhado para "normal" deve ser uma tarefa simples. Se você está preocupado com a legibilidade do seu regexps, provavelmente justificaria esse investimento com bastante facilidade.

    
por 15.04.2013 / 16:28
fonte
15

Toda linguagem que usa regexes permite que você as componha de blocos mais simples para facilitar a leitura, e com qualquer coisa mais complicada do que (ou tão complicada quanto) o seu exemplo, você deve definitivamente tirar vantagem dessa opção. O problema específico do Java e de muitas outras linguagens é que eles não tratam expressões regulares como cidadãos de "primeira classe", exigindo que eles se infiltrem na linguagem via literais de string. Isso significa muitas aspas e barras invertidas que não fazem parte da sintaxe da regex e tornam as coisas difíceis de ler, e também significa que você não pode ficar muito mais legível do que isso sem efetivamente definir sua própria mini-linguagem e interpretador. / p>

A melhor maneira prototípica de integrar expressões regulares era, obviamente, a Perl, com sua opção de espaço em branco e operadores de re-expressão. O Perl 6 estende o conceito de construção de expressões regulares de partes para gramáticas recursivas reais, o que é muito melhor de usar e realmente não é uma comparação. A linguagem pode ter perdido o rumo da oportunidade, mas o seu apoio de regex foi The Good Stuff (tm).

    
por 15.04.2013 / 14:52
fonte
11

Eu gosto de usar o Expresso: link

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

  • Você pode simplesmente copiar e colar seu regex e o aplicativo analisará para você
  • Uma vez que seu regex é escrito, você pode testá-lo diretamente do aplicativo (o aplicativo lhe dará a lista de capturas, substituições ...)
  • Após testá-lo, ele gerará o código C # para implementá-lo (observe que o código conterá as explicações sobre o seu regex).

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

É claro que tentar vale mais que mil palavras descrevendo-o. Por favor, note também que estou relacionado a qualquer nota com o editor desta aplicação.

    
por 15.04.2013 / 16:10
fonte
9

Para algumas coisas, pode ajudar apenas usar uma gramática como a BNF. Estes podem ser muito mais fáceis de ler do que expressões regulares. Uma ferramenta como o GoldParser Builder pode então 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 de fazer do que uma expressão regular complicada. OURO é uma ferramenta para essas coisas.

O link do wiki c2 abaixo tem uma lista de possíveis alternativas que podem ser pesquisadas, com algumas discussões sobre elas incluídas. É basicamente um link "ver também" para finalizar minha recomendação de mecanismo gramatical:

Alternativas às expressões regulares

Taking "alternative" to mean "semantically equivalent facility with different syntax", there are at least these alternatives to/with RegularExpressions:

  • Basic regular expressions
  • "Extended" regular expressions
  • Perl-compatible regular expressions
  • ... and many other variants...
  • SNOBOL-style RE syntax (SnobolLanguage, IconLanguage)
  • SRE syntax (RE's as EssExpressions)
  • different FSM syntaces
  • Finite-state intersection grammars (quite expressive)
  • ParsingExpressionGrammars, as in OMetaLanguage and LuaLanguage (http://www.inf.puc-rio.br/~roberto/lpeg/lpeg.html)
  • The parse mode of RebolLanguage
  • ProbabilityBasedParsing...
    
por 15.04.2013 / 20:06
fonte
4

Essa é uma pergunta antiga e não vi nenhuma menção a Expressões verbais , então eu pensei em adicionar essa informação aqui como bem para os futuros requerentes. As Expressões Verbais foram especificamente projetadas para tornar o regex humano compreensível, sem a necessidade de aprender o significado do símbolo de 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.

    
por 13.10.2016 / 21:15
fonte
3

A maneira mais simples seria usar o regex, mas construir a sua expressão a partir da composição de expressões mais simples com nomes descritivos, por ex. link (e sim, isso é da concat de string)

no entanto, como alternativa, você também pode usar uma biblioteca combinatória de analisador, por exemplo link que lhe dará um parser decente recursivo completo. novamente o poder real vem da composição (desta vez composição funcional).

    
por 15.04.2013 / 16:23
fonte
3

Eu achei que valeria a pena mencionar as expressões grok do logstash. Grok baseia-se na ideia de compor longas expressões de análise de expressões mais curtas. Ele permite testes convenientes desses blocos de construção e vem pré-empacotado com mais de 100 padrões comumente usados . Além desses padrões, permite o uso de todas as sintaxes de expressões regulares.

O padrão acima expresso em grok é (eu testei no aplicativo de depuração , mas poderia ter errado):

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

As peças e espaços opcionais fazem com que pareça um pouco mais feio do que o normal, mas tanto aqui quanto em outros casos, o uso do grok pode tornar a vida mais agradável.

    
por 17.04.2013 / 23:02
fonte
2

Em F # você tem o módulo FsVerbalExpressions . Ele permite que você escreva Regexes a partir de expressões verbais, mas também possui expressões regulares pré-construídas (como URL).

Um dos exemplos desta 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 do F #, groupName é a string "GroupNumber".

Então eles criam uma Expressão Verbal (VerbEx) que eles constroem como "COD (? < GroupNumber > [0-9] {3}) END". Que eles, então, testam na string "COD123END", onde recebem o grupo de captura nomeado "GroupNumber". Isso resulta em 123.

Eu honestamente acho a regex normal muito mais fácil de entender.

    
por 08.02.2017 / 13:26
fonte
-2

Primeiro, entenda que o código que simplesmente funciona é um código ruim. Um bom código também precisa informar 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 "trabalhado ou com falha" 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 corretamente. 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 em seu exemplo "analisar um conjunto de números no formato 1: 2: 3.4". Tudo o que regex faz é relatar um "pass / fail" que não permite que o feedback adequado seja apresentado ao usuário (se este feedback é uma mensagem de erro em um log, ou uma GUI interativa onde os erros são mostrados em vermelho como o tipos de usuário, ou qualquer outra coisa). Que tipos de erros ele não descreve corretamente? Caractere ruim no primeiro número, primeiro número muito grande, dois pontos após o primeiro número, etc.

Para converter "código incorreto que meramente funciona" em "código bom que fornece erros adequadamente descritivos", você tem que dividir o regex em muitos regexes menores (normalmente, regexes tão pequenos que é mais fácil fazê-lo sem expressões regulares) o primeiro lugar).

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

    
por 18.04.2013 / 02:02
fonte