Expressões Regulares - Introdução, Práticas e Técnicas Avançadas

Fabiano Reese Righetti
Publicado em 15/10/2006

r4 - 15 Oct 2006 - DavidDias

Expressões Regulares - Introdução, Práticas e Técnicas Avançadas

----

* 1. Introdução
* 2. O que são "Expressões Regulares"
* 3. Introdução a REGEXP

* 3.1. Parâmetros
* 3.2. Grupo de Caracteres
* 3.3. Repetições e Ocorrências
* 3.4. Grupo Fixo
* 3.5. Negação
* 3.6. Início e Fim
* 4. Capturando Dados

* 4.1. Utilizando Retorno
* 5. Substituição/Replace
* 6. Parâmentros

* 6.1. Parâmetro 'g' - Global
* 6.2. Parâmetro 'e' - Execução de comandos no REGEXP
* 6.3. Parâmetro 'x' - Extendendo espaços
* 7. Práticas Comuns

* 7.1. while e sub-REGEXP
* 7.2. split
* 7.3. Execução em Busca
* 9. Técnicas Avançadas

* 9.1. Método is_quoted()
* 10. Referências

----

1. Introdução

Este artigo visa dar uma visão geral sobre Expressões Regulares ("Regular Expressions" ou "REGEXP") e também abordar algumas técnicas avançadas.

2. O que são "Expressões Regulares"

"Expressões Regulares", em inglês "Regular Expressions", e apelidado de REGEXP, é uma das ferramentas mais úteis em programação, e utilizada em Perl a muito tempo. Recentemente várias outras linguagens vêm introduzindo tal recurso, sendo a mais recente Java. Ou seja, em Perl REGEXP existe a mais de 10 anos, mas em java a apenas 2 anos, o que demonstra como REGEXP é um recurso importante, poderoso bem desenvolvido em Perl.

Com REGEXP pode-se checar o formato de uma string, formata-la, substituir dados, capturar dados, ou até mesmo criar um parser.

Mas por que abordar REGEXP?

* Porque quando você pega o domínio deste recurso você não larga mais.

3. Introdução a REGEXP

Uma das maneiras mais simples de utilizar REGEXP é checando um formato:

  my $var = "Contenho fulano no texto." ;
  if ( $var =~ /fulano/ ) {
    print "A variável possui 'fulano'.\n" ;
  }

Note que o REGEXP é indicado por '=~' e é delimitado por '/'.

Para modificar-se o delimitador utiliza-se o 'm', igual ao 'q' para strings:

  if ( $var =~ m~fulano~ ) {
    print "A variável possui 'fulano'.\n" ;
  }

3.1. Parâmetros

Agora que sabe-se a declaração, parâmetros de controle:

  my $var = "Contenho FuLaNo no texto." ;
  if ( $var =~ /fulano/i ) {
    print "A variável possui 'fulano'.\n" ;
  }

Note o 'i' no fim do REGEXP, indicando "Case Insensitive" (não fiferenciar maiúsculas e minúsculas).

Os demais parâmetros existentes:

  g => global: todas as ocorrências.
  s => single line: não parar em \n.
  x => extend spaces. Extende/ignora espaço no seu REGEXP, permitindo melhor organização.
  e => execute. Executa comandos quando usado em replace.
  m => multiline: inverso de 's'.

* O uso destes parâmentros será visto adiante.

3.2. Grupo de Caracteres

Você pode definir um grupo de caracteres:

  my $var = "aluno" ;
  ## ou: my $var = "aluna" ;
  if ( $var =~ /alun[oa]/ ) { print "match\n" ;}

Veja que o REGEXP irá identificar tanto 'aluno' como 'aluna', já que o grupo de caracteres foi definido com ' ao '.

Para definir um grupo sequencial utilize o '-' entre o caracter base e o final:

  my $var = "321" ;
  if ( $var =~ /[1-9]/ ) { print "Possuo um número.\n" ;}

O grupo 1-9 identifica os caracteres: 1 2 3 4 5 6 7 8 9

O mesmo pode ser feito com letras:

  my $var = "ABCdef" ;
  if ( $var =~ /[a-zA-Z]/ ) { print "Tenho letras maiúsculas ou minúsculas.\n" ;}

* Note que no grupo acima foram definidos 2 sequências.

3.2.1. Grupos Pré-Definidos

O uso de grupos de caracteres é muito comun, para facilitar o uso existem alguns pré-definidos:

  \w => [0-9a-zA-Z_]  ## Letras, números e '_'.
  \d => [0-9]         ## Números.
  \s => [ \t\r\n]     ## espaço, tab, return, new line. (caracteres brancos).

Inversos:

  \W => inverso de \w ## caracteres diferentes de letras e números.
  \D => inverso de \d ## caracteres diferentes de números.
  \S => inverso de \s ## caracteres não brancos.

Exemplo de uso:

  my $var = "321" ;
  if ( $var =~ /\d/ ) { print "Possuo um número.\n" ;}

ou:




  my $var = "321" ;
  if ( $var =~ /[\d\.]/ ) { print "Possuo número(s) ou ponto(s).\n" ;}

* Note que o ponto dentro do grupo foi definido com uma barra antes!
  my $var = "fulano da silva" ;
  if ( $var =~ /\w+\s+\w+\s+\w+/ ) { print "Texto com 3 palavras!\n" ;}

3.3. Repetições e Ocorrências

Para definir múltiplas ocorrências de um caracter ou um grupo utiliza-se:

  +  => 1 ou mais ocorrências: 1*
  ?  => nenhuma ou 1: 0..1
  *  => nenhuma ou mais: *
  .  => 1 ocorrência de qualquer caracter.

Exemplo:

  my $var = "valor: 200" ;
  if ( $var =~ /\w+: \d+/ ) { print "Valor identificado.\n" ;}

ou:

  my $var = "valor 200" ;
  if ( $var =~ /\w+:? \d+/ ) { print "Valor identificado com ou sem ':'.\n" ;}

ou:

  my $var = "valor:: 200" ;
  if ( $var =~ /\w+:* \d+/ ) { print "Valor identificado com 0 ou mais ':'.\n" ;}

3.3.1. Ocorrência Qualquer

O uso de '.*' em REGEXP é muito comun, mas são poucos que entendem o seu real significado e derivações:

  my $var = "valorxyz" ;
  if ( $var =~ /valor.*/ ) { print "'valor' identificado.\n" ;}

* Identifica a palavra 'valor' seguida ou não por caracteres de qualquer tipo.
  my $var = "valorxyz123" ;
  if ( $var =~ /valor.*123/ ) { print "'valor' identificado.\n" ;}

* Note que o REGEXP acima tem igual siginificado que o 1o, em REGEXP ou strings mais complexos '123' será iguinorado por '.*'. Para evitar este problema utilize a derivação '.*?'.

3.3.1.1. Derivação

Um dos códigos mais mal entendidos é '.*?'. Tal falta de entendimento deve-se ao siginificado de '?' neste caso, que não tem nada de parecido com o significado comun: 0 ou 1 ocorrência.

O código '.*?' significa "qualquer ocorrência" até a ocorrência em seguida:

  my $var = "valorxyz123" ;
  if ( $var =~ /valor.*?123/ ) { print "'valor...123' identificado.\n" ;}

* Neste caso 123 não é ignorado por '.*', já que '?' (especial) indica a checagem da próxima ocorrência primeiro.

Note que '.' que dizer qualquer caracter, então a regra vale para um caracter específico também. Nexte exemplo '.' é trocado por 'x':

  my $var = "valorxxx123" ;
  if ( $var =~ /valorx*?123/ ) { print "'valorxxx123' identificado.\n" ;}

Em alguns casos '.*' pode comportar-se como '.*?', especialmente em versões anteriores do Perl. Então sempre procure ser específico, utilizando '.*' e '.*?' nas ocasiões certas, evitando enganos ou resultados não esperados!

3.4. Grupo Fixo

A definição de um grupo de caracteres já foi vista, mas note que ela não funciona como um grupo fixo de caracteres:

  [abc]+ => identifica: abc ; acb ; bca ; bac ; cba ; cab

Ou seja, indefica o grupo 'abc' em qualquer ordem e tamanho.

Para identificar uma sequência fixa utiliza-se (?:xxxx), onde xxxx é a sua sequência (palavras, etc...). Exemplo:

  my $var = "GATACAGATACAGATACA" ;
  if ( $var =~ /(?:GATACA)+/ ) { print "Sequência 'GATACA' identificada.\n" ;}

* O REGEXP acima identificou a ocorrência 'GATACA' 1 ou mais vezes.

3.4.1. Alternância

A alternância de uma sequência pode ser definida por '|' dentro do "grupo fixo":

  my $var = "ACATAGGATACA" ;
  if ( $var =~ /(?:GATACA|ACATAG)+/ ) { print "Sequência 'GATACA' ou 'ACATAG' identificada.\n" ;}

* O REGEXP acima identifica 'GATACA' ou 'ACATAG' em sequência.

3.4.2. Repetição

As mesmas regras de repetição para grupos de caracteres é válida para grupos fixos:

  (?:GATACA|ACATAG)+   => 1 ou mais
  (?:GATACA|ACATAG)?   => 0 ou 1
  (?:GATACA|ACATAG)*   => 0 ou mais
  (?:GATACA|ACATAG)*?  => 0 ou mais até a próxima ocorrência.

3.5. Negação

Em casos específicos precisamos encontra um texto que não venha depois ou antes de um determinado texto. Para isto temos a opção de utilizarmos (?!xxxx)...., onde 'xxxx' é o texto que não queremos e '....':

  my $var = "aaabbb cccBBB" ;
  my ($capt) = ( $var =~ /(?!a+)...(b+)/i );
  print "$capt\n" ;

* print:
  BBB

* Apenas o BBB no final foi capturado, pos ele não possui uma sequência de a+ antes dele.

Note que (?!xxxx) indica apenas que a próxima ocorrência não deve conte xxxx! Por isso que '...' foi especificado.

É muito fácil enganar-se ao trabalhar com (?!xxxx):

  my $var = "aaabbb cccBBB" ;
  my ($capt) = ( $var =~ /(?!aaa)...(b+)/i );
  print "$capt\n" ;

* print:
  bb

* O REGEXP acima apenas indica que deve capturar uma sequência de 'b' (de qualquer tamanho) sem 'aaa' na frente. Ou seja, 'bb' da 1a sequência é válido, pois antes dele existe 'aab' e não 'aaa'!

3.6. Início e Fim

Note que até agora os REGEXP poderiam ocorrem em qualquer parte da string:

  my $var = "aaa bbb ccc" ;
  if ( $var =~ /bbb/ ) { print "'bbb' identificado.\n" ;}

Mas digamos que queremos identificar 'bbb' apenas no início:

  my $var = "bbb ccc" ;
  if ( $var =~ /^bbb/ ) { print "'bbb' identificado no início.\n" ;}

* Para tal utiliza-se '^' no início do REGEXP.

Para determinar o fim utiliza-se '$':

  my $var = "bbb ccc" ;
  if ( $var =~ /ccc$/ ) { print "'ccc' identificado no fim.\n" ;}

4. Capturando Dados

A forma mais útil do REGEXP é para capturar dados. Para tal, utiliza-se mão de parênteses (), onde as ocorrências dentro dos parênteses serão capturadas/retornadas:

  my $var = "aaa BbB ccc" ;
  if ( $var =~ /(b+)/i ) { print "sequência b: $1\n" ;}

capturas múltiplas:




  my $var = "aaa BbB ccc" ;
  if ( $var =~ /(b+)\s*(c+)/i ) { print "capturas: $1 # $2\n" ;}

* Note que uma sequência de 'b' foi capturada na variável $1, sem diferenciação de maiúsculas e minúsculas. Resultado do print acima:

sequência b: BbB?

4.1. Utilizando Retorno

  my $var = "aaa BbB ccc" ;
  my ($bs) = ( $var =~ /(b+)/ ) ;
  print "sequência b: $bs\n" ;

múltiplos retornos:

  my $var = "aaa BbB ccc" ;
  my ($bs_e_cs) = ( $var =~ /(b+)\s*(c+)/ ) ;
  print "captura: $bs_e_cs\n" ;

5. Substituição/Replace

Outro uso comun é a substituição (replace) de partes de uma string:

  my $var = "aaa BbB ccc" ;
  $var =~ s/b+/x/i ;

* O REGEXP acima subistitui qualquer ocorrência concecutiva de 'b', sem diferenciação de maiúsculas e minúsculas.

O recurso de captura também pode ser combinado:

  my $var = "aaa BbB ccc" ;
  $var =~ s/(b+)/<$1>/i ;

* O REGEXP acima coloca uma sequência de 'b' entre '<' e '>', sem diferenciação de maiúsculas e minúsculas.

6. Parâmentros

6.1. Parâmetro 'g' - Global

O parâmetro 'g' indica para procurar/afetar todas as ocorrências da string.

  my $var = "aaa BbB ccc bBb ddd b fff BB" ;
  $var =~ s/(b+)/<$1>/ig ;

* Coloca qualquer sequência de 'b' entre '<' e '>'.

6.2. Parâmetro 'e' - Execução de comandos no REGEXP

Para criar REGEXP mais complexos, ou com uma interação maior, temos a opção de executar comandos dentro do REGEXP de substituições:

  my $var = "aaa 10 ccc" ;
  $var =~ s/(\d+)/ my $x10 = $1 * 10 ; $x10 /ie ;

* O REGEXP acima subistitui qualquer sequência de números por esta sequência multiplicada por 10. Note a variável $x10 no final, que informa de forma explícita qual dado é retornado para a substituição.
* O único tipo de comando que deve ser evitado dentro do REGEXP é um outro REGEXP! Isto inclui REGEXP dentro de funções (sub), ou seja, se uma função é chamada dentro do REGEXP ela não pode ter outros REGEXP, ou o REGEXP principal será corrompido!

6.3. Parâmetro 'x' - Extendendo espaços

A escrita de REGEXP pode ficar muito confusa e longa, para contornar este problema temos o parâmetro 'x':

  my $var = "nome: fulano" ;
  ## ou: my $var = "nome = fulano" ;

  $var =~ s/
  (
    (?:nome|email|tel)
    \s*
    [:=]
    \s*
  )
  (\S+)
  /$1<$2>/xi ;

  print "$var\n" ;

* O REGEXP acima coloca os valores dos campos 'nome', 'email' e 'tel' entre '<' e '>', sendo que o separador pode ser ':' ou '='.
* Note que a indicação de espaços na string é feita de forma explícita por \s.
* Resultado do print acima:
  nome: 

7. Práticas Comuns

7.1. while e sub-REGEXP

Uma forma especial de passar uma string é utilizando REGEXP + while:

  my $var = "aaa BbB ccc bBb ddd b fff BB" ;
  while( $var =~ /(b+)/gi ) {
    print "$1\n" ;
  }

* Resultados do print:
  BbB
  bBb
  b
  BB

Mas digamos que você quer fazer uma busca na string depois da sequência de 'b' capturada:

  my $var = "aaa BbB ccc bBb ddd b fff BB" ;
  while( $var =~ /(b+)/gi ) {
    my ($subvar) = ( $var =~ /\G\s*(\w+)/ );
    print "$1: $subvar\n" ;
  }

* Resultados do print:
  BbB: ccc
  bBb: ddd
  b: fff
  BB:

* Note a utilização do \G, que indica que o REGEXP irá continuar da posição do REGEXP anterior.

7.2. split

Algo que poucos percebem é que em todo split() existe um REGEXP:

  my $var = "nome = fulano" ;
  my ($start , $end) = split("=" , $var) ;

Na verdade o split acima deve ser escrito como abaixo, já que '"="' será sempre tratado como um REGEXP:

  my $var = "nome = fulano" ;
  my ($start , $end) = split(/=/ , $var) ;

Um split() não é muito diferente de:

  my $var = "nome = fulano" ;
  my ($start , $end) = ( $var =~ /^(.*?)=(.*)/ );

7.3. Execução em Busca

Códigos podem ser executados não só em substituições, mas no próprio REGEXP em si.

  my $var = "aaa BbB ccc bBb ddd b fff BB" ;
  $var =~ s/(\w+)(?{ print "[$1]\n" ; })/<$1>/g ;
  print "var: $var\n" ;

* Resultados do print:
  [aaa]
  [BbB]
  [ccc]
  [bBb]
  [ddd]
  [b]
  [fff]
  [BB]
  var:        

* O REGEXP acima tem como resultado na variável $var, colocar as sequências de \w entre '<' e '>'. Mas em cada captura executa um print.
* O código foi definido em (?{xxxx}) onde xxxx é o código. Mas o código só será executado quando algo for capturado em (\w+). Ou seja, (?{}) deve estar depois de uma captura.

9. Técnicas Avançadas

Bom, 1o o que é avançado para uns pode ser simples para outros e vice-versa.

Irei abordar 2 situações complexas em Perl e que vários programadores já se depararem com ela:

9.1. Método is_quoted()

Obejetivo é ter um método que determina se uma string já está delimitada por " ou ', necessário em versões antigas do DBI.

Um dos principais problemas é uma string que possui " ou ' dentro dela:

  "javascript: call(\"xyz\")"

* No caso acima \" deve ser identificado, pois o " não indica o fim.
* Função:
  sub is_quoted {
    my $data = shift ;

    $data =~ s/\\\\//gs ;   #1# Ignora \\

    if (
    $data =~ /^             #2# Início
      \s*                   #3# Ignora espaços no início.
      (?:(?!\\).|)          #4# Aspas sem \ antes.
      (?:
        "[^"\\]?"                  #5# Nenhum dado delimitado: ""
        |
        "(?:(?:\\")|[^"])+(?!\\)." #6# Dado sem ", ou com \"
        |
        '[^'\\]?'                  #7# mesmo que 5, mas para '
        |
        '(?:(?:\\')|[^'])+(?!\\).' #8# mesmo que 6, mas para '
      )
      \s*                   #9# Ignora espaços no fim.
    $/sx
    ) { return( 1 ) ;} ## return true
    return undef ;     ## return false
  }

* Passos:

* 1. Remove \\, para não atrapalhar em \" ou \', evitando a ocorrência de \\" e \\'.
* 2. Determina o início da string.
* 3. Ignora espaços no início. Este passo poderia ser removido caso não se queira ignora-los.
* 4. Determina que antes da 1a aspas não pode existir \, ou não deve existir nada. Note a barra '|' de alternância.

Os passos 5 e 6 (7 e 8 é repetição para ') são os mais importantes. Note que eles estão dentro de um "grupo fixo" alternado, que deverá identificar uma das opções.

  • * 5. Um dado "quoted" de tamanho 1 ou 0. Sendo que se for de tamanho 1 não deve conter \ ou ".
    * 6. É composto por 2 partes:
   "(?:(?:\\")|[^"])+
   ## e:
   (?!\\)."

A 1a parte é dividida em mais 2:

     (?:\\")
     ## e:
     [^"]

Indica que deve começar com ", conter um sequência de \" ou uma sequência diferente de ".

A 2a parte indica o fim, e que a aspas final não deve conter \ antes dela. Esta parte justifica o passo 5.

10. Referências

* Perldoc.perlrequick? - Perl regular expressions quick start
* Perldoc.perlretut? - Perl regular expressions tutorial
* Perldoc.perlre? - Perl regular expressions, the rest of the story

----

AUTHOR

Fabiano Reese Righetti

blog comments powered by Disqus