Subsecciones

Casos de Estudio


Secuencias de números de tamaño fijo

El siguiente problema y sus soluciones se describen en el libro de J.E.F. Friedl [8]. Supongamos que tenemos un texto conteniendo códigos que son números de tamaño fijo, digamos seis dígitos, todos pegados, sin separadores entre ellos, como sigue:

012345678901123334234567890123125934890123345126

El problema es encontrar los códigos que comienzan por 12. En negrita se han resaltado las soluciones. Son soluciones sólo aquellas que, comienzan por 12 en una posición múltiplo de seis. Una solución es:

@nums = grep {m/^12/} m/\d{6}/g;

que genera una lista con los números y luego selecciona los que comienzan por 12. Otra solución es:

@nums = grep { defined } m/(12\d{4})|\d{6}/g;

que aprovecha que la expresión regular devolverá una lista vacía cuando el número no empieza por 12:

DB<1> $x = '012345678901123334234567890123125934890123345126'
DB<2> x  ($x =~ m/(12\d{4})|\d{6}/g)
0  undef
1  undef
2  123334
3  undef
4  undef
5  125934
6  undef
7  undef
Obsérvese que se esta utilizando también que el operador | no es greedy.

¿Se puede resolver el problema usando sólamente una expresión regular? Obsérvese que esta solución ``casi funciona'':

 DB<3> x @nums = $x =~ m/(?:\d{6})*?(12\d{4})/g;
0  123334
1  125934
2  123345

recoge la secuencia mas corta de grupos de seis dígitos que no casan, seguida de una secuencia que casa. El problema que tiene esta solución es al final, cuando se han casado todas las soluciones, entonces la búsqueda exhaustiva hará que nos muestre soluciones que no comienzan en posiciones múltiplo de seis. Por eso encuentra 123345:

012345678901123334234567890123125934890123345126
Por eso, Friedl propone esta solución:

@nums = m/(?:\d{6})*?(12\d{4})(?:(?!12)\d{6})*/g;

Se asume que existe al menos un éxito en la entrada inicial. Que es un extraordinario ejemplo de como el uso de paréntesis de agrupamiento simplifica y mejora la legibilidad de la solución. Es fantástico también el uso del operador de predicción negativo.

Solución usando el ancla $ \backslash$ G

El ancla \G ha sido concebida para su uso con la opción /g. Casa con el punto en la cadena en el que terminó el último emparejamiento. Cuando se trata del primer intento o no se está usando /g, usar \G es lo mismo que usar \A.

Mediante el uso de este ancla es posible formular la siguiente solución al problema planteado:

pl@nereida:~/Lperltesting$ perl -wde 0
main::(-e:1):   0
DB<1> $_ = '012345678901123334234567890123125934890123345126'
DB<2> x m/\G(?:\d{6})*?(12\d{4})/g
0  123334
1  125934

Sustitución

Si lo que se quiere es sustituir las secuencias deseadas es poisble hacerlo con la siguiente expresión regular:

casiano@nereida:~/docs/curriculums/CV_MEC$ perl -wde 0
DB<1> x $x = '012345678901123334234567890123125934890123345126'
0  012345678901123334234567890123125934890123345126
DB<2> x  ($y = $x) =~ s/(12\d{4})|\d{6}/$1? "-$1-":$& /ge
0  8
DB<3> p $y
012345678901-123334-234567890123-125934-890123345126


Palabras Repetidas

Su jefe le pide una herramienta que compruebe la aparición de duplicaciones consecutivas en un texto texto (como esta esta y la anterior anterior). La solución debe cumplir las siguientes especificaciones:

    1 #!/usr/bin/perl -w
    2 use strict;
    3 use Term::ANSIScreen qw/:constants/;
    4 
    5 my $bold = BOLD();
    6 my $clear = CLEAR();
    7 my $line = 1;
    8 
    9 # read paragraph
   10 local $/ = ".\n";
   11 while (my $par = <>) {
   12   next unless $par =~ s{
   13         \b                 # start word ...
   14         ([a-z]+)           # grab word in $1 and \1
   15         (                  # save the tags and spaces in $2
   16         (\s|<[^>]+>)+      # spaces or HTML tags 
   17         )   
   18         (\1\b)             # repeated word in $4
   19   }!$bold$1$clear$2$bold$4$clear!igx;
   20 
   21   $par =~ s/^/"$ARGV(".$line++."): "/meg;   # insert filename and line number
   22 
   23   print $par;
   24 }

Sigue un ejemplo de uso:

pl@nereida:~/Lperltesting$ cat -n t.t
     1  one one
     2  nothing rep
     3  is two three
     4  three four
     5
pl@nereida:~/Lperltesting$ ./repeatedwords2.pl t.t
t.t(1): one one
t.t(2): nothing rep
t.t(3): is two three
t.t(4): three four
t.t(5):


Análisis de cadenas con datos separados por comas

Supongamos que tenemos cierto texto en $text proveniente de un fichero CSV (Comma Separated Values). Esto es el fichero contiene líneas con el formato:

"earth",1,"moon",9.374

Esta línea representa cinco campos. Es razonable querer guardar esta información en un array, digamos @field, de manera que $field[0] == 'earth', $field[1] == '1', etc. Esto no sólo implica descomponer la cadena en campos sino también quitar las comillas de los campos entrecomillados. La primera solución que se nos ocurre es hacer uso de la función split:

@fields = split(/,/,$text);

Pero esta solución deja las comillas dobles en los campos entrecomillados. Peor aún, los campos entrecomillados pueden contener comas, en cuyo caso la división proporcionada por split sería errónea.

   1 #!/usr/bin/perl -w
   2 use Text::ParseWords;
   3 
   4 sub parse_csv {
   5   my $text = shift;
   6   my @fields = (); # initialize @fields to be empty
   7 
   8   while ($text =~ 
   9     m/"(([^"\\]|\\.)*)",? # quoted fields
  10       | 
  11       ([^,]+),?           # $3 = non quoted fields
  12       | 
  13       ,                   # allows empty fields
  14     /gx 
  15     )
  16   {
  17     push(@fields, defined($1)? $1:$3); # add the just matched field
  18   }
  19   push(@fields, undef) if $text =~ m/,$/; #account for an empty last field
  20   return @fields;
  21 }
  22        
  23 $test = '"earth",1,"a1, a2","moon",9.374';
  24 print "string = \'$test\'\n";
  25 print "Using parse_csv\n:";
  26 @fields = parse_csv($test);
  27 foreach $i (@fields) {
  28   print "$i\n";
  29 }
  30 
  31 print "Using Text::ParseWords\n:";
  32 #  @words = &quotewords($delim, $keep, @lines);  
  33 #The $keep argument is a boolean flag.  If true, then the
  34 #tokens are split on the specified delimiter, but all other
  35 #characters (quotes, backslashes, etc.) are kept in the
  36 #tokens.  If $keep is false then the &*quotewords()
  37 #functions remove all quotes and backslashes that are not
  38 #themselves backslash-escaped or inside of single quotes
  39 #(i.e., &quotewords() tries to interpret these characters
  40 #just like the Bourne shell). 
  41 
  42 @fields = quotewords(',',0,$test);
  43 foreach $i (@fields) {
  44   print "$i\n";
  45 }

Las subrutinas en Perl reciben sus argumentos en el array @_. Si la lista de argumentos contiene listas, estas son ``aplanadas'' en una única lista. Si, como es el caso, la subrutina ha sido declarada antes de la llamada, los argumentos pueden escribirse sin paréntesis que les rodeen:

@fields = parse_csv $test;

Otro modo de llamar una subrutina es usando el prefijo &, pero sin proporcionar lista de argumentos.

@fields = &parse_csv;
En este caso se le pasa a la rutina el valor actual del array @_.

Los operadores push (usado en la línea 17) y pop trabajan sobre el final del array. De manera análoga los operadores shift y unshift lo hacen sobre el comienzo. El operador ternario ? trabaja de manera análoga como lo hace en C.

El código del push podría sustituirse por este otro:

push(@fields, $+);
Puesto que la variable $+ contiene la cadena que ha casado con el último paréntesis que haya casado en el ultimo ``matching''.

La segunda parte del código muestra que existe un módulo en Perl, el módulo Text::Parsewords que proporciona la rutina quotewords que hace la misma función que nuestra subrutina.

Sigue un ejemplo de ejecución:

> csv.pl
string = '"earth",1,"a1, a2","moon",9.374'
Using parse_csv
:earth
1
a1, a2
moon
9.374
Using Text::ParseWords
:earth
1
a1, a2
moon
9.374

Las Expresiones Regulares como Exploradores de un Árbol de Soluciones

Números Primos

El siguiente programa evalúa si un número es primo o no:

pl@nereida:~/Lperltesting$ cat -n isprime.pl
 1  #!/usr/bin/perl -w
 2  use strict;
 3
 4  my $num = shift;
 5  die "Usage: $0 integer\n" unless (defined($num) && $num =~ /^\d+$/);
 6
 7  if (("1" x $num) =~ /^(11+)\1+$/) {
 8    my $factor = length($1);
 9    print "$num is $factor x ".$num/$factor."\n";
10  }
11  else {
12    print "$num is prime\n";
13  }
Siguen varias ejecuciones:
pl@nereida:~/Lperltesting$ ./isprime.pl 35.32
Usage: ./isprime.pl integer
pl@nereida:~/Lperltesting$ ./isprime.pl 47
47 is prime
pl@nereida:~/Lperltesting$ ./isprime.pl 137
137 is prime
pl@nereida:~/Lperltesting$ ./isprime.pl 147
147 is 49 x 3
pl@nereida:~/Lperltesting$ ./isprime.pl 137
137 is prime
pl@nereida:~/Lperltesting$ ./isprime.pl 49
49 is 7 x 7
pl@nereida:~/Lperltesting$ ./isprime.pl 47
47 is prime

Ecuaciones Diofánticas: Una solución

Según dice la entrada Diophantine_equation en la wikipedia:

In mathematics, a Diophantine equation is an indeterminate polynomial equation that allows the variables to be integers only.

La siguiente sesión con el depurador muestra como se puede resolver una ecuación lineal diofántica con coeficientes positivos usando una expresión regular:

  DB<1> # Resolvamos 3x + 2y + 5z = 40
DB<2> x ('a'x40) =~  /^((?:...)+)((?:..)+)((?:.....)+)$/
0  'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
1  'aa'
2  'aaaaa'
DB<3> x map { length }  ('a'x40) =~  /^((?:...)+)((?:..)+)((?:.....)+)$/
0  33
1  2
2  5
DB<4> @c = (3, 2, 5)
DB<5> x map { length($_) / $c[$i++] }  ('a'x40) =~  /^((?:...)+)((?:..)+)((?:.....)+)$/
0  11
1  1
2  1
DB<6> p 3*11+2*1+5*1
40

Ecuaciones Diofánticas: Todas las soluciones

Usando el verbo (*FAIL) es posible obtener todas las soluciones:

main::(-e:1):   0
DB<1>  sub equ { my @c = @_; print "\t3*$c[0]+2*$c[1]+5*$c[2] = ",3*$c[0]+2*$c[1]+5*$c[2],"\n" }
DB<2> sub f { my @c = ((length($1)/3), (length($2)/2), (length($3)/5)); equ(@c); }
DB<3> x ('a'x40) =~  /^((?:...)+)((?:..)+)((?:.....)+)$(?{ f() })(*FAIL)/x
        3*11+2*1+5*1 = 40
        3*9+2*4+5*1 = 40
        3*8+2*3+5*2 = 40
        3*7+2*7+5*1 = 40
        3*7+2*2+5*3 = 40
        3*6+2*6+5*2 = 40
        3*6+2*1+5*4 = 40
        3*5+2*10+5*1 = 40
        3*5+2*5+5*3 = 40
        3*4+2*9+5*2 = 40
        3*4+2*4+5*4 = 40
        3*3+2*13+5*1 = 40
        3*3+2*8+5*3 = 40
        3*3+2*3+5*5 = 40
        3*2+2*12+5*2 = 40
        3*2+2*7+5*4 = 40
        3*2+2*2+5*6 = 40
        3*1+2*16+5*1 = 40
        3*1+2*11+5*3 = 40
        3*1+2*6+5*5 = 40
        3*1+2*1+5*7 = 40
  empty array
DB<4>

Ecuaciones Diofánticas: Resolutor general

El siguiente programa recibe en línea de comandos los coeficientes y término inependeinte de una ecuación lineal diofántica con coeficientes positivos y muestra todas las soluciones. El algoritmo primero crea una cadena conteniendo el código Perl que contiene la expresión regular adecuada para pasar luego a evaluarlo:

pl@nereida:~/Lperltesting$ cat -n diophantinesolvergen.pl
 1  #!/usr/local/lib/perl/5.10.1/bin//perl5.10.1 -w
 2  use v5.10;
 3  use strict;
 4
 5  # Writes a Perl solver for
 6  # a1 x1 + a2 x2 + ... + an xn = b
 7  # a_i and b integers > 0
 8  #
 9
10  my $b = pop;
11  my @a = @ARGV;
12  my $debug = 1;
13
14  my $b1 = '1'x$b;
15  my @a1 = map { '1'x$_ } @a;
16  my @index = map { 'length($'.$_.")/".$a[$_-1] } 1..(@a);
17  my $aux = join ",", @index;
18
19  my $regexp = '^';
20  $regexp .= "((?:$_)+)" for @a1;
21
22  $regexp .= '$(?{ f() })(*FAIL)';
23
24  my $solver = <<"SOLVER";
25  my \@stack;
26  sub f {
27    my \@s = ($aux);
28    push \@stack, [ \@s ];
29  }
30
31  q{$b1} =~ m{$regexp}x;
32
33  return \@stack;
34  SOLVER
35
36  print "Solver:\n--------\n$solver\n--------\n" if $debug;
37
38  my @stack = eval $solver;
39
40  say("@$_") for @stack
Sigue un ejemplo de ejecución:
pl@nereida:~/Lperltesting$ ./diophantinesolvergen.pl 3 2 5 40
Solver:
--------
my @stack;
sub f {
  my @s = (length($1)/3,length($2)/2,length($3)/5);
  push @stack, [ @s ];
}

q{1111111111111111111111111111111111111111} =~ m{^((?:111)+)((?:11)+)((?:11111)+)$(?{ f() })(*FAIL)}x;

return @stack;

--------
11 1 1
9 4 1
8 3 2
7 7 1
7 2 3
6 6 2
6 1 4
5 10 1
5 5 3
4 9 2
4 4 4
3 13 1
3 8 3
3 3 5
2 12 2
2 7 4
2 2 6
1 16 1
1 11 3
1 6 5
1 1 7

Las Tres Hijas

En la páginas de Retos Matemáticos de

DIVULGAMAT

puede encontrarse el siguiente problema:

Ejercicio 31.5.1   Dos matemáticos se vieron en la calle después de muchos años sin coincidir.

¿Qué edad tendrán las tres hijas?

¿Podemos ayudarnos de una expresión regular para resolver el problema? Al ejecutar el siguiente programa:

pl@nereida:~/Lperltesting$ cat -n playspiano.pl
 1  #!/usr/local/lib/perl/5.10.1/bin//perl5.10.1  -w
 2  use v5.10;
 3  use strict;
 4  use List::Util qw{sum};
 5
 6  local our %u;
 7  sub f {
 8    my @a = @_;
 9    @a = sort { $b <=> $a } (length($a[1]), length($a[0])/length($a[1]), 36/length($a[0]) );
10
11    local $" = ", ";
12    say "(@a)\t ".sum(@a) unless exists($u{"@a"});
13    $u{"@a"} = undef;
14  }
15
16  say "SOL\t\tNUMBER";
17  my @a =  ('1'x36) =~
18           /^((1+)\2+)(\1+)$
19                     (?{ f($1, $2, $3)
20                      })
21             (*FAIL)
22           /x;

obtenemos la salida:

pl@nereida:~/Lperltesting$ ./playspiano.pl
SOL             NUMBER
(9, 2, 2)        13
(6, 3, 2)        11
(4, 3, 3)        10
(18, 2, 1)       21
(12, 3, 1)       16
(9, 4, 1)        14
(6, 6, 1)        13

Explique el funcionamiento del programa. A la vista de la salida ¿Cuáles eran las edades de las hijas?

Mochila 0-1

Para una definición del problema vea la sección El Problema de la Mochila 0-1 en los apuntes de LHP

Ejercicio 31.5.2   ¿Sería capaz de resolver usando expresiones regulares el problema de la mochila 0-1?

¡Si lo logra merece el premio a la solución mas freak que se haya encontrado para dicho problema!

Véase también

Véase también:

Número de substituciones realizadas

El operador de substitución devuelve el número de substituciones realizadas, que puede ser mayor que uno si se usa la opción /g. En cualquier otro caso retorna el valor falso.
   1 #!/usr/bin/perl -w
   2 undef($/);
   3 $paragraph = <STDIN>;
   4 $count = 0;
   5 $count = ($paragraph =~ s/Mister\b/Mr./ig);
   6 print "$paragraph";
   7 print "\n$count\n";
El resultado de la ejecución es el siguiente:
> numsust.pl
Dear Mister Bean,
Is a pleasure for me and Mister Pluto
to invite you to the Opening Session
Official dinner that will be chaired by
Mister Goofy.
 
Yours sincerely
  Mister Mickey Mouse
Dear Mr. Bean,
Is a pleasure for me and Mr. Pluto
to invite you to the Opening Session
Official dinner that will be chaired by
Mr. Goofy.
 
Yours sincerely
  Mr. Mickey Mouse
 
4

Expandiendo y comprimiendo tabs

Este programa convierte los tabs en el número apropiado de blancos.
pl@nereida:~/Lperltesting$ cat -n expandtabs.pl
    1 #!/usr/bin/perl -w
    2 use strict;
    3 
    4 my @string = <>;
    5 
    6 for (@string) {
    7   while (s/\t+/' ' x (length($&)*8 - length($`)%8)/e) {}
    8   print $_;
    9 }
Sigue un ejemplo de ejecución:
pl@nereida:~/Lperltesting$ cat -nt tabs.in
     1  012345670123456701234567012345670
     2  one^Itwo^I^Ithree
     3  four^I^I^I^Ifive
     4  ^I^Itwo
pl@nereida:~/Lperltesting$ ./expandtabs.pl tabs.in | cat -tn
     1  012345670123456701234567012345670
     2  one     two             three
     3  four                            five
     4                  two

Ejercicio 31.5.3   ¿Funciona igual si se cambia el bucle while por una opción /g?
pl@nereida:~/Lperltesting$ cat -n ./expandtabs2.pl
    1   #!/usr/bin/perl -w
    2   use strict;
    3 
    4   my @string = <>;
    5 
    6   for (@string) {
    7     s/\t+/' ' x (length($&)*8 - length($`)%8)/ge;
    8     print $_;
    9   }
¿Porqué?

Modificación de Múltiples Ficheros: one liner

Aunque no es la forma de uso habitual, Perl puede ser utilizado en ``modo sed'' para modificar el texto en múltiples ficheros:

perl -e 's/nereida\.deioc\.ull\.es/miranda.deioc.ull.es/gi' -p -i.bak *.html

Este programa sustituye la palabra original (g)lobalmente e i)gnorando el ``case'') en todos los ficheros *.html y para cada uno de ellos crea una copia de seguridad *.html.bak.

Otro ejemplo: la sustitución que sigue ocurre en todos los ficheros info.txt en todos los subdirectorios de los subdirectorios que comiencen por alu:

perl -e 's/\|hyperpage//gi' -p -i.bak  alu*/*/info.txt

Las opciones de línea de comandos significan lo siguiente:

-e
puede usarse para definir el script en la línea de comandos. Multiples -e te permiten escribir un multi-script. Cuando se usa -e, perl no busca por un fichero de script entre la lista de argumentos.

-p
La opción -p hace que perl incluya un bucle alrededor de tu ``script'' al estilo sed:
while (<>) {
        ...             # your script goes here
} continue {
        print;
}
-n
Nótese que las líneas se imprimen automáticamente. Para suprimir la impresión usa la opción -n

-i[ext]
La opción -i Expresa que los ficheros procesados serán modificados. Se renombra el fichero de entrada file.in a file.in.ext, abriendo el de salida con el mismo nombre del fichero de entrada file.in. Se selecciona dicho fichero como de salida por defecto para las sentencias print. Si se proporciona una extensión se hace una copia de seguridad. Si no, no se hace copia de seguridad.

En general las opciones pueden ponerse en la primera línea del ``script'', donde se indica el intérprete. Asi pues, decir

perl -p -i.bak -e "s/foo/bar/;"

es equivalente a usar el ``script'':

#!/usr/bin/perl -pi.bak
s/foo/bar/;

Casiano Rodríguez León
2013-04-23