Dominando Expressões Regulares em PHP, Parte 1: Perl Pode Ser o Rei da Regex, mas PHP Pode Reorganizar Entrada Rapidamente, Também

A correspondência de padrão é uma tarefa tão comum para software que uma taquigrafia especial — expressões regulares — evoluiu para tornar leve o trabalho da tarefa. Aprenda como usar essa taquigrafia em seu código aqui na Parte 1 desta série " Dominando Expressões Regulares em PHP".

Martin Streicher , Software Developer, 自由职业者

Photo of Martin StreicherMartin Streicher é chefe executivo em tecnologia da McClatchy Interactive, editor chefe da Linux Magazine, um desenvolvedor da Web e um colaborador regular do developerWorks. Ele possui mestrado em ciência da computação pela Purdue University e programa sistemas semelhantes ao UNIX desde 1986.



01/Jan/2008

Todas as máquinas consomem entrada, executam algum tipo de trabalho e produzem saída. Um telefone, por exemplo, converte energia sonora em um sinal elétrico e retorna para áudio para possibilitar a conversa. Um motor absorve combustível (vapor, fissão, gasolina ou esforço físico) e transforma-o em trabalho. E um liquidificador devora rum, gelo, limão e curaçau e mistura vigorosamente para produzir um Mai Tai. (Ou, caso prefira algo mais metropolitano, experimente um pouco de champanhe e néctar de pêra para apreciar um Bellini. O liquidificador é uma máquina realmente incrível e flexível.)

Como software transforma dados, cada aplicativo também é uma máquina — apesar de ser uma "virtual", considerando a ausência de partes físicas. Um compilador, por exemplo, espera o código de origem como entrada e transforma-o em código binário adequado para execução. Um modelador de clima produz previsões baseadas em medições históricas. E um editor de imagem consome e emite pixels, aplicando regras a cada pixel ou grupos de pixels para, digamos, tornar uma imagem mais nítida ou estilizá-la.

Exatamente como qualquer outra máquina, um aplicativo de software espera determinadas matérias primas, como uma lista de números, dados encapsulados em um esquema XML ou um protocolo. Se um programa receber o material errado — divergente em tipo ou formato — o resultado será provavelmente imprevisível, até mesmo catastrófico. Como o ditado diz: "Tudo que entra, sai".

Praticamente todos os problemas não triviais requerem que você filtre bons dados dos ruins, rejeite os dados ruins para evitar saída errante ou ambos. Esse é certamente o caso para um aplicativo da Web do PHP. Se a entrada vem de um formulário manual ou de um pedido programático Asynchronous JavaScript + XML (Ajax), o programa deve examinar cuidadosamente as informações recebidas antes de qualquer computação poder ocorrer. Um valor numérico pode precisar estar em um determinado intervalo ou ser restringido a um número inteiro. Um valor pode precisar corresponder a um formato específico, como um código postal de entrega. Por exemplo, um CEP nos EUA tem cinco dígitos mais um qualificador opcional "Mais 4" composto por um hífen e quatro dígitos adicionais. Outras strings podem precisar ter um determinado número de caracteres, também, como duas letras para a abreviação de um estado dos EUA. Strings são especialmente nefandas: um aplicativo PHP deve permanecer vigilante com relação a um ator malicioso integrando consultas SQL, código JavaScript ou outro código capaz de alterar o comportamento do aplicativo ou burlar a segurança.

Mas como um programa sabe se a entrada é numérica ou está em conformidade com uma convenção, como um código de endereçamento postal? Fundamentalmente, para executar uma correspondência é necessário um pequeno analisador — criar uma máquina de estado, ler entrada, processar tokens, monitorar estado e produzir um resultado. No entanto, mesmo um analisador simples pode ser difícil de criar e manter.

Felizmente, análises de correspondência de padrão são tão comumente necessárias na computação que uma taquigrafia especial e, sim, mecanismo evoluíram ao longo do tempo (desde a aurora do UNIX® para tornar leve o trabalho da tarefa. Uma expressão regular (regex) descreve padrões em uma notação concisa e legível. Considerando uma regex e um dado, um mecanismo da regex determina se o dado corresponde a um padrão e, se uma correspondência for encontrada, qual foi a correspondência.

Segue um exemplo resumido da aplicação de uma regex, retirado do utilitário de linha de comando do UNIX grep, que procura um padrão especificado entre o conteúdo de um ou mais arquivos de texto do UNIX. O comando grep -i -E '^Bat' procura a seqüência beginning-of-line (indicada pelo circunflexo, [^]), seguido, imediatamente por letras maiúsculas ou minúsculas b, a e t (a opção -i ignora maiúsculas e minúsculas em correspondências padrão, portanto, B e b são equivalentes, por exemplo). Portanto, considerando o arquivo heroes.txt:

Lista 1. heroes.txt
Catwoman
Batman
The Tick
Black Cat
Batgirl
Danger Girl
Wonder Woman
Luke Cage
The Punisher
Ant Man
Dead Girl
Aquaman
SCUD
Blackbolt
Martian Manhunter

O comando grep acima mencionado produziria duas correspondências:

Batman
Batgirl

Expressões Regulares

O PHP oferece duas interfaces de programação de regex, uma para Portable Operating System Interface (POSIX) e outra para Perl Compatible Regular Expressions (PCRE). Em geral, a segunda interface é preferida, pois PCRE é muito mais poderosa do que a implementação POSIX, oferecendo todos os operadores encontrados no Perl. Leia a documentação do PHP para saber mais sobre as chamadas de função regex da POSIX (consulte Recursos). Aqui, foco os recursos da PCRE.

A regex da PCRE do PHP contém operadores a serem correspondidos com relação aos caracteres específicos e outros operadores; com relação a um local específico, como o início ou fim de uma string; ou com relação ao início ou fim de uma palavra. Uma regex também pode descrever alternativas, que você pode descrever como "isto" ou "aquilo"; repetição com comprimento fixo, variável ou indefinido; configura os caracteres (por exemplo, "qualquer uma das letras de a até m"); e classes ou tipos de caracteres (caracteres para impressão ou pontuação), entre outras técnicas. Operadores especiais em regexes também permitem agrupamento — uma maneira de aplicar um operador a outros operadores em um grupo.

A Tabela 1 mostra alguns dos operadores comuns da regex. Você pode concatenar e combinar os primitivos da Tabela 1 (e outros operadores) e usá-los em combinação para construir regexes (muito) complexas.

Tabela 1. Operadores Comuns da Regex
OperadorPropósito
. (ponto)Corresponder a qualquer caractere único
^ (circunflexo)Corresponder à string vazia que ocorre no início de uma linha ou string
$ (símbolo do dólar)Corresponder à string vazia que ocorre no final de uma linha
ACorresponder a uma letra maiúscula A
aCorresponder a uma letra minúscula a
\dCorresponder a qualquer dígito único
\DCorresponder a qualquer caractere não dígito
\wCorresponder a qualquer caractere alfanumérico; um sinônimo é [:alnum:]
[A-E]Corresponder a qualquer letra maiúscula A, B, C, D ou E
[^A-E]Corresponder a qualquer caractere, exceto à letra maiúscula A, B, C, D ou E
X?Corresponder a nenhuma ou a uma letra maiúscula X
X*Corresponder a zero ou mais letras maiúsculas X
X+Corresponder a uma ou mais letras maiúsculas X
X{n}Corresponder exatamente a n letras maiúsculas X
X{n,m}Corresponder a pelo menos n e não mais do que m letras maiúsculas X ; se você omitir m, a expressão tenta corresponder pelo menos nX
(abc|def)+Corresponder uma sequência de pelo menos um abc e def;abc e def corresponderiam

Segue um exemplo de um uso comum de uma regex. Digamos que seu Web site precise que cada usuário crie um login. Cada nome de usuário deve ter pelo menos três, mas não mais do que 10 caracteres alfanuméricos e deve começar com uma letra. Para impingir essas especificações, você poderia usar a seguinte regex para validar o nome do usuário quando é enviado a seu aplicativo: ^[A-Za-z][A-Za-z0-9_]{2,9}$.

O circunflexo corresponde ao início da string. O primeiro conjunto, [A-Za-z], representa qualquer letra. O segundo conjunto, [A-Za-z0-9_]{2,9}, representa uma série de pelo menos duas e até nove de qualquer letra, qualquer dígito e o sublinhado. E o símbolo de dólar ($) corresponde ao final da string.

À primeira vista, o símbolo de dólar pode parecer desnecessário, mas é crítico. Se você omiti-lo, sua regex corresponderá a qualquer string que comece com uma letra, contenha de dois a nove caracteres alfanuméricos e qualquer número de qualquer outro caractere. Ou seja, sem o símbolo de dólar para fazer a âncora do final da string, uma string muito longa com um prefixo correspondente, como "martin1234-cruft", produziria um falso positivo.


Programando PHP e Regexes

O PHP fornece funções para localizar correspondências em texto, substituir cada correspondência por outro texto (através de procura e substituição) e para localizar correspondências entre os elementos de uma lista. As funções são:

  • preg_match()
  • preg_match_all()
  • preg_replace()
  • preg_replace_callback()
  • preg_grep()
  • preg_split()
  • preg_last_error()
  • preg_quote()

Para demonstrar as funções, vamos escrever um pequeno aplicativo PHP que procure em uma lista de palavras um padrão específico, onde as palavras e a regex são fornecidas por um formulário da Web tradicional e os resultados são ecoados no navegador usando a função simple print_r() . Esse pequeno programa é útil se você quiser testar ou refinar uma regex.

A Lista 2 mostra o código do PHP. Toda a entrada é fornecida através de um formulário HTML simples. (O formulário correspondente não é mostrado e o código para efetuar trap dos erros no código do PHP foi omitido por brevidade.)

Lista 2. Comparar Texto a um Padrão
<?php
	//
	// divide the comma-separated list into individual words
	//   the third parameter, -1, permits a limitless number of matches
	//   the fourth parameter, PREG_SPLIT_NO_EMPTY, ignores empty matches
	//
 $words = preg_split( '/,/',  $_REQUEST[ 'words' ], -1, PREG_SPLIT_NO_EMPTY );

	//
	// remove the leading and trailing spaces from each element
	//
	foreach ( $words as $key => $value ) { 
		$words[ $key ] = trim( $value ); 
	}

	//
	// find the words that match the regular expression
	//
	$matches = preg_grep( "/${_REQUEST[ 'regex' ]}/", $words );

	print_r( $_REQUEST['regex' ] ); 
	echo( '<br /><br />' );
	
	print_r( $words ); 
	echo( '<br /><br />' );
	
	print_r( $matches );
	
	exit;
?>

Primeiro, a string de palavras separadas por vírgula é dividida em elementos individuais usando a função preg_split() . Essa função divide a string em todos os pontos que correspondem à regex fornecida. Aqui, a regex é simplesmente , (uma vírgula, o delimitador epônimo de uma lista separada por vírgula). As barras à esquerda e à direita mostradas no código simplesmente indicam o início e o fim da regex.

O terceiro e o quatro argumentos de preg_split() são opcionais, mas cada um é útil. Forneça um número inteiro, n, para que o terceiro argumento retorne somente as n primeiras correspondências; ou forneça -1 para todas as correspondências. Se você especificar um quarto argumento, o sinalizador PREG_SPLIT_NO_EMPTY, preg_split() elimina quaisquer resultados vazios.

Em seguida, cada elemento da lista de palavras separadas por vírgula é reduzido (os espaços em branco à esquerda e à direita são eliminados) através da função trim() , em seguida, comparada à regex fornecida. A função, preg_grep(), facilita muito o processamento de uma lista: simplesmente forneça o padrão como o primeiro argumento e uma array de palavras para corresponder como o segundo argumento. A função retorna uma array de correspondências.

Por exemplo, se você digitar a regex ^[A-Za-z][A-Za-z0-9_]{2,9}$ como o padrão e uma lista de palavras de comprimentos variados, poderá obter algo semelhante à Lista 3.

Lista 3. Resultado de uma Regex Simples
^[A-Za-z][A-Za-z0-9_]{2,9}$
Array ( [0] => martin [1] => 1happy [2] => hermanmunster )
Array ( [0] => martin )

Por sinal, você pode inverter a operação preg_grep() e localizar elementos que não correspondem ao padrão (o mesmo que grep -v na linha de comando) com o sinalizador opcional PREG_GREP_INVERT. Substituir a linha 22 por $matches = preg_grep( "/${_REQUEST[ 'regex' ]}/", $words, PREG_GREP_INVERT ) e reutilizar a entrada da Lista 3 produz Array ( [1] => 1happy [2] => hermanmunster ).


Decompondo strings

As funções preg_split() e preg_grep() são excelentes pequenas funções. A primeira pode decompor uma string em substrings se as substrings forem separadas por um padrão previsível. A função preg_grep() também pode filtrar uma lista rapidamente.

Mas o que acontece se uma string precisar ser decomposta usando uma ou mais regras complexas? Por exemplo, os números de telefone nos EUA frequentemente aparecem como "(305) 555-1212", "305-555-1212" ou "305.555.1212". Se você remover a pontuação, tudo é reduzido para 10 dígitos, o que é fácil de ser reconhecido como uso da regex \d{10}. No entanto, o código de área de três dígitos e o prefixo de três dígitos dos números de telefone nos Estados Unidos não podem iniciar por zero nem por um (pois ambos são prefixos para chamadas não locais). Em vez de dividir a sequência numérica em dígitos individuais e escrever código complexo, uma regex pode testar a validade.

A Lista 4 mostra um trecho de código para executar essa tarefa.

Lista 4. Determinar se um Número de Telefone é um Número de Telefone Válido dos EUA
<?php   
	$punctuation = preg_quote( "().-" );
	$number = preg_replace( "/[$punctuation]/", '', $_REQUEST[ 'number' ] );

	$valid = "/[2-9][0-9]{2}[2-9][0-9]{2}[0-9]{4}/";	
	if ( preg_match( $valid, $number ) == 1 ) {
		echo(  "${_REQUEST[ 'number' ]} is valid<br />" );
	}
		
	exit;
?>

Vamos percorrer o código:

  • Conforme mostrado na Tabela 1, regexes usam um pequeno conjunto de operadores, como colchetes ([ ]), para denominar um conjunto. Se quiser corresponder tal operador em texto de assunto, você deve "escapar" o operador na regex com uma barra invertida anterior (\). Após escapar o operador, corresponde como qualquer outro literal. Por exemplo, se quiser corresponder um ponto literal, digamos, conforme encontrado em um nome de host completo, escreva \.. Como opção, você pode passar uma string para preg_quote() para escapar automaticamente qualquer operador da regex localizado, como na linha 1. Se você usar echo() $punctuation após a linha 1, deverá ver \(\)\.-.
  • A linha 2 remove toda a pontuação do número do telefone. A função preg_replace() substitui qualquer ocorrência de um caractere em $punctuation— portanto, os operadores configurados [ ]— com a string vazia, eliminam de forma efetiva os caracteres. A nova string é retornada e designada para $number.
  • A linha 4 define o padrão para um número de telefone válido dos EUA.
  • A linha 5 executa a correspondência, comparando o número de telefone agora somente com dígitos ao padrão. A função preg_match() retorna 1 se houver uma correspondência. Se nenhuma correspondência for localizada, preg_match() retorna um zero. Se tiver ocorrido um erro durante o processamento, a função retorna False. Assim, para verificar o sucesso, consulte se o valor de retorno é 1. Caso contrário, verifique o resultado de preg_last_error() (se você usar o PHP V5.2.0 ou posterior). Se não for zero, você pode ter excedido um limite de computação, como até onde uma regex pode ocorrer novamente. Você pode encontrar uma discussão sobre as constantes e os limites usados com as regexes do PHP na página Funções de Expressões Regulares da PCRE (consulte Recursos).

Capturas

Há muitas instâncias em que um teste "Isso corresponde?" é tudo de que se precisa — como em validação de dados. Mais frequentemente, no entanto, uma regex é usada para provar uma correspondência e para extrair informações sobre a correspondência.

Retornando ao exemplo do número de telefone, se uma correspondência for feita, você pode querer armazenar o código de área, o prefixo e o número da linha em campos separados de um banco de dados. Regexes podem se lembrar do que foi correspondido com capture. O operador capture é o parêntese e o operador pode aparecer em qualquer parte da regex. Você também pode aninhar capturas para localizar sub-segmento de capturas maiores. Por exemplo, para capturar o código de área, o prefixo e o número da linha do telefone de 10 dígitos, você pode usar:

/([2-9][0-9]{2})([2-9][0-9]{2})([0-9]{4})/

Se ocorrer uma correspondência, os três primeiros dígitos são capturados no primeiro conjunto de parênteses, os três dígitos seguintes no segundo conjunto e os quatro dígitos finais no operador restante. Uma variação da chamada de preg_match() recupera as capturas.

Lista 5. Como preg_match()
$valid = "/([2-9][0-9]{2})([2-9][0-9]{2})([0-9]{4})/";	
if ( preg_match( $valid, $number, $matches ) == 1 ) {
	echo(  "${_REQUEST[ 'number' ]} is valid
" ); echo( "Entire match: ${matches[0]}
" ); echo( "Area code: ${matches[1]}
" ); echo( "Prefix: ${matches[2]}
" ); echo( "Number: ${matches[3]}
" ); }

Se você fornecer uma variável como o terceiro argumento para preg_match() , como $matches aqui, é configurada para uma lista de resultados de capturas. O elemento zeroth (indexado com 0) é a correspondência inteira; o primeiro elemento é a correspondência associada ao primeiro conjunto de parênteses, etc., respectivamente.

As capturas aninhadas capturam segmentos e sub-segmentos para praticamente qualquer profundidade. O truque com capturas aninhadas é prever onde cada correspondência aparece em uma array de correspondência, como $matches. Aqui está a regra a seguir: conte o número de parênteses esquerdos a partir do início da regex — a contagem é o índice para a array de correspondência.

A Lista 6 fornece um exemplo (um tanto quanto planejado) para extrair partes de um endereço.

Lista 6. Código para Extrair um Endereço
$address = "123 Main, Warsaw, NC, 29876";
$valid = "/((\d+)\s+(\w+)),\s+(\w+),\s+([A-Z]{2}),\s+(\d{5})/";
if ( preg_match( $valid, $address, $matches ) == 1 ) {
	echo(  "Street: ${matches[1]}<br />" );
	echo(  "Street number: ${matches[2]}<br />" );
	echo(  "Street name: ${matches[3]}<br />" );
	echo(  "City: ${matches[4]}<br />" );
	echo(  "State: ${matches[5]}<br />" );
	echo(  "Zip: ${matches[6]}<br />" );
}

Novamente, toda a correspondência está localizada no índice 0. Onde se encontra o número da rua? Contando a partir da esquerda, o número da rua é correspondido por \d+. O parêntese esquerdo é o segundo da esquerda; portanto, $matches[2] é 123. $matches[4] contém o nome da cidade, enquanto $matches[6] captura o CEP.


Técnicas de Poder

O processamento de texto é muito comum e o PHP fornece alguns recursos que facilitam grandes números de operações. Seguem alguns atalhos a serem lembrados:

  • A função preg_replace() pode operar um uma única string ou em uma array de strings. Se você chamar preg_replace() com uma array de strings em vez de uma string, todos os elementos da array são processados para fazerem substituições. Nesse caso, preg_replace() retorna uma array de strings modificadas.
  • Como com outras implementações PCRE, você pode fazer referência a uma correspondência de subpadrão a partir da substituição, permitindo que uma operação seja uma auto-referência. Para demonstrar, considere o problema de unificar um formato de número de telefone. Toda a pontuação é removida, substituída por pontos. Uma solução é mostrada na Lista 7.
Lista 7. Substituindo a Pontuação por Pontos
$punctuation = preg_quote( "().-" );
$number = preg_replace( "/[$punctuation]/", '', $_REQUEST[ 'number' ] );
$valid = "/([2-9][0-9]{2})([2-9][0-9]{2})([0-9]{4})/";	

$standard = preg_replace( $valid, "\\1.\\2.\\3", $number );
if ( strcmp ($standard, $number) ) {
	echo(  "The standard number is $standard<br />" );
}

O teste com relação ao padrão e a transformação em um número de telefone padrão se as correspondências de padrão ocorrerem em uma etapa.


Expresse-se

Os aplicativos PHP gerenciam cada vez mais grandes quantidades de dados. Se você precisa validar entrada de formulário ou decompor conteúdo, expressões regulares podem fazer o truque.

Recursos

Aprender

Obter produtos e tecnologias

Discutir

Comentários

developerWorks: Conecte-se

Los campos obligatorios están marcados con un asterisco (*).


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


A primeira vez que você entrar no developerWorks, um perfil é criado para você. Informações no seu perfil (seu nome, país / região, e nome da empresa) é apresentado ao público e vai acompanhar qualquer conteúdo que você postar, a menos que você opte por esconder o nome da empresa. Você pode atualizar sua conta IBM a qualquer momento.

Todas as informações enviadas são seguras.

Elija su nombre para mostrar



Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Los campos obligatorios están marcados con un asterisco (*).

(Escolha um nome de exibição de 3 - 31 caracteres.)

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


Todas as informações enviadas são seguras.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Software livre
ArticleID=382634
ArticleTitle=Dominando Expressões Regulares em PHP, Parte 1: Perl Pode Ser o Rei da Regex, mas PHP Pode Reorganizar Entrada Rapidamente, Também
publish-date=01012008