#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)
Nell’episodio della scorsa settimana abbiamo usato Nokogiri per estrarre del contenuto da una singola pagina HTML. Se avessimo delle necessità più complesse di screen-scraping, per esempio che prevedano il log-in per il recupero dei dati, allora questo semplice approccio non funzionerebbe. Questa volta useremo Mechanize per interagire con un sito al fine di poter estrarre da esso dei dati.
Il sito che useremo sarà Ta-da list. Si tratta di una semplice applicazione di to-do list scritta dalla 37 Signals. Abbiamo già impostato un account e creato una lista. Se vogliamo rivedere la lista, dobbiamo ri-loggarci al sito e cliccare il nome della lista nella home page.
La nostra lista contiene un elenco di prodotti che vorremmo automaticamente importare in una applicazione Rails. Dobbiamo, a questo scopo, interagire col sito della Ta-da List per ottenere gli elementi, dopodichè possiamo riutilizzare lo script già scritto per l’episodio precedente per ottenere un prezzo per ciascun prodotto.
Dal momento che la lista è privata, non ci basta visitare l’URL della lista stessa. Possiamo rendercene conto usando curl
per provare ad ottenere la pagina desiderata:
$ curl http://asciicasts.tadalist.com/lists/1463636 <html><body>You are being <a href="http://asciicasts.tadalist.com/session/new">redirected</a>.</body></html>
Per cui, visto che non possiamo accedere direttamente alla pagina, dovremo autenticarci preventivamente all’applicazione per poter avere accesso alla nostra lista. Qui entra in gioco Mechanize. Mechanize utilizza Nokogiri aggiungendo alcune funzionalità extra per l’interazione coi siti, in modo da poter eseguire operazioni tipo il click sui link e la compilazione di form.
Mechanize è distribuito come gem e lo si installa dunque al solito modo:
sudo gem install mechanize
Una volta installato, possiamo aprire una console Rails per vedere come funziona. Per prima cosa, richiediamo Mechanize:
>> require 'mechanize' => []
Poi instanziamo un agente 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 questo agente possiamo autenticarci alla nostra Ta-da list. Per farlo, dobbiamo raggiungere la pagina di login, inserire una password e fare il submit:
Per recuperare i contenuti di una pagina con una richiesta GET, invochiamo agent.get
, passando l’URL della pagina:
>> 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}>}>
Ciò restituisce un oggetto di tipo Mechanize::Page
che include tutti gli attributi per tale pagina, incluso, per la nostra pagina, la maschera di login.
La chiamata a agent.page
in qualunque momento restituisce la pagina corrente, sulla quale possiamo ispezionare proprietà per accedere ai vari elementi della pagina. Per esempio, per ottenere una form presente in pagina, potremmo chiamare agent.page.forms
che restituisce un array di oggetti Mechanize::Form
. Dal momento che c’è un’unica form nella nostra pagina, possiamo tranquillamente chiamare agent.page.forms.first
per ottenerne un riferimento. Faremo uso di questa form, per cui ne assegnamo il riferimento ad una variabile:
>> 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}>
Se diamo un’occhiata alla collezione di fields
della form nell’output riportato di sopra, vedremo che il campo username è già compilato, ma che il campo password è vuoto. Il riempimento di un campo di form si fa esattamente come impostare un attributo in un oggetto Ruby. Possiamo impostare il campo password con:
form.password = "password"
Il submit della form è altrettanto semplice: tutto ciò che occorre fare è chiamare il metodo form.submit
. Quest’ultimo ci resituirà un altro oggetto di tipo Mechanize::Page
:
>> 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}>
Questa è la pagina che mostra la nostra lista, per cui il prossimo passo sarà quello di cliccare sul link per visualizzare la lista dei prodotti. Di sotto vediamo come appare la pagina in questione sul browser. Può essere utile seguire sul browser i passaggi che deve compiere Mechanize, per capire bene cosa si trova in maschera e che script usare per identificare gli elementi successivi:
Per ottenere la lista che vogliamo, dobbiamo cliccare sul link “Wish List”. Ci sono molti link nella pagina, per cui dobbiamo trovare il modo di ottenere di solo link corretto che Mechanize dovrà poi cliccare. Potremmo prenderci tutti i link presenti in pagina col metodo agent.page.links
e poi iterare su questi controllando la property text
di ciascuno, fintanto che non troviamo quello giusto, ma esiste in realtà un modo più semplice per fare tutto ciò, usando link_with
:
>> agent.page.link_with(:text => "Wish List") => #<WWW::Mechanize::Page::Link "Wish List" "/lists/1463636">
Il metodo link_with
restituisce un link che fa match con la condizione specificata, in questo caso un link con testo “Wish List”. Un metodo analogo esiste anche per le form, ed è chiamato, guardacaso, form_with
. Inoltre ne esistono anche le versioni al plurale links_with
e forms_with
per recuperare collezioni di link o di form che facciano match con una data condizione.
Ora che abbiamo recuperato il nostro link, possiamo chiamarci sopra click
per essere ridiretti alla pagina con l’elenco. (la lunga lista di proprietà della pagina è stata omessa qui sotto):
agent.page.link_with(:text => "Wish List").click => #<WWW::Mechanize::Page {url #<URI::HTTP:0x103261138 URL:http://asciicasts.tadalist.com/lists/1463636>}
Abbiamo finalmente raggiunto la nostra meta ed abbiamo trovato la pagina da cui volevamo estrarre i contenuti. Possiamo usare Nokogiri per proseguire, ma prima dobbiamo trovare il selettore CSS che faccia match con l’elenco di articoli. Come già fatto la volta scorsa, possiamo utilizzare SelectorGadget per trovare il selettore.
Cliccando sul primo elemento della lista, otterremo un selettore per quell’elemento soltanto, ma al click sul successivo tutti gli elementi verranno selezionati ed avremo il selettore che cercavamo: .edit_item
:
Ci sono due metodi nell’oggetto pagina che possiamo usare per estrarre elementi dalla stessa, usando Nokogiri. Il primo di questi è chiamato at
e restituisce un singolo elemento che fa match col selettore:
agent.page.at(".edit_item")
Il secondo metodo è search
. Analogo al prima, con la differenza che restituisce un array di tutti gli elementi che fanno match con l’espressione del selettore (non solo col primo):
agent.page.search(".edit_item")
Nel nostro caso abbiamo una serie di articoli in lista, per cui dobbiamo utilizzare il secondo dei due. Il comando di sopra restituisce un lungo array di oggetti di tipo Nokogiri::XML::Element
, ciascuno dei quali rappresenta un elemento della lista. Possiamo modificare l’output per generare qualcosa di più leggibile:
>> 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!"]
Prendendo il valore della property text
da ciascuno elemento e chiamandovi sopra strip
per rimuovere gli spazi bianchi, rimaniamo con un array di nomi di elementi che è esattamente ciò che vogliamo.
Integrazione di Mechanize nella nostra applicazione Rails
Ora che abbiamo un’idea di come usare Mechanize, possiamo usare ciò che abbiamo appreso all’interno di una applicazione Rails. Useremo la stessa applicazione di negozio virtuale usata nell’ultimo episodio.
Questa volta, invece di estrapolare i prezzi degli elementi da un altro sito, vogliamo importare nuovi prodotti dalla nostra Ta-da list. Creiamo un task rake per farlo. che possiamo mettere nello stesso file /lib/tasks/product_prices.rake
già usato per l’altro task. Ma quale codice ci dobbiamo scrivere? Beh, il codice scritto in console poco fa è un buon punto di partenza per cui possiamo cominciare copiando quello.
Il problema è che è difficile estrarre codice scritto in console, dal momento che ogni comando è mischiato al proprio output. Esiste, tuttavia, un comando che possiamo inserire che restituisce ogni precedente comando immesso:
>> 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
Ora abbiamo una lista di comandi che possiamo copiare nel nostro task rake. Poi ripuliremo lo script e itereremo sugli elementi ottenuti dalla lista della pagina, creando un nuovo Product
per ciascuno:
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
Potremmo cambiare lo script per rimuovere lo username e la password e renderlo più generico mediante l’utilizzo di parametri, ma per il momento lasciamo tutto così com’è. Vediamo se il nostro script funziona:
$ rake import_list (in /Users/asalicetti/rails/apps_for_asciicasts/ep191/shop)
Non sono state sollevate eccezioni al lancio dello script, per cui proviamo a ricaricare la pagina dei prodotti:
Lo script ha funzionato: ora abbiamo un Product per ciascun elemento della nostra Ta-da list. Se lanciamo il task rake scritto la scorsa settimana, poi, possiamo anche ottenere i prezzi per tutti i nuovi articoli:
Dunque, abbiamo raggiunto il nostro obiettivo. Abbiamo usato Mechanize e Nokogiri per navigare attraverso diverse pagine del sito, interagendo con esso, riempiendo form, cliccando su link ed estraendo le informazioni di cui avevamo bisogno. Quella vista quest’oggi è un’ottima soluzione per lo screen-scraping da siti per i quali non esistono alternative migliori per l’estrazione di informazioni.