Tutorial de PERL en castellano : Regularizando la situación

Pero ya puestos...
Preguntas frecuentemente preguntadas
Bibliografía.
Recursos Internet
Presentando lo impresentable

Expresiones regulares: comparación y sustitución

Una forma todavía más directa de trabajar con estos ficheros de texto que tienen una estructura regular (que son la mayoría), es usar expresiones regulares. Una expresión regular es una forma de expresar gramaticalmente la estructura de cualquier cadena alfanumérica. Por ejemplo, una cadena compuesta por una letra inicial, con una letra o un número a continuación se podría expresar de la forma siguiente

letra {letra|número}*

donde | expresa una alternativa y * indica que puede aparecer 0 o más veces. Pues bien, estas expresiones regulares se utilizan enormemente en PERL (de hecho ya hemos usado una, aunque compuesta por un solo carácter, en la línea 4 del programa anterior), y cada vez que hemos usado split. Las expresiones regulares se usan para hacer comparaciones, es decir, hallar si un texto sigue o contiene una determinada expresión regular, y también para sustituir una subcadena que cumpla una expresión regular por otra cadena.

La expresión regular más simple es la propia cadena con la que se quiere comparar; es decir, una cadena coincidirá consigo misma. Las expresiones regulares en PERL siempre van encerradas entre //, o bien m{} (las llaves pueden ser sustituidas por cualquier otro elemento que no esté incluido en la expresión regular), y si no se indica nada, comparará la expresión regular con la variable por defecto, $_, es decir, que /pepe/ será cierto si $_ contiene íntegra la cadena pepe. El programilla

$_ = "pepeillo"; print "si" if /pepe/;

imprimirá

si

Las expresiones regulares usan símbolos convencionales para referirse a grupos de caracteres y otros para repetición o señales de puntuación. En algunos casos, si el símbolo significa algo dentro de la expresión regular (o en PERL), se precede por \ (escape). Por ejemplo, . significa "cualquier carácter" dentro de una expresión regular; luego para referirnos al punto como tal, usaremos \.. También se usa \/,\? y \*, por ejemplo. Otras expresiones más complicadas incluirían repeticiones de símbolos; por ejemplo, \w+ casaría con cualquier palabra (un carácter alfanumérico repetido uno o más veces).

. Describe cualquier carácter, excepto newline.
( ) Agrupa una serie de patrones en un simple elemento.
+,*,? Coinciden con el elemento al que preceden repetido 1 o más veces, 0 o más, ó 0 ó 1. Seguidos por {N,M}, indican el número mínimo y máximo que debede aparecer; {N} significa exactamente N veces; {N,}, como mínimo N veces.
[..] Indica una clase de caracteres, [^...] niega la clase, - indica un rango decaracteres, como [a-z].
(..¦..¦..) coincide con una de las alternativas.
\w,\W coincide con los alfanuméricos, \W con los no-alfanuméricos.
\s,\S con los espacios en blanco, \S con los que no lo son.
\d \D con los numéricos, \D no-numéricos.
\b,\B con límites de palabra, \B con el interior de una palabra.
$,^ con el final de una línea o cadena y con el principio.
Tabla 4: operadores de expresiones regulares

Para agrupar símbolos se usa el paréntesis, que además sirve para indicarle a PERL que con lo que coincida con la expresión en su interior se va a hacer algo. Para empezar, se asigna a la variable $&; pero además, podemos asignar a otra variable el resultado de la comparación; en general, una comparación con una expresión regular que incluya paréntesis devuelve una lista con todo lo que coincida con el contenido de los paréntesis

$zipi = "Pepeillo Gonzalez MacKenzie 8000";
@parejas = ($zipi =~ /(\D+) (\d+)/);

(el nombre de la variable y el símbolo =~ se pueden suprimir si la comparación se hace sobre la variable por defecto $_); @parejas contendrá ("Pepeillo Gonzalez MacKenzie",8000), ya que la primera expresión regular indica "uno o más caracteres no numéricos", mientras que la segunda representa "uno o más caracteres numéricos".

Por ejemplo, el fichero de paganinis usado en ejemplos anteriores anterior tiene la estructura siguiente: una o más palabras, separadas por un espacio, una cantidad (números y puntos), una hora (números y dos puntos) y una fecha (números y /). Esto se dice en PERL mediante la expresión siguiente(\D+) (\d+\.?\d+) (\S+) (\d+)\/(\d+)\/(\d+)que puede parecer un poco críptica (y en realidad lo es), pero cuyo significado se puede resolver mirando la tabla 4. Con esta modificación, el bucle central del programa totales.pl se queda reducido a


while(<>) {
    ($paganini, $pasta, $hora, $dia, $mes) = /(\D+) (\d+\.?\d+) (\S+) (\d+)\/(\d+)\/(\d+)/;
    $totalDia{"$mes-$dia"}+=$pasta;
}

Más compacidad no se puede pedir. Además, ya de camino, nos vamos ahorrando algunas variables. En la primera línea del interior del bucle se asigna la parte de la cadena que coincide con lo descrito en el interior de los paréntesis a cada una de las variables, es decir, divide la línea en cinco campos, cada uno de los cuales tiene un tipo diferente. Hay cinco pares de paréntesis, para cinco variables. Veamos cada expresión regular por partes.

Las expresiones regulares se usan en casi todos los lenguajes de programación; el ligro Mastering Regular Expressions: Powerful Techniques for Perl and Other Tools , de Jeffrey E. Friedl (Editor), Andy Oram (Editor), el libro del búho. Es un libro avanzado, pero útil para quien quiera descifrar el arcano arte de las expresiones regulares

[Mastering regular expressions front page]

La primera expresión regular es \D+. Como se ve en la tabla 4, \D coincide con todo lo que no sea numérico, y en particular letras y espacios. Este campo es problemático, porque puede incluir una o varias palabras; sin embargo, sabemos que el siguiente campo comienza por un número; por tanto, incluiremos en este campo todo lo que haya hasta que encontremos un número. Se puede insertar el código siguiente print $paganini; en el bucle para ver qué se incluye dentro de ese campo.

La siguiente expresión regular, (\d+\.?\d+), no describe otra cosa que un número real en notación de coma flotante, es decir, una o más cifras (\d+), seguidas o no por un punto (\.?, no olvidar que el punto tiene significado especial en las expresiones regulares), que a su vez debe de estar seguido por una o más cifras (\d+). Esta expresión regular coincide además con aquellos números sin punto enmedio.

Y la siguiente \S+ coincide con la hora, aunque quizás podría coincidir con cualquier cosa, simplemente coge todo lo que haya entre los dos espacios. En realidad, si pusiéramos toda la expresión regular anterior como(\S+) (\S+) (\S+) (\S+) funcionaría perfectamente.

Y para terminar,(\d+)\/(\d+)\/(\d+)coincide con el día, el mes y el año, que están separados por una barra diagonal.

Con estas expresiones regulares se pueden construir programas superpotentes, que convierten casi cualquier texto en casi cualquier otro. Incluso, si uno se atreve (nuestro político corrupto no creemos que se atreva) un analizador sintáctico de un lenguaje de alto nivel, aunque sea simplificado. Otra utilidad es hacer filtros de correo electrónico, o de noticias de USENET (de hecho, el killfile o fichero que incluye todas las expresiones regulares de mensajes que deben de ser borrados, usa expresiones regulares). También se pueden usar expresiones regulares en archie y en la Web; el problema es que la mayoría de las veces usan convenciones diferentes, pero en cualquier caso, nunca viene mal saberlas.

El político corrupto decide no contarle al señor X todo el dinero obtenido (por no mencionar a Hacienda), y poniéndose a trabajar sobre los ficheros generados anteriormente (como cliente.mas), elabora el siguiente programilla (changec.pl)


#!/usr/local/bin/perl -p
s/(\d+\.?\d+)/$&*0.85/e;

que ni siquiera pongo en un cuadro aparte, porque no merece la pena.

En este programa se introducen novedades desde la primera línea. Para empezar, se usa PERL con un switch en la línea de comandos, -p. Este switch indica que alredededor de lo que hay debajo hay que sobreentender el siguiente bucle

while(<>)
{
    s/(\d+\.?\d+)/$&*0.85/e;
    print;
}

-n Incluye el bucle while(<>) {}alrededor de las órdenes del fichero.
-p Incluye el bucle while(<>) {print;} alrededor de las órdenes del fichero.
-a Modo autosplit, es decir, que al principio del bucle anterior incluye la orden split.
-d Modo de depuración. En vez de ejecutar el programa, se ejecuta el depurador sobre el programa.
-i[extensión] En combinación con -p y -n, en vez de imprimir en salida estándar, trabaja sobre el mismo fichero, creando una copia de seguridad con la extensión que se indica,
Tabla 5: Switches o interruptores que se pueden usar en la línea de comandos

; es decir, un bucle que abre y lee del fichero que se pasa en la línea de comandos, ejecuta las órdenes encontradas en el fichero, y al final imprime la variable por defecto. Si el switch fuera -n en vez de -p no imprimiría.

También se incluye la nueva orden s///[switches]tomada, como otras órdenes, del editor de UNIX sed. En concreto, esta orden lo que hace es sustituir la expresión regular encontrada entre los primeros // por la expresión en los segundos. En este caso actúa sobre la variable por defecto, pero en general$zipi="pepeillo"; $zipi=~ s/p/q/g;daría "qeqeillo". El switch e que aparece al final de la orden en el programa indica que se tiene que evaluar la expresión que aparece en la segunda parte de la orden; el g que aparece aquí indica que se tiene que hacer una sustitución global, es decir, que tienen que sustituirlo todas las veces que aparezca en la primera expresión, no solo una vez.

Por último, la variable $& contiene la cadena que coincida con la expresión regular en la primera parte de la orden; los paréntesis indican qué parte de la expresión hay que asignar a tal variable. Si hay varios paréntesis, las cadenas correspondientes serán asignadas a las "variables" \1, \2 (dentro de la orden s), o $1, $2... fuera de la expresión (estas variables sólo funcionan un ratito, asi que no van a estar disponibles hasta que uno quiera; en caso de duda, es mejor asignarlas inmediatamente a otra variable.

Toda la información sobre expresiones regulares está en la página de manual perldoc perlre.

  1. Dado un fichero que contenga varias líneas en el formato siguiente:
    Apellido 1 Apellido 2, Nombre		Nota como un número real
    poner los nombres en el formato Nombre Apell1 Apell2 Nota como Calificación (Notable, Sobresaliente, etc), ordenándolos por orden de nota.
  2. Dado un fichero que contenga URLs, o direcciones internet, generar otro que incluya alrededor de las mismas una referencia del tipo <a href=URL>URL</a>.
  3. Hacer un programa que imprima todos los ficheros de cabecera incluidos en un programa en código fuente C. Estos tienen el formato #include <nombre-de- fichero.h>. Comprobarlo sobre algún fichero el directorio /usr/include (en UNIX), o cualquier otro que pille a mano.
  4. En un programa en código fuente C o PERL, imprimir las líneas en las que comience un bucle for, e imprimir la variable de bucle que se usa.
  5. En un programa en código fuente C, sustituir todas las constantes definida usando la directiva de precompilación #define por su valor, definido en la misma orden.
Ejercicios
[ Pero ya puestos...] [ Preguntas frecuentemente preguntadas] [ Bibliografía.] [ Recursos Internet] [ Presentando lo impresentable]