26.03.2013 Views

062-066PerlLM45

062-066PerlLM45

062-066PerlLM45

SHOW MORE
SHOW LESS

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

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!