Expressões regulares para profissionais

Cenário

Esta postagem se origina em meu blog pessoal, em http://www.mullie.eu/regular-expressions-advanced/

Expressões regulares são ferramentas poderosas de manipulação de strings, embora provavelmente você nem saiba metade do que é possível com elas. Antes de tocar em algumas das maravilhas do PCRE, certifique-se de já estar familiarizado com as expressões regulares.

Embora você provavelmente não use nenhum dos itens a seguir diariamente, com certeza deve estar ciente de sua existência. A sintaxe exata pode ter escapado de sua mente quando você começa a usar alguns desses, mas acho que você sempre pode voltar para refrescar sua memória quando precisar, certo?

Se você já sabe tudo sobre o tutorial básico , mergulhe!

Referências anteriores

Documentos PHP

Digamos que você esteja tentando combinar as tags XML, tanto as tags de abertura quanto as de fechamento. Obviamente, você desejará encontrar a tag de fechamento correspondente à tag de abertura, não a tag de fechamento de outro elemento.

O conjunto de ferramentas PCRE fornece: referências anteriores! Mais uma sequência de fuga. Usando referências anteriores, você pode definir que uma determinada parte em sua expressão regular precisa corresponder exatamente a uma parte anterior de sua expressão regular. Este ponto de referência anterior deve ser um subpadrão e você pode simplesmente apontar para qualquer subpadrão fornecido escapando do número de índice dos subpadrões, começando em 1. São possíveis até 99 referências anteriores em 1 expressão regular.

Exemplo

No tutorial básico , já criamos um regex para encontrar todos os URLs de link dentro de uma fonte HTML. O padrão que criamos parecia . Ignoramos o fato de que o atributo HTML nem sempre está entre aspas duplas: as aspas simples são igualmente válidas. Isso basicamente significa que o caractere de fechamento de abertura deve ser ou , e o caracter de fechamento deve corresponder a esse caractere de abertura. A melhoria olhares regex como: ./href="(.*?)"/is"'/href=(['"])(.*?)\1/is

Cuidado : como as funções de expressão regular do PHP esperam que a regex seja lançada como uma string, não se esqueça de aplicar as regras de escape de string regulares aplicáveis ​​no PHP. Se a string estiver entre aspas simples, todas as aspas simples dentro dela devem ter escape e também devemos escapar da barra invertida da regex. Este exemplo finalmente ficaria assim em PHP:preg_match_all('/href=([\'"])(.*?)\\1/is', $test, $matches)

Subpadrões avançados

Documentos PHP

Os subpadrões são muito divertidos. Eles são como aquelas pequenas “expressões regulares dentro de uma expressão regular” e desbloqueiam muitos recursos interessantes.

$text = '<p id="element">Hi, this is some text</p>';
$pattern
= '/<([a-z][a-z0-9]*).*>(.*)<\/\\1>/is';
if(preg_match($pattern, $text, $match)) {
var_dump
($match);
}

A saída deste código será:

array
0 => string '<p id="element">Hi, this is some text</p>' (length=41)
1 => string 'p' (length=1)
2 => string 'Hi, this is some text' (length=21)

O primeiro valor (índice 0) é o resultado da expressão regular completa, os outros 2 valores (índice 1 e 2) são o resultado dos 2 subpadrões, tornando realmente fácil obter dados específicos diretamente dos resultados. Este é também o índice em que estão disponíveis para referência posterior.

Vamos colocar alguma ordem neste caos, no entanto.

Grupos de não captura

Os resultados podem ser ajustados ainda melhor. Uma vez que os subpadrões também podem ser usados ​​para outros fins (por exemplo, alternância), podemos não querer que todos os subpadrões apareçam no array de correspondência. Para não capturar um determinado subpadrão, basta preceder as instruções no subpadrão com ?:. Isso também tornará o subpadrão inacessível para referências anteriores.

Exemplo

$text = '<p id="element">Hi, this is some text</p>';
$pattern
= '/<([a-z][a-z0-9]*).*>(?:.*)<\/\\1>/is';
if(preg_match($pattern, $text, $match)) {
var_dump
($match);
}

A saída deste código será:

array
0 => string '<p id="element">Hi, this is some text</p>' (length=41)
1 => string 'p' (length=1)

Observe como o segundo subpadrão não aparece mais em nossa correspondência!

Subpadrões nomeados

Mas a manipulação dos subpadrões não para por aí. Não apenas podemos controlar quais subpadrões estão sendo capturados, mas também podemos dar a eles qualquer nome, adicionando as instruções do subpadrão antes de , ou . Voltar fazendo referência a um padrão chamado ainda pode ser feito pelo índice, ou pelo nome: , ou .?P<name>?<name>?'name'(?P=name)\k<name>\k'name'

Exemplo

$text = '<p id="element">Hi, this is some text</p>';
$pattern
= '/<(?P<tag>[a-z][a-z0-9]*).*>(?P<content>.*)<\/(?P=tag)>/is';
if(preg_match($pattern, $text, $match)) {
var_dump
($match);
}

A saída deste código será:

array
0 => string '<p id="element">Hi, this is some text</p>' (length=41)
'tag' => string 'p' (length=1)
1 => string 'p' (length=1)
'content' => string 'Hi, this is some text' (length=21)
2 => string 'Hi, this is some text' (length=21)

Agora que temos chaves descritivas mapeadas para nossos valores (em vez de índices), sua camada de abstração de banco de dados ou mecanismo de modelo pode até aceitar seu array de dados como está, sem ter que repeti-lo mais uma vez apenas para “formatá-lo” .

Cuidado: por padrão, você está limitado a usar 1 nome específico apenas uma vez por expressão regular. É possível ativar o suporte para vários subpadrões com o mesmo nome, porém, adicionando no início de sua expressão regular, como: . Isso pode ser útil em uma alternância, onde ambos os ramos alternativos têm um subpadrão cujo resultado você gostaria de capturar com o mesmo nome.(?J)/(?J)<(?P<something>[a-z][a-z0-9]*).*>(?P<something>.*)<\/\\1>/is

Subpadrões condicionais

Documentos PHP

Os subpadrões condicionais fornecem construções if-then (-else) dentro de uma expressão regular: se uma certa condição for correspondida, somente então um certo padrão deve ser executado (e, opcionalmente, caso contrário, outro padrão deve ser executado).

(?(condition)yes-pattern) ou (?(condition)yes-pattern|no-pattern)

A condição pode ser uma referência anterior, onde condição é o índice do subpadrão referenciado, ou uma asserção (consulte o próximo capítulo).

Exemplo

Quanto mais complicados esses conceitos se tornam, mais difícil se torna encontrar um exemplo plausível. Vamos fingir que estamos tentando corresponder as instruções CSS @import , que podem vir em ambas as formas abaixo:

$test = '
@import url("path/to/my/first/style.css");
@import "path/to/my/second/style.css");
'
;

Tanto com quanto sem invólucro constituem uma instrução @import válida , o que torna um pouco mais difícil combinar o patch em uma única regex. Vamos tentar:url()

if(preg_match_all('/@import (url\()?"(.*?)"(?(1)\))/', $test, $matches))
{
var_dump
($matches);
}

O que a regex acima faz é começar primeiro combinando a instrução @import . Depois disso, ele irá procurar por um subpadrão opcional que irá corresponder ao url ( .
Depois disso, estamos procurando por uma aspa dupla de abertura (ignorando que também podem ser aspas simples) e capturando o caminho para o arquivo CSS importado, seguido por uma aspa dupla de fechamento.
Então, o interessante acontece: o subpadrão condicional verificará a condição (1) (referência anterior ao primeiro subpadrão, que era o url opcional ( – observe que esta referência anterior não precisa de escape): se que foi correspondido, vamos exigir um fechamento entre parênteses. Não há outra instrução neste exemplo.

O resultado de $ correspondências será semelhante a este, com o índice 2 contendo os caminhos para ambas as importações. O índice 1 é o resultado do subpadrão opcional que foi usado como condição para verificar se precisamos procurar um parêntese de fechamento.

array
0 =>
array

0 => string '@import url("path/to/my/first/style.css")' (length=41)
1 => string '@import "path/to/my/second/style.css"' (length=37)
1 =>
array

0 => string 'url(' (length=4)
1 => string '' (length=0)
2 =>
array

0 => string 'path/to/my/first/style.css' (length=26)
1 => string 'path/to/my/second/style.css' (length=27)

Afirmações

Documentos PHP

Agora, temos uma caixa de ferramentas bastante séria para realizar a correspondência de padrões complexos. Mas todos os truques existentes ainda resultarão na análise sequencial de sua expressão regular. Algum dia, você só vai querer instruir “ei, eu só quero corresponder ABC, se for precedido por XYZ, mas não quero que XYZ faça parte desta correspondência”, ou “… não deve ser seguido por DEF. “

É aí que as afirmações de olhar para a frente e para trás entram em jogo. Sem realmente fazer parte do padrão a ser correspondido, eles fornecerão instruções adicionais que influenciarão o que realmente será capturado.

Para ilustrar melhor o conceito, vamos fingir que estamos procurando todas as moedas mencionadas em um texto. Para ter certeza de que o caractere é uma moeda, precisaremos que ele seja seguido imediatamente por um número (caso contrário, poderíamos encontrar muitos dólares americanos na documentação do PHP, onde as variáveis ​​são prefixadas com $). Estamos apenas procurando corresponder os sinais de moeda, mas há uma restrição adicional que precisamos procurar (mas está fora do escopo do que procuramos corresponder).

Existem 4 afirmações: lookahead positivo (= seguido por um certo padrão), lookahead negativo (= não seguido por um certo padrão), lookbehind positivo (= precedido por um certo padrão) e lookbehind negativo (= não precedido por um certo padrão )

Asserções não estão sendo capturadas e, como resultado, não podem ser referenciadas.

Olhe para frente

  • Positivo: (?=pattern)
  • Negativo: (?!pattern)

Olhar para trás

  • Positivo: ,(?<=pattern)
  • Negativo: (?<!pattern)

Cuidado : em PHP, as afirmações de lookbehind devem ter comprimento fixo, caso contrário, você será saudado com um . Meio de comprimento fixo que você deve evitar o uso de quantificadores não-fixos, como , , ou . Em asserções antecipadas, é perfeitamente aceitável usar quantificadores de comprimento variável, por exemplo , mas em asserções antecipadas, você não pode. Bem, não em PHP.Warning: Compilation failed: lookbehind assertion is not fixed length
*+?{1,2}(?=.*?blah)

Exemplo

$test = 'I found a €5 note today.
$this, however is just a simple PHP variable.'
;

Se estivermos procurando resolver o problema mencionado de encontrar todas as moedas em um texto, notaremos que neste texto o símbolo € é usado como moeda EUR, enquanto $ não representa USD aqui. Queremos verificar se os símbolos de moeda são realmente seguidos por um número:

if(preg_match_all('/[$€£¥](?=[0-9])/u', $test, $matches))
{
var_dump
($matches);
}

Observe também como o modificador de padrão PCRE_UTF8 é usado para fazer a expressão regular interpretar corretamente os símbolos de moeda UTF8 multibyte.

O resultado desta solução corresponderá com precisão apenas ao símbolo EUR:

array
0 =>
array

0 => string '€' (length=3)

Comentários

Documentos PHP

Eu o encorajo a escrever documentação para suas expressões regulares. As expressões regulares já são suficientemente difíceis de criar, mas são ainda mais difíceis de decifrar sem contexto suficiente.

Comente-os corretamente: não há necessidade de dividi-los em várias strings separadas e concatená-los em PHP, apenas para poder adicionar comentários no estilo PHP. Comentários de estilo Perl podem ser adicionados em linha, em uma expressão regular, por meio do uso do modificador de padrão PCRE_EXTENDED. O uso deste modificador resultará em espaços em branco sem escape sendo ignorados em sua regex.

/
# match currency symbols for USD, EUR, GBP & YEN
[$€£¥]
# currency symbols must be followed by number, to indicate price
(?=[0-9])
# pattern modifiers: u for UTF-8 interpretation (currency symbols),
# x to ignore whitespace (for comments)
/ux

Tudo o que segue o # será considerado um comentário, até o final da linha / regex. O modificador x irá garantir que as guias antes e novas linhas após os comentários também sejam ignoradas.

Fim

Se você simplesmente não se cansa, você pode dar uma olhada nesta apresentação que carreguei no SpeakerDeck . Não é nada mais do que uma versão compacta das informações do tutorial básico e avançado, embora com alguns outros exemplos.

Acho que agora você já aprendeu a apreciar o poder que as expressões regulares exercem. Agora, você sempre terá seu conhecimento aprimorado de regex para salvar seu traseiro ao lidar com dados estruturados complexos, mas não fique cego para outras soluções. Embora as possibilidades sejam infinitas, dependendo de sua tarefa específica, outras soluções podem ser muito superiores, como um analisador baseado em DOM / SAX para XML.

Observe que os exemplos de código foram centrados em explicar com precisão um assunto específico e podem não abranger todos os casos extremos. Por motivos de clareza, @ import-regex ignora espaços em branco e delimitadores de aspas simples, e os nós XML ignoram as tags de fechamento automático.