You also want an ePaper? Increase the reach of your titles
YUMPU automatically turns print PDFs into web optimized ePapers that Google loves.
DESARROLLO • Perl: Amtrack<br />
Un script Perl que vigila precios de Amazon<br />
CAZA<br />
GANGAS<br />
Si eres un caza gangas es probable que te guste este script Perl que monitoriza la evolución de los precios en<br />
Amazon y nos alerta si los baja súbitamente en productos por los que nos hemos interesado.<br />
POR MICHAEL SCHILLI<br />
¿Debería comprar esa cámara digital<br />
a la que he echado el ojo<br />
recientemente? ¿O debería esperar<br />
a que bajara un poco de precio? Estas<br />
preguntas son difíciles de contestar, pero<br />
un vistazo a la evolución de los precios<br />
en los meses anteriores puede indicarnos<br />
qué camino pueden tomar esos precios.<br />
Historial de Precios<br />
Si Amazon ofreciese un historial de precios<br />
para sus productos, de manera similar<br />
a la evolución del precio de las acciones<br />
en las páginas de finanzas, los clientes<br />
podrían disgustarse al descubrir que<br />
han desaprovechado oportunidades. O<br />
podrían llegar a la conclusión de que los<br />
precios van a seguir cayendo y esperar<br />
un momento mejor para comprar. La<br />
famosa tienda online no ofrece este servicio,<br />
así que tendremos que desarrollarlo<br />
nosotros mismos.<br />
Precios a la Vista<br />
El script que vamos a ver, amtrack, parsea<br />
un archivo de configuración<br />
~/.amtrack-rc, parecido al de la Figura<br />
1, para encontrar los productos que<br />
quiere el usuario. Un cronjob llama al<br />
script a intervalos periódicos. Cada vez<br />
que el script conecta con el servicio Web<br />
de Amazon, consulta los precios de los<br />
artículos señalados y los guarda en una<br />
base de datos SQLite local.<br />
Si el precio de un producto cae, el<br />
script envía un correo electrónico con la<br />
URL del producto y el precio a la dirección<br />
configurada en la línea 79 . Lo único<br />
que le resta por hacer al caza gangas es<br />
pulsar la URL en el cliente de correo,<br />
echarle otro vistazo al producto en el<br />
navegador, y en su caso, cazarlo al<br />
vuelo.<br />
Debido a que los precios se<br />
guardan de forma local en una<br />
base de datos, el script puede<br />
solicitar y mostrar información<br />
del histórico al momento. Una<br />
llamada a amtrack -l devuelve<br />
los últimos precios de todos los<br />
productos monitorizados (véase<br />
la Figura 2). Si estamos interesados<br />
en el contenido completo<br />
de la base de datos podemos<br />
62 Número 45 43 WWW.LINUX- MAGAZINE.ES<br />
activar el flag -a , pero tenga en cuenta<br />
que probablemente se mostrará un montón<br />
de información si ha estado monitorizando<br />
precios desde hace tiempo y ha<br />
vigilado diferentes productos.<br />
Cuando se ejecuta sin ninguna opción<br />
en línea de comandos, amtrack realiza<br />
su trabajo mediante los atajos definidos<br />
en el archivo de configuración ~/<br />
.amtrack-rc y actualiza la base de datos<br />
con los últimos precios.<br />
Lista de Deseados<br />
El archivo de configuración tiene dos<br />
columnas. La primera contiene el<br />
Figura 1: El archivo de configuración ~/.amtrack-rc lista<br />
los productos especificados y sus números ASIN.<br />
Vasiliy Yakobchu, Fotolia
Figura 2: Cuando se llama con la opción -l, el script<br />
amtrack lista los precios actuales de todos los productos<br />
que está vigilando.<br />
Figura 3: La base de datos SQLite también puede consul-<br />
tarse con el cliente en línea de comandos sqlite3.<br />
número ASIN del producto en cuestión,<br />
con una breve descripción a su derecha,<br />
separado por uno o varios espacios en<br />
blanco. Esto no tiene ninguna influencia<br />
en la base de datos, pero hace que la<br />
alerta por correo electrónico sea más<br />
fácil de leer.<br />
Las líneas de comentario comienzan<br />
con el símbolo de almohadillas (#), y el<br />
script las ignora, al igual que ignora los<br />
espacios en blanco. La función<br />
config_read() (Listado 1, líneas 91-111)<br />
carga la configuración y devuelve dos<br />
referencias: una al array ordenado @config,<br />
y la otra al hash %config . El array<br />
contiene parejas de ASIN y valores de<br />
texto, mientras que el hash mapea directamente<br />
las ASINs a los textos para<br />
poder buscarlas rápidamente.<br />
Oportunidades<br />
El módulo Net::Amazon de CPAN proporciona<br />
una interfaz orientada a objetos<br />
al web service (basado en RESR) de<br />
Amazon. Si introducimos el número<br />
ASIN del producto, el módulo contacta<br />
con Amazon y recupera el precio.<br />
Cuando comenzó, Amazon sólo vendía<br />
libros, que podían ser identificados<br />
de manera unívoca por sus números<br />
ISBN. A medida que el catálogo de productos<br />
creció, añadió el número ASIN,<br />
que tiene una estructura similar pero<br />
también incluye letras, y de esta manera<br />
es capaz de direccionar muchos más productos.<br />
El método request() de la clase<br />
Net::Amazon acepta un objeto Net::Ama-<br />
zon::Re-quest::ASIN con parámetros<br />
que incluyen el<br />
número ASIN de un producto.<br />
Tras hacer esto, controla las<br />
comunicaciones con la página<br />
web de Amazon y devuelve un<br />
objeto de clase Net::Amazon::Response::ASIN.<br />
La propiedad<br />
is_success() del objeto<br />
nos indica si la petición ha<br />
tenido éxito. En este caso, el<br />
método properties() devuelve<br />
un único objeto de clase<br />
Net::Amazon::Property que<br />
contiene el producto coincidente,<br />
incluida una descripción<br />
del producto, puntuación<br />
de los clientes, URL a las imágenes,<br />
y mucho más, incluyendo<br />
precio. Amazon ofrece<br />
diferentes opciones de búsqueda<br />
(por autor por ejemplo), por lo<br />
que properties() también puede devolver<br />
múltiples entradas. El método OurPrice()<br />
de una propiedad devuelve el precio<br />
actual de un producto en formato $X.XX,<br />
£X.XX (para Reino Unido) o EUR X,XX<br />
(para otras ubicaciones europeas: ver<br />
más abajo).<br />
Caché del Histórico<br />
El script también se apoya en el módulo<br />
Cache::Historical de CPAN, que no sólo<br />
guarda información bajo un índice primario,<br />
como cualquier caché normal,<br />
sino que también inserta una fecha que<br />
usa como índice secundario. El script<br />
guarda los precios de los productos,<br />
con el ASIN como<br />
índice primario, y guarda la<br />
información recuperada en la<br />
caché. Tras el escenario,<br />
Cache::Historical se apoya en<br />
una base de datos SQLite<br />
basada en archivo, listada por<br />
el módulo como requerimiento,<br />
y que también instala<br />
gracias a la cobertura de 10$.<br />
CPAN. El parámetro sqlite_file<br />
del constructor new() fija el<br />
nombre del archivo<br />
~/.amzn-tracker-sqlite en el<br />
cual se deposita la base de<br />
datos. Si así lo queremos,<br />
podemos consultar la base de<br />
datos SQLite con el programa<br />
cliente sqlite3 para ver la<br />
información, como muestra la<br />
Figura 3.<br />
WWW.LINUX- MAGAZINE.ES<br />
Perl: Amtrack • DESARROLLO<br />
La llamada get_interpolated() de la<br />
caché recupera el valor de una fecha<br />
concreta desde la base de datos (la fecha<br />
actual en el script) y una clave específica<br />
(el ASIN de un producto). El script<br />
guarda esto en la variable $last_price, y<br />
entonces actualiza la base de datos con<br />
el último valor de la página Web de<br />
Amazon. Tras hacer esto, recupera el<br />
precio actual desde la base de datos nuevamente<br />
y la compara con $last_price.<br />
Por contra, el método values()<br />
devuelve una lista de parejas de valores<br />
que coinciden con la clave especificada.<br />
Cada pareja es una referencia a un array<br />
que contiene la fecha como un objeto<br />
DateTime y el precio.<br />
Do What I Mean<br />
Si el precio actual de un producto bajo<br />
supervisión es menor que el último precio<br />
guardado en la base de datos, el<br />
script genera un correo electrónico en la<br />
línea 78 (Figura 4). A pesar de que<br />
muchos módulos de CPAN pueden<br />
enviar correos electrónicos, Mail::DWIM<br />
(Do What I Mean) es uno de los más<br />
sencillos: exporta la función mail(), que<br />
acepta un destinatario, una línea de<br />
asunto, y el cuerpo de texto del correo<br />
electrónico como parámetros. Configura<br />
valores por efecto con sentido para el<br />
resto de parámetros, como el remitente o<br />
el trasporte del correo electrónico (en<br />
este caso, el usuario activo más el dominio<br />
configurado y el demonio activo<br />
Sendmail). En cuanto a otros mecanis-<br />
Figura 4: Llegada de un correo electrónico que anuncia<br />
que el precio del robot aspiradora Roomba [4] ha bajado<br />
Figura 5: Configuración de Log4perl para el script.<br />
Número 45<br />
63
DESARROLLO • Perl: Amtrack<br />
mos de transporte de correo electrónico,<br />
también soporta SMTP especificando el<br />
host. Estos valores por efecto se fijan<br />
como parámetros en el archivo local<br />
.maildwim. Para más detalles acerca de<br />
esto, sólo tiene que leer la página man<br />
Mail::DWIM.<br />
El correo electrónico también contiene<br />
la URL del producto, que se consigue<br />
añadiendo /dp/$asin a la URL base de la<br />
página web de Amazon.<br />
Logueo Profesional<br />
Para mantener al usuario al corriente de<br />
lo que está haciendo el script se usa<br />
Log4perl para registrar sus actividades.<br />
El archivo amtrack.l4p, inicializado por<br />
Log4perl, se guarda en el mismo directorio<br />
que el script (Figura 5). Para permitir<br />
al script que encuentre el archivo de<br />
001 #!/usr/bin/perl -w<br />
002 use strict;<br />
003 use Getopt::Std;<br />
004 use Net::Amazon;<br />
005 use<br />
Net::Amazon::Request::ASIN;<br />
006 use Log::Log4perl qw(:easy);<br />
007 use Cache::Historical 0.02;<br />
008 use DateTime;<br />
009 use Mail::DWIM qw(mail);<br />
010 use FindBin qw($Bin);<br />
011<br />
012 my ($home) = glob “~”;<br />
013 my $amzn_rc =<br />
014<br />
015<br />
016<br />
“$home/.amtrack-rc”;<br />
Log::Log4perl->init(“$Bin/amt<br />
rack.l4p”);<br />
017 my $cache =<br />
Cache::Historical->new(<br />
018 sqlite_file =><br />
019 “$home/.amtrack-sqlite”<br />
020 );<br />
021<br />
022 my $UA = Net::Amazon->new(<br />
023 token =><br />
‘YOUR_AMZN_TOKEN’,<br />
024 # locale => ‘uk’,<br />
025 );<br />
026<br />
027 my($config, $txt_by_asin) =<br />
028<br />
config_read();<br />
029 getopts(“al”, \my %opts);<br />
configuración, incluso si se le llama<br />
desde un directorio diferente (por ejemplo,<br />
bin/amtrack o amtrack desde el<br />
directorio de usuario), el módulo Find-<br />
Bin nos ayuda a exportar la variable $Bin<br />
como directorio donde se encuentra el<br />
script, asegurando de esta manera que<br />
$Bin/amtrack.l4p representa la ruta<br />
absoluta a la configuración de Log4perl.<br />
La configuración de Log4perl no es<br />
precisamente sencilla. Después de todo,<br />
queremos que el script escriba sus actividades<br />
normales en el archivo de log<br />
(Figura 6) y muestre los errores en la<br />
consola.<br />
Se llama a un cronjob a intervalos<br />
regulares para añadir información en el<br />
archivo de registro (por defecto), pero se<br />
enviarán los errores (como una conexión<br />
fallida de red) a STDERR, y esto provoca<br />
Listado 1: amtrack<br />
64 Número 45 WWW.LINUX- MAGAZINE.ES<br />
030<br />
031 if($opts{l} or $opts{a}) {<br />
032 for my $key (sort keys<br />
%$txt_by_asin) {<br />
033 my $txt =<br />
$txt_by_asin->{$key};<br />
034 for my $val<br />
($cache->values( $key )) {<br />
035 my($dt, $price) =<br />
@$val;<br />
036 print “$dt $txt<br />
$price\n”;<br />
037 last if $opts{l};<br />
038 }<br />
039 }<br />
040 } else {<br />
041 update($config);<br />
042 }<br />
043<br />
044 ####################<br />
045 sub fix_price {<br />
046 ####################<br />
047 my($price) = @_;<br />
048<br />
049 if(defined $price) {<br />
050 $price =~ s/[^\d]//g;<br />
051 $price =~<br />
s/..$/.$&/g;<br />
052 }<br />
053 return $price;<br />
054 }<br />
055<br />
056 ####################<br />
057 sub update {<br />
058 ####################<br />
059 my($config) = @_;<br />
que cron, que inició el script, envíe un<br />
correo electrónico al administrador.<br />
Se define un único usuario registrado<br />
para la categoría main (es decir, para el<br />
programa principal). Net::Amazon también<br />
permite Log4perl, y otra entrada en<br />
el archivo de configuración mostrará<br />
rápidamente los detalles de las comunicaciones<br />
con el servidor Web de Amazon<br />
por pantalla. El usuario main controla<br />
dos appenders: Logfile y Screen. Para<br />
asegurarnos de que Screen sólo recibe<br />
mensajes con prioridad ERROR superior,<br />
la línea<br />
log4perl.appender.Screen.U<br />
Threshold = ERROR<br />
configura este umbral en la definición<br />
del appender.<br />
060<br />
061 for my $line (@$config) {<br />
<strong>062</strong><br />
063 my($asin, $txt) = @$line;<br />
064 my $now =<br />
065<br />
DateTime->now();<br />
066 my $last_price =<br />
067<br />
068<br />
fix_price($cache-><br />
get_interpolated($now,<br />
$asin));<br />
069 track($asin, $txt,<br />
070<br />
$cache);<br />
071 my $price_now =<br />
072<br />
073<br />
fix_price($cache-><br />
get_interpolated($now,<br />
$asin));<br />
074 if(defined $last_price<br />
and<br />
075 defined $price_now) {<br />
076<br />
077 if( $price_now <<br />
$last_price) {<br />
078 mail(<br />
079 to =><br />
‘foo@bar.com’,<br />
080 subject =><br />
“[amtrack] “ .<br />
081 “$txt cheaper<br />
($price_now < “ .<br />
082 “$last_price)”,
DESARROLLO • Perl: Amtrack<br />
083 text => “URL: “<br />
084<br />
.<br />
“http://amazon.com/dp/$asin”,<br />
085 );<br />
086 }<br />
087 }<br />
088 }<br />
089 }<br />
090<br />
091 ####################<br />
092 sub config_read {<br />
093 ####################<br />
094<br />
095 my @config = ();<br />
096 my %config = ();<br />
097<br />
098 open AMZNRC, “$amzn_rc”<br />
or<br />
099 die “Cannot open<br />
$amzn_rc”;<br />
100 while() {<br />
101 s/#.*//;<br />
102 next if /^\s*$/;<br />
En caso de que queramos aprender<br />
más acerca del entorno de trabajo<br />
Log4perl, podemos visitar la página Web<br />
de Log4perl [2], que tiene una documentación<br />
exhaustiva y una FAQ con configuraciones<br />
frecuentes a modo de ejemplo.<br />
No sin Mi Token<br />
Amazon requiere un token para los<br />
scripts que estén trabajando con su web<br />
service. El token está a libre disposición<br />
de cualquiera que se registre y acepte<br />
las condiciones [3]. Tras recibirlo, sólo<br />
tenemos que remplazar<br />
YOU_AMZN_TOKEN de la línea 23 con<br />
el token correcto.<br />
El script funcionará con la página<br />
Web de Estados Unidos o con la de<br />
cualquier otro país, como puede ser la p<br />
del Reino Unido. Para esta última, sim-<br />
Listado 1: amtrack (Continuación)<br />
103 chomp;<br />
104 my($asin, $txt) =<br />
split ‘ ‘, $_, 2;<br />
105 push @config, [$asin,<br />
$txt];<br />
106 $config{ $asin } =<br />
$txt;<br />
107 }<br />
108 close AMZNRC;<br />
plemente tenemos que descomentar<br />
locale => ‘uk’ en la línea 24.<br />
Para otros países europeos, los precios<br />
debería mostrarse en formato EUR X,XX,<br />
pero la función fix_price los convierte a<br />
un formato en punto flotante adecuado,<br />
que podemos comparar con el uso de<br />
operaciones matemáticas.<br />
Como las cifras en Estados Unidos usan,<br />
para el punto flotante, tanto un punto<br />
como comas para separar los miles,<br />
fix_price() simplemente desecha todo lo<br />
que no es un dígito e inserta el punto decimal<br />
delante de los últimos dos dígitos.<br />
Instalación<br />
Un shell de CPAN instala los módulos de<br />
CPAN especificados al comienzo del<br />
script, y resuelve inmediatamente todas<br />
las dependencias al mismo tiempo.<br />
Una entrada crontab con el formato<br />
230***<br />
/path/to/amtrack<br />
llama al script una vez al día,<br />
23 minutos después de medianoche.<br />
Esto debería ser más<br />
que suficiente para mantenernos<br />
al día. El módulo<br />
Net::Amazon se asegura de<br />
66 Número 45 WWW.LINUX- MAGAZINE.ES<br />
109<br />
110 return \@config,<br />
111 }<br />
112<br />
\%config;<br />
113 ####################<br />
114 sub track {<br />
115 ####################<br />
116 my($asin, $txt, $cache) =<br />
117<br />
@_;<br />
118 INFO “Tracking asin $asin”;<br />
119<br />
120 my $req =<br />
121<br />
Figura 6: Fragmento del archivo de log tras una ejecución<br />
exitosa del script.<br />
Net::Amazon::Request::ASIN->n<br />
ew(<br />
122 asin => $asin);<br />
123<br />
124 my $resp =<br />
125<br />
$UA->request($req);<br />
126 if($resp->is_success()) {<br />
127 my($prop) =<br />
$resp->properties();<br />
128 my $price =<br />
$prop->OurPrice();<br />
129 INFO “Tracking $asin “,<br />
130 “($txt): $price”;<br />
131<br />
$cache->set(DateTime->now(),<br />
132 $asin,<br />
$price) if $price;<br />
133 } else {<br />
134 ERROR “Can’t fetch asin<br />
$asin: “,<br />
135 $resp->message();<br />
136 }<br />
137 }<br />
que el script mantiene las condiciones de<br />
uso de Amazon, e impone limites si el<br />
usuario recupera precios a intervalos<br />
muy cortos.<br />
Mejoras<br />
Para mejorar el script podríamos asignar<br />
un límite para cada precio en el archivo<br />
de configuración e indicar al script que<br />
no nos notifique a menos que el precio<br />
baje por debajo de este valor. Otra aplicación<br />
podría ser dibujar un gráfico de<br />
los cambios en el precio en un período<br />
de tiempo. Los módulos RRDTool::OO o<br />
Imager::Plot de CPAN serían perfectos<br />
para este propósito. ■<br />
RECURSOS<br />
[1] Listados de este artículo: http://<br />
www.linux-magazine.es/Magazine/<br />
Downloads/45<br />
[2] Log4perl homepage: http://<br />
log4perl.com<br />
[3] Los tokens de Amazon Web Services<br />
están disponibles en: http://<br />
www.amazon.com/soap<br />
[4] El robot aspirador Roomba: http://<br />
www.amazon.com/<br />
iRobot-Roomba-Intelligent-Floorva<br />
c-Robotic/dp/B00008439Y