27 septiembre 2007

Jugando con Perl (5)

Una de las grandes ventajas de Perl sobre otros lenguajes "más antiguos" como C es el manejo más sencillo de cosas frecuentes, como las cadenas de texto. Otro es la enorme cantidad de funciones incorporadas, que permiten hacer gran cantidad de cosas con una cierta sencillez. Otro es el uso de expresiones regulares directamente desde el lenguaje. Eso es lo que vamos a ver hoy: las expresiones regulares, y de paso, algo más sobre el manejo de cadenas.

En C, para ver si una cadena empieza por "a", haríamos algo como

if (texto[0] == 'a') ...


Si queremos ver si empieza por "ab", la cosa se va haciendo más engorrosa

if ((texto[0] == 'a') && (texto[1] == 'b')) ...

Esto, además de ser más largo y más difícil, tiene "problemas menores": ¿si mi cadena tiene sólo longitud 1, debería yo acceder a la segunda posición? La mayoría de los compiladores, si no se cumple la primera parte de una condición unida por &&, no se molestarían en seguir comprobando, pero... ¿y si mi cadena es "a"? Podría ir afinando un poco la comparación haciendo

if ((strlen(texto) >=2) && (texto[0] == 'a') && (texto[1] == 'b')) ...

Está claro que apenas una comparación tan sencilla como "empieza por ab en minúsculas" ya va siendo engorrosa de programar en C.

En Perl todo esto es mucho más sencillo, gracias al uso de expresiones regulares. Vamos a ver cómo se haría y luego detallamos qué hemos hecho:

$frase1 = "abcde";

if ($frase1 =~ m/ab.*/) {
print "La frase 1 contiene ab y quizá algo más\n";
}


Cosas a tener en cuenta para empezar:
  • Para comparar una cadena con una expresión regular usaremos =~ (si queremos ver si cumple la condición) o !~ (para comprobar si NO se cumple).
  • La expresión regular se indica entre / y /
  • La operación más sencilla con cadenas es comparar ("match"), así que antes de la primera barra añadiríamos una m.
  • Un punto (.) quiere decir "cualquier carácter".
  • Un asterisco (*) quiere decir "el carácter anterior, repetido 0 o más veces".

(luego en la expresión anterior estamos buscando: a, b, 0 o más veces cualquier carácter).

Si eso queda claro, sigamos...

Para ver si se ha tecleado en mayúsculas o en minúsculas, usaríamos corchetes. Los corchetes indican opcionalidad, es decir, que nos sirve cualquiera de los elementos que indicamos entre ellos:

$frase2 = "aBcde";

if ($frase2 =~ m/[aA][bB].*/) {
print "La frase 2 contiene ab (mayúscula o minúscula) y quizá algo más\n";
}


Si se trata de un rango de valores, podemos usar un guión. Por ejemplo, podríamos comprobar si se ha tecleado un número, haciendo

$numeroEntero = "12345";

if ($numeroEntero =~ /[0-9]*/) {
print "El número entero sólo contiene cifras\n";
}


(0 o más cifras del 0 al 9)

Pero siendo estrictos, eso daría una cadena vacía como número válido, y eso es un tanto discutible. Si queremos que al menos haya una cifra, en vez de usar el asterisco (0 o más), usaríamos el símbolo de la suma (+), que indica "1 o más veces":

if ($numeroEntero =~ /[0-9]+/) {
print "El número entero sólo contiene cifras\n";
}


(1 o más cifras del 0 al 9)

Realmente, esto aceptaría cualquier cadena que contenga un número entero. Si queremos que SÓLO sea una número entero, podemos indicar el principio de la cadena con ^ y el final de la cadena con $, de modo que haríamos

if ($numeroEntero =~ m/^[0-9]+$/) {
print "El número entero sólo contiene cifras\n";
}

(la cadena contiene sólo 1 o más cifras del 0 al 9)


¿Cómo lo haríamos para un número real? 1 o más cifras del 0 al 9, seguidas quizás por un punto decimal (pero sólo 1) y, en ese caso, seguidas también por 1 o más cifras del 0 al 9.

Para eso necesitamos saber alguna cosa más:
  • El "." es un comodín, de modo que si queremos buscar si aparece un punto dentro de nuestra cadena, deberemos indicar que no nos referimos al comodín, sino al símbolo del punto ("."), y para eso lo precedemos con la barra invertida: \
  • Si queremos que algo se repita como mucho una vez (0 o 1 veces), añadiremos una interrogación ("?") a continuación.
  • Si necesitamos agrupar cosas (que se repita "el punto y una secuencia de cifras") lo haremos usando paréntesis antes del símbolo "?" (o el que nos interese en nuestra comprobación).

Con esas consideraciones, lo podríamos escribir así:

if ($numeroReal1 =~ m/^[0-9]+(\.[0-9]+)?$/) {
print "El número real 1 parece correcto\n";
}


Un ejemplo que agrupe todo esto:

------

$frase1 = "abcde";
$frase2 = "aBcde";
$numeroEntero = "12345";
$numeroEnteroFalso = "123a45";
$numeroReal1 = "123.45";
$numeroReal2 = "123.4.5";
$numeroReal3 = "123.";

if ($frase1 =~ m/ab.*/) {
print "La frase 1 contiene ab y quizá algo más\n";
} else {
print "La frase 1 no contiene ab\n";
}

if ($frase2 =~ m/ab.*/) {
print "La frase 2 contiene ab y quizá algo más\n";
} else {
print "La frase 2 no contiene ab\n";
}

if ($frase2 =~ m/[aA][bB].*/) {
print "La frase 2 contiene ab (mayúscula o minúscula) y quizá algo más\n";
} else {
print "La frase 2 no contiene por ab (mayúscula o minúscula)\n";
}

if ($numeroEntero =~ m/^[0-9]+$/) {
print "El número entero sólo contiene cifras\n";
} else {
print "El número entero no sólo contiene cifras\n";
}

if ($numeroEnteroFalso =~ m/^[0-9]+$/) {
print "El falso número entero sólo contiene cifras\n";
} else {
print "El falso número entero no sólo contiene cifras\n";
}

if ($numeroReal1 =~ m/^[0-9]+(\.[0-9]+)?$/) {
print "El número real 1 parece correcto\n";
} else {
print "El número real 1 no parece correcto\n";
}

if ($numeroReal2 =~ m/^[0-9]+(\.[0-9]+)?$/) {
print "El número real 2 parece correcto\n";
} else {
print "El número real 2 no parece correcto\n";
}

if ($numeroReal3 =~ m/^[0-9]+(\.[0-9]+)?$/) {
print "El número real 3 parece correcto\n";
} else {
print "El número real 3 no parece correcto\n";
}


-----

que daría como resultado:

La frase 1 contiene ab y quizá algo más
La frase 2 no contiene ab
La frase 2 contiene ab (mayúscula o minúscula) y quizá algo más
El número entero sólo contiene cifras
El falso número entero no sólo contiene cifras
El número real 1 parece correcto
El número real 2 no parece correcto
El número real 3 no parece correcto


Otras posibilidades que no hemos comentado (los ejemplos, más abajo):
  • Si queremos que la búsqueda no distinga entre mayúsculas y minúsculas, añadiremos una "i" a continuación de la segunda barra,
  • Para expresar que algo debe repetirse un número concreto de veces, lo indicamos entre llaves: {2,4} querría decir "un mínimo de 2 veces y un máximo de 4 veces"; {3} sería "exactamente 3 veces"; {7,} indicaría "7 o más veces".
  • Una barra vertical ("|") indica opcionalidad.
  • Un acento circunflejo ("^") nada más comenzar un corchete indica que buscamos el caso contrario.


Algunos ejemplos más, para poner casi todo a prueba:

  • Una secuencia de una o más letras "a": /a+/
  • Una secuencia enre 3 y 5 "a": /a{3,5}/
  • Cualquier letra: /[a-zA-z]/
  • Una secuencia de cero o más puntos: /\.*/
  • Una secuencia de letras "a" o "b": /[a|b]+/
  • Las palabras "Jorge" o "Jorgito": /Jorg(e|ito)/
  • Las palabras "Pedro", "pedro", "Juan", "juan": /[Pp]edro|[Jj]uan/
  • Las palabras "Pedro" o "Juan", en mayúsculas o minúsculas: /Pedro|Juan/i
  • Cualquier cosa que no sea una cifra: /[^0-9]/

Finalmente, quizá sea interesante comentar que para ahorrarnos trabajo en cosas frecuentes como "cualquier cifra" tenemos abreviaturas similares a "\d". Estas son las más habituales:

\d  Un dígito numérico
\D un carácter que no sea un dígito numérico
\w Un carácter "de palabra" (alfanuméricos y "_")
\W Un carácter "no de palabra" (opuesto al anterior)
\s Un carácter de espacio en blanco
\S Un carácter que no sea de espacio en blanco
\t Un tabulador
\n Un avance de línea
\r Un retorno de carro
\043 Un carácter indicado en octal (en este caso, 43 octal = 35 decimal)
\x3f Un carácter hexadecimal (éste es 3f hexadecimal = 63 decimal)

(Se puede hacer más cosas aún, pero como introducción es bastante...)