La Columna.pl #4
Automatización en web con WWW::Mechanize
Introducción
WWW::Mechanize es un módulo muy interesante. Básicamente nos permite automatizar o mecanizar una conversación a través de una página web. Es decir, poder automatizar el procesamiento de enlaces, imágenes, formas, etc. En realidad WWW::Mechanize actúa como una subclase de LWP::UserAgent, que es el mítico LWP en Perl.
Hay que entender que todo lo que hacemos en un navegador web, o casi todo, es posible automatizarlo por medio de scripts, debido a que lo que en realidad sucede es que trabajamos con cabeceras y cuerpo HTTP, tanto en petición como en respuesta. Todo es a través del protocolo HTTP.
Mi trabajo actual requiere mucha interacción automatizada con páginas web y WWW::Mechanize me ha permitido hacer desarrollos muchos más rápidos, mucho mejor encapsulados y con un nivel de abstracción menor.
En esta cuarta columna, quiero hacer algo para mostrar de qué es capaz WWW::Mechanize y que ustedes, amables lectores, vean su utilidad real. Lo que haré será un proceso de autenticación en el wiki del proyecto Debian y la obtención de una de sus páginas. Básicamente es loguearme en el wiki y obtener el valor de uno de los campos de una forma. Lo interesante en este asunto viene al automatizar el login en un sistema basado en web, escribir en una forma y enviar forma, obtener campos, etc: WWW::Mechanize lo hace muy simple.
Manos a la obra.
Script
Vamos a empezar por invocar a nuestro intérprete de Perl activando warnings, así como el pragma strict.
#!/usr/bin/perl -w
use strict;
Como necesitaremos un nombre de usuario y contraseña para acceder el wiki en Debian (es necesario para editar páginas), vamos a tomarlos como argumentos:
my $username = $ARGV[0] || die "ERR: Es necesario especificar un nombre de usuario", "\n";
my $password = $ARGV[1] || die "ERR: Es necesario especificiar una contraseña", "\n";
Los argumentos vienen en el arreglo @ARGV: El primer argumento de nuestro programa será el nombre de usuario y el segundo será la contraseña. Si no existen, entonces nuestro programa morirá.
A continuación llamamos al módulo WWW::Mechanize y creamos un objeto de ese tipo.
use WWW::Mechanize;
my $mech = WWW::Mechanize->new;
Bastante simple.
Ahora tenemos que ver qué es lo que haríamos desde un navegador convencional para hacer lo que queremos. Lo más lógico es que entráramos a la página del wiki de Debian, cuya URL es http://wiki.debian.org/. Vamos a hacerlo de la misma manera en el script.
$mech->get('http://wiki.debian.org/');
unless($mech->success) {
die "Tuvimos problemas al acceder la página de Debian", "\n";
}
Básicamente estamos utilizando dos métodos disponibles en Mechanize: get() y success(). El primero hará que nuestro objeto viaje hasta la página que le indiquemos. En cualquier caso, el método success() nos indica por medio de un valor booleano si la última operación que se realizó en Mechanize, fue exitosa o algo falló, lo cual nos sirve para tener más control del flujo de nuestra aplicación. Si no tenemos problemas con get(), entonces el programa no morirá con el mensaje especificado; sin embargo si sí hubo problema, es decir, success() regresa falso, entonces entrará en acción el die(). Continuemos.
Una vez que estamos en Firefox (u Opera, o Internet Explorer, no sé) en la página http://wiki.debian.org/, ¿qué es lo siguiente? Bueno, lo normal, que es al no estar logueados, nos aparezca un link que dice “Login”. En un proceso común tendríamos que dar clic en él para continuar con el proceso de autenticación. Eso mismo haremos:
$mech->follow_link(text => 'Login') or die "ERR: No pude dar clic en login", "\n";
Así de simple. Con el método follow_link() y el parámetro ‘text’, podemos indicarle a Mechanize que donde estamos actualmente, que sería la página frontal del wiki, siga el enlace cuyo texto es ‘Login’. Mechanize nos permite incluso acomodar regex en estos campos, lo cual nos facilitaría aún más al lidiar con contenido dinámico o situaciones más complejas. Como follow_link() regresa realmente un objeto HTTP::Response en caso de éxito, también provee un retorno en caso de falla, que es undef. Así que si hubo una falla al intentar seguir el enlace, moriremos con el mensaje de error especificado.
Una vez que dimos clic en ‘Login’, podemos ver en nuestro navegador convencional que la página donde nos encontramos es aquella con la URL ‘http://wiki.debian.org/UserPreferences’. En realidad pudimos habernos evitado algo de código al empezar nuestro primer get() con esta URL pero intentaba mostrar un poco de las bondades de Mechanize. ¿Qué tal si en tu script no sabes en qué URL se encuentra tu navegador de Mechanize? Puedes usar el método uri() e incluso el método response(). Eso te ayudará a analizar bien el flujo de tu programa.
En fin, en esa página de UserPreferences podemos ver que tenemos una forma para introducir “Name”, “Password”, etc. En realidad esos dos campos son los que realmente nos interesan. Para ésto, nos puede servir mucho la extensión WebDeveloper de Firefox, pues con ella podemos ver la información detallada de las formas en las páginas. Lo que queremos saber es cuál es nombre de la forma para ingresar el nombre de usuario y la contraseña, y además, el nombre de estos dos campos (hay que recordar que comúnmente las formas en HTML llevan un parámetro “name”). O incluso puedes intentar buscar estos parámetros viendo el código HTML directo desde la página.
Luego de ver el HTML (o de usar la extensión de Firefox) nos damos cuenta que en la página de autenticación de Debian hay tres formas, sin embargo, la forma donde se introducen el nombre de usuario y la contraseña, ¡no tiene nombre! Mechanize sabe que algunos webmasters no colocan esta información, así que provee otra forma en que puedes usar esas formas: Por número. Puedes especificar si trabajarás con la primera, segunda, tercera o enésima forma. En nuestro ejemplo, la forma de autenticación es la tercera:
eval {
$mech->form(3);
$mech->submit_form('username' => $username, 'password' => $password);
};
die "ERR: Problemas al enviar la forma: $@", "\n" if $@;
Un bonito pedazo de código aquí. Lo primero que notamos es que usamos un bloque eval{} para encapsular dos métodos, form() y submit_form(). form() selecciona la forma con la que trabajaremos, en este caso la tercera y submit_form() envía esa forma con los campos indicados. Es bastante intuitivo, ¿no crees? Y ya, básicamente eso es lo único que necesitamos para autenticarnos. En caso de falla con submit_form(), la aplicación muere, sin embargo, eso lo intentamos evitar usando eval{}. Si algo dentro del bloque de eval fallara y muriera, eval{} llena una variable especial, $@ con el mensaje de error y ya después nosotros podemos morir o hacer lo que queramos. Bonito, ¿no es así?
Una vez que mandamos la forma, tenemos que asegurarnos que no nos haya mandado mensaje de password erróneo o algo así. Una cosa es que el envío de la forma con sus parámetros sea exitoso o no, y otra diferente que la información de usuario y contraseña sean incorrectos. ¿Cómo saber lo que necesitamos para identificar ésto? Podemos intentar en nuestro Firefox poniendo información errónea en estos campos e intentar loguearse. Al hacer pruebas de este tipo veo que es diferente si el usuario no existe o si la contraseña es errónea.
Si el usuario no existe, la página nos manda un error que dice: “Unknown user name: ”sdfsdfsdf“. Please enter user name and password.”. Si la contraseña está mal (o sea, el usuario existe, pero la contraseña es errónea), la página despliega: “Sorry, wrong password.”. Vamos a ver cómo controlaríamos dichos mensajes:
die "Unknown user name!", "\n" if $mech->content =~ /Unknown user name/;
die "Wrong password!", "\n" if $mech->content =~ /Sorry, wrong password/;
¡Qué sencillo! Morimos en caso de que el método content(), que nos regresa la cadena con todo el HTML de la página actual, contiene alguna de ambas cadenas. Código simple para regexs simples. Si no tenemos ninguno de ambos, asumimos que estaremos ya bien logueados.
Ahora, ¿qué es lo que queremos hacer? Digamos que queremos obtener lo que nuestra propia página tiene escrito. Las páginas de los usuarios son simples páginas del wiki con nuestro nombre de usuario, en mi caso, “http://wiki.debian.org/DavidMorenoGarza”, o lo que es lo mismo “http://wiki.debian.org/$username”:
$mech->get("http://wiki.debian.org/$username");
Estando en dicha página hay en enlace que dice “Edit”, ese es en el que tendríamos que dar clic para entrar a la forma directa de nuestra página:
$mech->follow_link(text => 'Edit');
Y una vez en la página de edición, nos encontramos con una forma nuevamente, que es básicamente la caja de edición de la página. Al leer el HTML o usando WebDeveloper, nos encontramos con que la forma sí tiene nombre y la caja con el texto de la página se encuentra en un campo llamado “editor-textarea”. Pues vamos a obtener ese texto:
$mech->form_name('editor');
my $pagetext = $mech->value("editor-textarea");
Con eso seleccionamos la forma cuyo nombre es “editor” y con value(), seleccionamos el valor de “editor-textarea” y lo guardamos en la variable $pagetext.
¿Qué más podemos hacer? Lo que queramos, quizás queremos usar ese valor y escribirlo en un archivo, modificarlo y volverlo a guardar, obtener el preview luego de modificarlo, etc, etc. En realidad desde aquí la imaginación es el límite.
Fin
Una regla de oro que he aprendido luego de usar mucho WWW::Mechanize es que casi todo lo que puedas hacer en un navegador convencional, lo puedes hacer también con este módulo. No, no puedes interpretar JavaScript, pero muchas veces no lo necesitas si saber leer bien el HTML de una página y entiendes lo que estás realmente haciendo. A final de cuentas si entiendes perfectamente lo que pasa en una conversación por HTTP quizás ni siquiera necesites Mechanize, pues como un amigo me comentaba, Mechanize es simplemente una rompecabezas armado de muchos módulos alrededor de LWP.
Hazme llegar tus preguntas o comentarios a mi correo, damog@espiral.org.mx. Estaré encantado de saber qué usos le das a Mechanize.
Referencias Útiles
Autor
Durante el día, David Moreno Garza (http://www.damog.net/) desarrolla aplicaciones, sistemas y proyectos para una incipiente empresa norteamericana;; adicionalmente es consultor independiente en empresas mexicanas y extranjeras utilizando Perl. Durante la noche intenta salvar al mundo del mal usando expresiones regulares y netiquette.
Licencia de uso
Copyright © 2007 David Moreno Garza.
This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at http://www.opencontent.org/openpub/).
Sobre La Columna.pl
La Columna.pl es una columna quincenal que escribe el autor alrededor de Perl. Está inspirada en las columnas que Randal L. Schwartz ha escrito desde hace varios años. Por medio de recetas, consejos, instructivos y guías, el autor pretende propiciar interés en la gente para que conozca un poco más a fondo este apasionante lenguaje de programación y así fomentar una comunidad más sólida alrededor de él.
Visite http://www.damog.net/la-columnapl.