#191 Mechanize
- Download:
- source codeProject Files in Zip (93.6 KB)
- mp4Full Size H.264 Video (19.3 MB)
- m4vSmaller H.264 Video (12 MB)
- webmFull Size VP8 Video (29.8 MB)
- ogvFull Size Theora Video (29.2 MB)
En el episodio de la semana pasada utilizábamos Nokogiri para extraer contenidos de una página HTML. Sin embargo, este mecanismo no funcionará si tenemos necesidades más complicadas, como por ejemplo recuperar datos que requieren que por ejemplo iniciemos sesión en un site, para extraer contenido de una página HTML. Esta vez utilizaremos Mechanize para interactuar con un site de forma que podamos extraer esos datos.
Nuestro site de ejemplo será Ta-da list, que es una aplicación sencilla de to-dos escrita por 37 Signals. Ya hemos creado una cuenta y hemos creado una lista. Si queremos volver a ver la lista tendremos que entrar en el site y posteriormente hacer click en el nombre de la lista en la página principal.
La lista contiene un listado de productos que queremos importar automáticamente a nuestra aplicación Rails. Tendremos que interactuar con Ta-da List para obtener los ítems, y luego podremos utilizar el script que escribimos en el episodio anterior para ver el precio de cada uno.
No podemos simplemente visitar la URL de la lista porque ésta es privada. Lo comprobaremos si utilizamos curl
para intentar recuperar la página.
$ curl http://asciicasts.tadalist.com/lists/1463636 <html><body>You are being <a href="http://asciicasts.tadalist.com/session/new">redirected</a>.</body></html>
Asi que como no podemos acceder a esta página directamente tendremos que iniciar sesión en la aplicación antes de acceder a nuestra lista. Aquí es donde entra Mechanize. Mechanize utiliza Nokogiri y añade cierta funcionalidad extra para interactuar con los sites de forma que se puede usar para tareas como hacer clic en enlaces o enviar formularios.
Como Mechanize es una gema se instala de la forma habitual:
sudo gem install mechanize
Una vez que está instalado podemos abrir la consola de Rails para ver cómo funciona. Primero requerimos Mechanize.
>> require 'mechanize' => []
Luego instanciaremos un agente de Mechanize:
> agent = WWW::Mechanize.new => #<WWW::Mechanize:0x101c74780 @follow_meta_refresh=false, @proxy_addr=nil, @digest=nil, @watch_for_set=nil, @html_parser=Nokogiri::HTML, @pre_connect_hook=#<WWW::Mechanize::Chain::PreConnectHook:0x101c74190 @hooks=[]>, @open_timeout=nil, @log=nil, @keep_alive_time=300, @proxy_pass=nil, @redirect_ok=true, @post_connect_hook=#<WWW::Mechanize::Chain::PostConnectHook:0x101c74168 @hooks=[]>, @conditional_requests=true, @password=nil, @cert=nil, @user_agent="WWW-Mechanize/0.9.3 (http://rubyforge.org/projects/mechanize/)", @pluggable_parser=#<WWW::Mechanize::PluggableParser:0x101c74550 @default=WWW::Mechanize::File, @parsers={"application/xhtml+xml"=>WWW::Mechanize::Page, "text/html"=>WWW::Mechanize::Page, "application/vnd.wap.xhtml+xml"=>WWW::Mechanize::Page}>, @verify_callback=nil, @connection_cache={}, @proxy_user=nil, @pass=nil, @ca_file=nil, @request_headers={}, @user=nil, @cookie_jar=#<WWW::Mechanize::CookieJar:0x101c746b8 @jar={}>, @scheme_handlers={"https"=>#<Proc:0x00000001020c12c0@/Library/Ruby/Gems/1.8/gems/mechanize-0.9.3/lib/www/mechanize.rb:152>, "file"=>#<Proc:0x00000001020c12c0@/Library/Ruby/Gems/1.8/gems/mechanize-0.9.3/lib/www/mechanize.rb:152>, "http"=>#<Proc:0x00000001020c12c0@/Library/Ruby/Gems/1.8/gems/mechanize-0.9.3/lib/www/mechanize.rb:152>, "relative"=>#<Proc:0x00000001020c12c0@/Library/Ruby/Gems/1.8/gems/mechanize-0.9.3/lib/www/mechanize.rb:152>}, @redirection_limit=20, @proxy_port=nil, @history_added=nil, @auth_hash={}, @read_timeout=nil, @keep_alive=true, @history=[], @key=nil>
Con este agente podremos entrar en nuestra lista, accediendo a la página de login, introduciendo la clave y luego enviando el formulario de login.
Para leer los contenidos de una página con una petición GET invocaremos agent.get
, pasando la URL de la página.
>> agent.get("http://asciicasts.tadalist.com/session/new") => #<WWW::Mechanize::Page {url #<URI::HTTP:0x101c5c180 URL:http://asciicasts.tadalist.com/session/new>} {meta} {title "Ta-da List"} {iframes} {frames} {links #<WWW::Mechanize::Page::Link "forgot password?" "/account/send_forgotten_password">} {forms #<WWW::Mechanize::Form {name nil} {method "POST"} {action "/session"} {fields #<WWW::Mechanize::Form::Field:0x1035f1708 @name="username", @value="asciicasts"> #<WWW::Mechanize::Form::Field:0x1035ef4a8 @name="password", @value="">} {radiobuttons} {checkboxes #<WWW::Mechanize::Form::CheckBox:0x1035eeb48 @checked=false, @name="save_login", @value="1">} {file_uploads} {buttons}>}>
Esto nos devuelve un objeto de tipo Mechanize::Page
que incluye todos los atributos de la página incluyendo, en nuestro caso, el formulario de login.
Si en cualquier momento llamamos a agent.page
nos devolverá la página actual y podremos llamar a sus propiedades para acceder a los diferentes elementos de la página. Por ejemplo, para acceder a los formularios de la página podríamos invocar agent.page.forms
que nos devolverá un array de objetos Mechanize::Form
y dado que solo hay un formulario en nuestra página podremos ejecutar agent.page.forms.first
para obtener una referencia. Este formulario nos hará falta más tarde así que se lo asignaremos a una variable.
>> form = agent.page.forms.first => #<WWW::Mechanize::Form {name nil} {method "POST"} {action "/session"} {fields #<WWW::Mechanize::Form::Field:0x1035f1708 @name="username", @value="asciicasts"> #<WWW::Mechanize::Form::Field:0x1035ef4a8 @name="password", @value="">} {radiobuttons} {checkboxes #<WWW::Mechanize::Form::CheckBox:0x1035eeb48 @checked=false, @name="save_login", @value="1">} {file_uploads} {buttons}>
Si nos fijamos en la coleección fields
del formulario en la salida anterior veremos que el campo usuario ya ha sido rellenado pero que el campo de la contraseña está aún vacío. Para rellenar un campo de un formulario sólo tenemos que hacerlo de la misma manera que modificaríamos un atributo en un objeto Ruby:
form.password = "password"
El envío del formulario es igual de fácil: sólo hay que hacer form.submit
. Esto nos devolverá otro objeto Mechanize::Page
distinto.
>> form.submit => #<WWW::Mechanize::Page {url #<URI::HTTP:0x10336ad68 URL:http://asciicasts.tadalist.com/lists>} {meta} {title "My Ta-da Lists"} {iframes} {frames} {links #<WWW::Mechanize::Page::Link "Highrise" "http://www.highrisehq.com"> #<WWW::Mechanize::Page::Link "Try it free" "http://www.highrisehq.com"> #<WWW::Mechanize::Page::Link "Tada-mark-bg" "http://asciicasts.tadalist.com/lists"> #<WWW::Mechanize::Page::Link "Create a new list" "/lists/new"> #<WWW::Mechanize::Page::Link "Wish List" "/lists/1463636"> #<WWW::Mechanize::Page::Link "Rss" "http://asciicasts.tadalist.com/lists.rss?token=8ee4a563af677d3ebf3ceb618dac600a"> #<WWW::Mechanize::Page::Link "Log out" "/session"> #<WWW::Mechanize::Page::Link "change password" "/account/change_password"> #<WWW::Mechanize::Page::Link "change email" "/account/change_email_address"> #<WWW::Mechanize::Page::Link "cancel account" "/account/destroy"> #<WWW::Mechanize::Page::Link "FAQs" "http://www.tadalist.com/help"> #<WWW::Mechanize::Page::Link "Terms of Service" "http://www.tadalist.com/terms"> #<WWW::Mechanize::Page::Link "Privacy Policy" "http://www.tadalist.com/privacy"> #<WWW::Mechanize::Page::Link "other products from 37signals" "http://www.37signals.com">} {forms}>
Esta es la página que muestra nuestras listas, por tanto el próximo paso es hacer click en el enlace del listado de productos. Abajo se muestra la página tal y como aparece en pantalla. Cuando utilizamos Mechanize es bastante útil ir repitiendo los pasos en nuestro navegador para que nos sea más fácil determinar cuál es el siguiente paso a automatizar.
Para acceder al listado tenemos que hacer clic en el enlace "Wish List". Hay algunos enlaces en la página y necesitamos averiguar cómo recuperar el enlace adecuado para que Mechanize haga clic en él. Podríamos recuperar todos los enlaces con agent.page.links
y luego iterar sobre ellos leyendo la propiedad text
de cada uno de ellos hasa que encontremos el que nos interesa pero hay una forma más fácil de hacerlo empleando link_with
:
>> agent.page.link_with(:text => "Wish List") => #<WWW::Mechanize::Page::Link "Wish List" "/lists/1463636">
El método link_with
devolverá un enlace que cumpla cierta condición, en este caso que el texto sea "Wish List". Existe un método similar para los formularios denominado, cómo no, form_with
y también existen los métodos plurales links_with
y forms_with
para encontrar varios enlaces o formularios que cumplan una condición dada.
Y ya que hemos encontrado nuestro enlace podemos ejecutar click
para ser redirigidos a la página de la lista. (Obsérvese que el listado de las propiedades de la página ha sido omitido debajo, por su longitud).
agent.page.link_with(:text => "Wish List").click => #<WWW::Mechanize::Page {url #<URI::HTTP:0x103261138 URL:http://asciicasts.tadalist.com/lists/1463636>}
Hemos alcanzado al fin nuestro destino y hemos encontrado la página de la que queremos extraer los datos. Podemos usar Nokogiri para hacerlo pero en primer lugar tenemos que encontrar el selector CSS que escoge los ítems de la lista. Como hicimos la vez antrior utilizaremos SelectorGadget para determinar qué selector es.
Haciendo clic en el primero item de la lista veremos que se selecciona tan sólo ese item, pero si seleccionamos el siguiente también se seleccionarán todos los demás y ya tendremos el selector que necesitábamos: .edit_item
.
Hay dos métodos en el objeto page
que podemos usar para extraer elementos de una página utilizando Nokogiri. El primer de ellos se llama at
y devolverá un único elemento que casa con un selector.
agent.page.at(".edit_item")
El segundo método es search
. Es similar, pero devuelve un array de todos los elementos que son seleccionados.
agent.page.search(".edit_item")
Como tenemos varios ítems en nuestra lista vamos a utilizar el segundo. El comando anterior devolverá un largo array de Nokogiri::XML::Element
cada uno de los cuales representa un ítem de la lista. Podemos modificar la salida para mostrar algo más legible.
>> agent.page.search(".edit_item").map(&:text).map(&:strip) => ["Settler's of Catan", "Go for Beginners book", "Nintendo DSi", "Chess Set", "Dark Knight on Blu Ray", "Modern Warfare 2 for Xbox", "Scrabble", "Dragon Age Strategy Guide", "Wario Land: Shake It!"]
Si leemos el atributo text
y sobre él invocamos el método strip
para eliminar los espacios en blanco nos quedaremos con un array de nombres que es exactamente lo que buscamos.
Cómo integrar Mechanize en nuestra aplicación Rails
Ahora que tenemos una idea de cómo utilizar Mechanize podemos utilizar todo lo aprendido en una aplicación Rails. Utilizaremos la misma aplicación de tienda del episodio anterior.
Esta vez en lugar de hacer scraping de los precios en otro sitio web queremos importar los productos desde nuestra lista en Ta-da Lists. Crearemos una tarea de Rake que podemos poner en el mismo fichero en que pusimos la otra /lib/tasks/product_prices.rake
. Pero ¿qué código tendremos que añadir a la tarea? Un buen punto de partida es el código que hemos escrito anteriormente en la consola, así que podemos epezar copiándolo.
El problema aquí es que es difícil extrer el código que hemos tecleado en la consola porque está entremezclado con la salida de cada comando. Sin embargo hay un comando que podemos usar y que nos devolverá cada una de las sentencias que hemos tecleado.
>> puts Readline::HISTORY.entries.split("exit").last[0..-2].join("\n") require 'mechanize' agent = WWW::Mechanize.new agent.get("http://asciicasts.tadalist.com/session/new") form = agent.page.forms.first form.password = "password" form.submit agent.page.link_with(:text => "Wish List").click agent.page.search(".edit_item").map(&:text).map(&:strip) => nil
Ahora que tenemos un listado de los comandos podemos copiarlos en nuestra tarea Rake. Luego limpiaremos un poco el script e iteraremos sobre cada uno de los items recuperados, creando un número objeto Product
para cada uno.
desc "Import wish list" task :import_list => :environment do require 'mechanize' agent = WWW::Mechanize.new agent.get("http://asciicasts.tadalist.com/session/new") form = agent.page.forms.first form.password = "password" form.submit agent.page.link_with(:text => "Wish List").click agent.page.search(".edit_item").each do |product| Product.create!(:name => product.text.strip) end end
Podríamos modificar este script para eliminar el usuario y clave y convertirlos en argumentos que se pasan por parámetros, pero por ahora lo dejaremos así. Veamos si nuestra tarea de Rake funciona.
$ rake import_list (in /Users/eifion/rails/apps_for_asciicasts/ep191/shop)
No se han lanzado excepciones durante la ejecución del script, así que recarguemos la página de productos.
El script ha funcionado: ahora tenemos un Product para cada uno de los ítems guardados en nuestra lista. Si ejecutamos la tarea Rake que escribimos la semana pasada podremos obtener los precios de todos estos nuevos ítems.
Hemos alcanzado nuestro objetivo utilizando Mechanize y Nokogiri para navegar por varias pagínas de un site, interactuando con ellas para rellenar formularios y hacer clic en enlaces y extrayendo la información que buscábamos. Esta es una gran solución para extraer datos de sitios web cuando no hay una alternativa mejor.