Expressões Regulares(Regex) - PHP

Encontrar padrões de caracteres em textos é uma tarefa bem rotineira no dia a dia de um desenvolvedor, seja para realizar uma substituição ou a validação de uma entrada de um usuário. As Expressões regulares(regex) facilitam isso permitindo criar expressões customizadas através de diversos delimitadores e meta-caracteres para verificar correspondências dentro de um texto.

O PHP possui diversas funções que recebem expressões regulares, entre as mais utilizadas temos as preg_match, preg_match_all e preg_replace que utiliza expressões PCRE.

A função preg_match realiza a busca em uma string dado uma um pattern(expressão regular). Esta função retorna 1 se o pattern for encontrado e 0 se não. A função preg_match encontra somente a primeira ocorrência do padrão.

$texto = "#FFAACC";
$pattern= "/#[0-9a-f]{6}/i"; 
if(preg_match($pattern, $texto) == 1){
    echo "É uma cor RGB Hexadecimal \n";
}

Podemos recuperar qual o valor que correspondeu a expressão regular dada. Para isso passamos uma variável como terceiro valor. Nela será gravado o valor encontrado, sendo a primeira posição do array contendo a correspondência completa e nas posições seguintes as correspondencia dos grupos dentro de nossa expressão regular(veremos grupos quando explicarmos como criar uma expressão).

$texto = "A cor é #FFAACC!";
$pattern= "/#[0-9a-f]{6}/i"; 
preg_match($pattern, $texto, $match);
if( !empty($match) ){
    echo "É $match[0] uma cor RGB Hexadecimal \n";
}
//É #FFAACC uma cor RGB Hexadecimal

A função preg_match_all é muito semelhante a a preg_match, mas ela busca todas as ocorrências encontradas no texto.

$texto = "Lista de cores: #FFAACC, #CC3311, #9912FC e #EE4545";
$pattern= "/#[0-9a-f]{6}/i"; 
preg_match_all($pattern, $texto, $matches);
if( !empty($matches) ){
    foreach($matches[0] as $match){
        echo "Encontrado $match\n";
    }
}
//Encontrado #FFAACC
//Encontrado #CC3311
//Encontrado #9912FC
//Encontrado #EE4545

As ocorrências encontradas são armazenadas em uma matriz, sendo o primeiro índice dele contendo o array com todas as ocorrências completas, e os demais índices com as ocorrências agrupadas pelos grupos definidos na expressão regular(veremos mais para frente).

A função preg_replace realiza uma busca por em um texto através de uma expressão regular e a substitui, retornando assim o novo valor.

$texto = "O Evento irá ocorrer no dia 11";
$pattern = '/\d{2}/';
echo preg_replace($pattern,'15', $texto) . PHP_EOL;
//O Evento irá ocorrer no dia 15

Pode-se também passar um quarto parâmetro inteiro informando quantas substituições no máximo serão realizadas. Também é possível passar um quinto parâmetro como um variável que receberá o número de substituições que foram feitas.

Podemos utilizar uma das próprias correspondências encontradas pela regex como parte do texto a ser substituído, bastando adicionar “$<posição>” onde se deseja o valor, sendo posição o índice da correspondência como no array match utilizado na função preg_match. Este recurso é útil, por exemplo, para encontrar urls dentro de um texto e transformá-lo em um link HTML.

$texto = "segue o link https://www.botecodigital.dev.br .";
$pattern= "/([a-z]+:\/\/)([a-z0-9\-_]+\.[a-z0-9-\_\.]+)(\/[a-z0-9\-_\/]+)*/";

$texto = preg_replace($pattern, '<a href="$1">$1</a>', $texto);

echo $texto;
//segue o link <a href="https://www.botecodigital.dev.br">https://www.botecodigital.dev.br</a>.

Construindo uma Expressão Regular(regex)

Uma expressão regular é um conjunto de regras que uma string deve seguir. Para definir estas regras são utilizados diversos caracteres cada um simbolizando uma regra a ser atendida.

Começamos definindo o delimitador da expressão regular. Toda expressão regular é uma string que deve começar e terminar com um caractere delimitador, embora esse possa ser qualquer caractere não alfanumérico, é utilizado como padrão a barra “/”. Por exemplo "/PHP/" é uma expressão regular com os delimitadores “/” e que busca exatamente a palavra PHP. Qualquer caractere dentro dos limitadores que não for um símbolo com algum significado especial dentro das expressões regulares terá uma correspondência literal

Como buscar uma palavra exata é um pouco limitado, podemos utilizar um símbolo que representam um tipo de caractere.

SímboloTipo de caractere
\dUm caractere de digito decimal. Ex.: “1”,”2″, “3”, etc
\DUm caractere que não seja digito. Ex.: “a”, “@”, “$”, “.”, etc
\wUm caractere de “palavras”(letra, digito, ou undercore). Ex. “a”, “B”, “2”, etc
\WUm caractere que não um caractere de “palavra”. Ex.: ” “, “@”, “$”, “&”, “.”, etc
\sUm caractere de espaço em branco. Ex.: ” “, “\t”, “\r”, “\f”.
\SUm caractere que não seja um espaço em branco.
.Qualquer caractere. Ex.: “a”,”1″,”@”,”\t”,”\n”.
Obs.: Como o ponto é um símbolo com significado especial dentro de um expressão regular, para representar o caractere ponto “.” propriamente dito dentro de uma string devemos escapá-lo com uma “\”, ou seja “\.”.

Com os símbolos acima já podemos construir algumas expressões regulares se quisermos, por exemplo a expressão "/\d\d\d/" encontra valores compostos por três dígitos seguidos. Embora esta expressão funcione ela não é muito prática, principalmente o numero de inteiros for grande. Para isso temos os quantificadores.

Quantificadores são símbolos que definem quantas vezes um caractere(grupo ou classe) pode ser repetido em sequencia. Ele é informado logo após a definição do caractere na expressão.

SimboloQuantidade
?Zero ou um
*Zero ou mais
+Um ou mais
{}{4} – Exatamente quatro
{2,6} – Entre dois e seis vezes
{5,} – Mais que cinco

* Lembrando que como os caracteres quantificadores possuem significado especial dentro da expressão regular se quisermos construir uma expressão que represente um padrão de texto que possua estes caracteres de forma literal devemos escapá-los com um contra-barra “\” . Ex.: “\*”, “\+”, “\?”.

Agora já podemos criar algumas expressões por exemplo para encontrar uma string no formato de um CEP.

"/\d{5}-\d{3}/" : Definimos que o padrão de busca é 5 caracteres de digito seguido de um caractere de traço(“-“) e logo após três caracteres de digito. Este padrão encontra: “04429-003”, “18017-186”, “13467-706”, mas não encontra: “23423-0”, “aaaaa-bbb”, “14785236”.

"/\d{3}\.\d{3}\.\d{3}-\d{2}/": Definimos que o padrão de busca é 3 digitos, seguidos de um caractere ponto(note que ele foi escapado com uma “\”), seguido 3 digitos, mais um caractere de ponto, com mais 3 caracteres de digitos, seguidos de um caractere de barra(‘-“) e por fim com mais dois caracteres de digito. Este padrão encontra: “826.266.720-41”, “893.427.460-39”, mas não encontra: “52323482050”, “842.20.340-97” e “842@250@340-97”.

"/go+gle/": Define um padrão de busca para da palavra google, mas aceitando um ou mais caracteres “o”. Ele irá encontrar “gogle”, “google”, “gooogle” ou “goooooogle”, mas não encontrará “ggle”

Ou “|”

Outro meta-caractere comum é o “|” ele representa que a expressão deve corresponder a um padrão ou outro. Bastante útil quando queremos corresponder a uma expressão bem especifica ou outra.

$texto = "-> Teste";
$pattern= "/^-> .+|^=> .+/"; 
preg_match($pattern, $texto, $match);

print_r($match);

//Array
//(
//    [0] => -> Teste
//)

No exemplo acima definimos uma expressão em que o texto deve começar com os caracteres "-> " (expressão ^-> .+) ou com "=> " (expressão ^=> .+).

Classes de caracteres

Em alguns casos desejamos que um caractere tenha um grupo mais reduzido que todos os caracteres abrangidos pelo meta caractere \w por exemplo, ou por um numero limitado de dígitos. Isto pode ser obtido utilizando os caracteres “[]” e dentro deles colocarmos a lista de caracteres válido.

"/[abc]+/": Este padrão defini um ou mais(quantificador “+”) caracteres “a”, “b” ou “c”. Ele irá encontrar “aaa”, “bcc”,”a”,”c”, mas não ira encontra “d” ou “dede”.

"/[abc012]*/": Este padrão defini zero ou mais(quantificador “*”) caracteres “”, “a”, “b”, “c”, “0”, “1” ou “2”, Ele irá encontrar “aabb11”, “12ab”, “a0b1c2” mas não irá encontrar “ff” ou “55d”.

Quando temos um conjunto de caracteres que correspondem a uma sequencia podemos abrevia-los utilizando um “-“, por exemplo uma classe que compreende todas as letras poderia ser definido por "/[a-z]+/" ou uma que compreenda todos os dígitos “/[0-9]/”. Também é possível combinar dois intervalos, por exemplo, “/[a-zA-Z]/” que defini um conjunto de todas as letras minúsculas e todas as letras maiúsculas.

/#[0-9a-fA-F]{6}/: Este padrão defini que a string deve começar com um cerquilha(#) seguido de seis caracteres que devem ser um digito de 0 até 9 ou uma letra de a minúsculo até z maiúsculo ou uma letra de A minúsculo até Z maiúsculo. Ele irá encontrar: #FFCCD1, #CC1212, #dfdee1, #A0b6F9, mas não irá encontrar: #G12C12 ou #CdCCdde

Boundary

Em alguns casos estamos procurando um padrão de caracteres que deve estar no inicio ou final de uma linha, para isso temos alguns caracteres que podemos utilizar para definir isto.

SímboloDescrição
^Corresponde ao inicio de uma linha se a expressão está em modo multi-line ou inicio do texto inteiro.
$Corresponde ao final de uma linha se a expressão esta em modo multi-line ou ao final do texto inteiro.
\bO limite de uma palavra.
\BNão é o limite de uma palavra.
\AInicio do texto inteiro.
\ZFinal do texto inteiro.

"/^[a-zA-Z][a-zA-Z0-9_]*$/": Define um padrão que deve começar com uma letra de “a” até “z” seguido de zero ou mais caractere de “a” até “z”, digito ou underscore até o final da linha. Este padrão encontra “linguagem”, “teste123”, mas não encontra “2error” ou ” um teste”.

"/\b[a-zA-Z]{5}\b/": Defini um padrão de string de uma palavra de cinco letras. Este padrão encontra dentro do texto “teste busca regex rodrigo abc1def” as strings “teste”, “busca” e “regex”.

Capturando Grupos

Partes da expressão regular podem ser agrupados para serem mais facilmente recuperados no array de match das funções preg_match e preg_match_all.

Um grupo em uma expressão regular é definido utilizando o parênteses ” ( ) “, podendo este receber um nome para tornar mais fácil a recuperação desta parte pelas funções. Isto é feito através da marcação (?<nome>expressao).

Para exemplificar vamos fazer uma expressão simples apenas para exemplo(não irá pegar muitas urls) para pegar as partes de uma URL.

$texto = "https://www.botecodigital.dev.br/php/teste-regex";
$pattern = '/^(?<protocolo>[a-z]+:\/\/)(?<dominio>[a-z0-9\-_]+\.[a-z0-9-\_\.]+)(?<path>\/[a-z0-9\-_\/]+)*$/';
$result = preg_match_all($pattern,$texto, $match);

print_r($match);
//Array
//(
//    [0] => Array(
//            [0] => https://www.botecodigital.dev.br/php/teste-regex
//        )
//    [protocolo] => Array(
//            [0] => http://
//        )
//    [1] => Array(
//            [0] => http://
//        )
//    [dominio] => Array(
//            [0] => www.botecodigital.dev.br
//        )
//    [2] => Array(
//            [0] => www.botecodigital.dev.br
//        )
//    [path] => Array(
//            [0] => /php/teste-regex
//        )
//    [3] => Array(
//            [0] => /php/teste-regex
//        )
//)

Como podemos ver a expressão regular possui três grupos definidos pelos parênteses, sendo o primeiro grupo (?<protocolo>[a-z]+:\/\/) com o nome protocolo sendo um ou mais caracteres de a até z seguidos de dois pontos e duas barras. Esta parte da expressão regular(quando um valor for encontrado) será disponível no array $match através do índice “protocolo”.

Grupos também podem ser referenciados na função de preg_replace.

Greedy e Lazy

Por padrão a correspondência de uma expressão regular é greedy(gananciosa), ou seja, vai tentar corresponder ao maximo de caracteres possível. Um exemplo deste comportamento pode ser visto ao tentar realizar uma busca por marcações HTML, vejo o exemplo:

$texto = "Uma <strong>frase importante</strong>!";
$pattern = '/<.+>/';
$result = preg_match_all($pattern,$texto, $match);

print_r($match);
//Array
//(
//    [0] => Array
//        (
//            [0] => <strong>frase importante</strong>
//        )
//)

Observando o padrão podemos pensar que somente irá corresponder a <strong> e </strong>, mas não é isso que vemos, o conteúdo entre as marcações também foi incluído pois o padrão utilizado foi qualquer caractere ( . ) entre “<” e “>” uma ou mais vezes. Então ele começa procurar na string no caractere 5 ele começa a bater o padrão com o “<” a partir dai ele vai encontrado até encontrar o “>” no caractere 12, que também corresponde ao meta-caractere “.” então ele segue encontrando correspondências até encontrar o último “>” no caractere 37 fechando assim padrão completo.

Podemos mudar este comportamento na nossa expressão para lazy (preguiçoso) para pegar o menor numero de correspondência possível adicional um “?” após o quantificador. Veja o exemplo no “modo” lazy.

$texto = "Uma <strong>frase importante</strong>!";
$pattern = '/<.+?>/';
$result = preg_match_all($pattern,$texto, $match);

print_r($match);

//Array
//(
//   [0] => Array
//        (
//            [0] => <strong>
//            [1] => </strong>
//        )
//)

Expressões Regulares comuns

Algumas expressões regulares comuns para exemplo:

  • "/^\d+$/": Números inteiros
  • "/^\d+(\.\d+)?$/": Números decimais
  • /^-?\d+(\.\d+)?$/": Números decimais com sinal
  • "/^[a-zA-Z0-9]*$/": Alfanumérico sem espaço
  • "/^[a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/": E-mail
  • "/^[a-z0-9_-]{3,16}$/": nome de usuário com o numero de caracteres entre 3 e 16
  • "/\b(?:(?:https?|ftp):\/\/|www\.)[-a-z0-9+&@#\/%?=~_|!:,.;]*[-a-z0-9+&@#\/%=~_|]/": URL
  • "/^\d{3}\.\d{3}\.\d{3}\-\d{2}$/": CPF
  • "/^\d{2}\.\d{3}\.\d{3}\/\d{4}\-\d{2}$/": CNPJ
  • "/^\d{5}-\d{3}$/": CEP
  • "/^\(\d{2}\)?\s?\d{5}-?\d{4}$/": TELEFONE
  • "/^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:[.](?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$/”: IP

Modificadores de Padrões

Podemos adicionar alguns modificadores após o delimitador final da expressão regular para modificar como a expressão é interpretada. Eles são colocados após o delimitador final da expressão regular. Ex.: "/PHP/i"

ModificadorFunção
iTorna a expressão case-Insensitive
m Habilita o modo multi-line. Por padrão a string alvo é tratada como uma única linha, mesmo se esta possui múltiplos caracteres de nova linha. Quando o modo multi-line esta habilitado os os mate-caracteres ^ e $ passam a considerar os caracteres de nova linha.
xQuando utilizado caracteres de whitespace são ignorados por padrão e para ser utilizado devem ser escapado.
sPermite que o meta-caractere “.” corresponda a novas linhas, por padrão ele não corresponde.
UInverte a gulosidade padrão dos quantificadores. Por padrão passa a ser lazy e se seguido de ? passará a ser greedy

Para testar mais facilmente as expressões regulares você pode utilizar um testador online como o https://www.phpliveregex.com/ para facilitar.

Bom era isso, com estes conceitos já é possível entender um pouquinho de expressões regulares, é um assunto amplo e escrever boas expressões não é tarefa simples, mas ajudam muito no desenvolvimento.

T++