Scraping con Python e WebKit2

Ho sviluppato una applicazione in Python e GTK3 per Ubuntu Linux per la gestione e l’invio degli ordini dei tabacchi. Nel corso del tempo ho cercato di rendere sempre più automatico e meno ripetitivo l’invio di un ordine e la raccolta delle informazioni sul suo stato.

Ho usato le librerie PyGObject per accedere ai bindings per WebKitGTK+, la mia piattaforma di riferimento è una Ubuntu 16.04.2 LTS.

apt install gir1.2-webkit2-4.0

Ho realizzato un mini-browser dedicato, che si occupa, tramite una sequenza di script, dell’autenticazione e della navigazione fino al punto da me desiderato ed in fase di chiusura, tramite un’altra sequenza di script, di ottenere informazioni sull’invio e lo stato degli ordini.

Invio ordine

Invio ordine

Uno dei problemi principali è stato ottenere risultati dall’esecuzione del codice Javascript usando solo Python, senza ricorrere a codice C.
Come si può vedere dalla documentazione online di WebKit2 4.0, l’esecuzione di codice Javascript tramite la classe Webkit2.Webview avviene tramite il metodo run_javascript che, tramite una callback,  grazie al metodo run_javascript_finish ritorna un oggetto JavascriptResult.
Il problema è che i bindings Python non gestiscono l’oggetto appena descritto e non si riesce ad estrarne informazioni utili.
Una soluzione, non eccessivamente complessa e neanche troppo “sporca” è quella di segnalare a Python l’arrivo di un messaggio proveniente da Javascript connettendo l’UserContentManager della WebView ad uno speciale segnale "script-message-received::XXX" da noi predefinito usando la clipboard come canale di trasferimento.
Javascript si occuperà di porre l’elemento da inviare nella clipboard e quindi di segnalare la sua disponibilità tramite window.webkit.messageHandlers.XXX.postMessage().

Di seguito un esempio per dimostrare questa soluzione usando GTK 3 e WebKit2 versione 4.


#!/usr/bin/python
# coding: UTF-8
#
# Copyright (C) Francesco Guarnieri 2017
# 

import gi
gi.require_version('WebKit2', '4.0')
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, Gio, WebKit2, Pango

start_url = "http://www.google.it"

# Uno stack di script da eseguire al termine del caricamento di ogni pagina
scripts = ["""var range = document.createRange();
          var elemento = document.getElementsByClassName('g')[0];;
          range.selectNode(elemento);
          window.getSelection().removeAllRanges();
          window.getSelection().addRange(range);
          document.execCommand('copy');    
	  window.webkit.messageHandlers.CANALE_JS.postMessage('Test');""",
       
	"""document.getElementById('lst-ib').value = 'Il blog di Francesco Guarnieri'; 
	   document.getElementById('tsf').submit();"""	       
	]
			
class Browser(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.set_default_size(1000,600)
	self.scripts = None
	contentManager = WebKit2.UserContentManager() 
        contentManager.connect("script-message-received::CANALE_JS", self.__handleScriptMessage)
        if not contentManager.register_script_message_handler("CANALE_JS"):
            print("Error registering script message handler: CANALE_JS")
	
	# Inizializza Webview con il contentManager
        self.web_view = WebKit2.WebView.new_with_user_content_manager(contentManager)
	self.web_view.connect("load-changed", self.__loadFinishedCallback)    
            
        # Peronalizza i settings:
	# Nel nostro caso abilita Javascript ad accedere alla clipboard
        settings = WebKit2.Settings()
        settings.set_property('javascript-can-access-clipboard', True)
        self.web_view.set_settings(settings)

        okButton = Gtk.Button()
        icon = Gio.ThemedIcon(name="emblem-ok-symbolic")
        image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.SMALL_TOOLBAR)
        okButton.add(image)
        okButton.connect("clicked", self.__close)
        cancelButton = Gtk.Button()
        icon = Gio.ThemedIcon(name="window-close-symbolic")
        image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.SMALL_TOOLBAR)
        cancelButton.add(image)
        cancelButton.connect("clicked", self.__close)
        headerBar = Gtk.HeaderBar()
        headerBar.set_show_close_button(False)
	self.set_titlebar(headerBar)
        boxTitle = Gtk.Box(spacing = 6)
        self.spinner = Gtk.Spinner()
        labelTitle = Gtk.Label()
        labelFont = labelTitle.get_pango_context().get_font_description()
        labelFont.set_weight(Pango.Weight.BOLD)
        labelTitle.modify_font(labelFont)
        boxTitle.add(labelTitle)
        boxTitle.add(self.spinner)
        headerBar.set_custom_title(boxTitle)
        self.stopButton = Gtk.Button()      
	icon = Gio.ThemedIcon(name="media-playback-stop-symbolic")
        image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.SMALL_TOOLBAR)
        self.stopButton.add(image)
	self.stopButton.connect("clicked", self.__on_stop_click)
        headerBar.pack_end(self.stopButton)
        box = Gtk.Box()
        Gtk.StyleContext.add_class(box.get_style_context(), "linked")
        box.add(okButton)
        box.add(cancelButton)
        headerBar.pack_start(box)
        browserBox = Gtk.Box()
        browserBox.set_orientation(Gtk.Orientation.VERTICAL)
        browserBox.pack_start(self.web_view, True, True, 0)
        self.add(browserBox)
	
    # Callback richiamata ad ogni cambiamento di stato della fase di load della webview
    # I possibili eventi: WEBKIT_LOAD_STARTED, WEBKIT_LOAD_REDIRECTED, 
    # WEBKIT_LOAD_COMMITTED, WEBKIT_LOAD_FINISHED
    def __loadFinishedCallback(self, web_view, load_event):
        if self.scripts and (len(self.scripts) > 0) and (load_event == WebKit2.LoadEvent.FINISHED):
            self.__waitMode(False)
	    web_view.run_javascript(self.scripts.pop(), None, self.__javascript_finished, None)
	return False
    
    # Callback per gestire la fine dell'esecuzione del codice javascript
    def __javascript_finished(self, webview, task, user_data = None):
        try:
	    # Qui si pone il problema dell'oggetto result di tipo JavascriptResult 
	    # non gestibile
            result = webview.run_javascript_finish(task)
        except Exception as e:
            print("JAVASCRIPT ERROR MSG: %s" % e)

    # Gestisce i messaggi ricevuti da Javascript anche in modo asincrono, non
    # necessariamente al termine dell'esecuzione 
    def __handleScriptMessage(self, contentManager, js_result):
        # Anche qui si pone il problema dell'oggetto js_result di tipo 
	# JavascriptResult non gestibile. Per ottenere risultati senza ricorrere
	# al codice C è possibile usare la clipboard       
        print "[ Ricevuto messaggio da JavaScript ]"
	clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        resultStr = clipboard.wait_for_text()
        if resultStr:
	     print "%s" % resultStr
	return True
    
    # Imposta la modalita di attesa se attiva o meno    
    def __waitMode(self, toggle):
        self.stopButton.set_sensitive(toggle)
        self.stopButton.set_visible(toggle)
        self.web_view.set_sensitive(not toggle)
        if toggle: self.spinner.start()
        else: self.spinner.stop()
    
    # Gestisce la pressione del pulsante di stop
    def __on_stop_click(self, widget):
        self.__waitMode(False)
        self.web_view.stop_loading()

    # Gestisce la chiusura del mini-browser
    def __close(self, widget=None):
        self.web_view.stop_loading()
        self.destroy()
        Gtk.main_quit()

    # Apre il sito
    def open(self, site, scripts = None):
	self.scripts = scripts
        self.__waitMode(True)
        self.web_view.load_uri(site)

if __name__ == "__main__":
    browser = Browser()
    browser.show_all()
    browser.open(start_url, scripts)
    Gtk.main()

Ubuntu 15.04 Vivid Vervet : Promette bene

Ho provato la nuova Ubuntu Vivid Vervet, ancora in beta, ma vicinissima al rilascio (23 Aprile) e devo dire che mi ha fatto una buona impressione.

I tempi di boot sono migliorati in modo sensibile (systemd docet).

Finalmente (vedi post precedenti), i driver NVidia proprietari più recenti, serie 346, sono disponibili e integrati, risolvendo molti dei problemi che affliggevano unity, compiz e compagnia bella.

Da un punto di vista di uno sviluppatore, le librerie GTK3 sono state aggiornate e migliorate, ho notato che il componente GtkTreeView è nettamente più veloce nel mostrare tabelle che prima si “impuntavano” con un numero di righe che superavano le poche decine.

Glade Gtk2: Soluzione drastica

Visto che anche Ubuntu Trusty non prende in considerazione chi sviluppa con gtk2, ho rimosso i pacchetti glade-gtk2, glade-gnome e tutte le dipendenze non più utilizzate:

apt-get remove --purge glade-gtk2 glade-gnome
apt-get autoremove

Ho scaricato i sorgenti della versione 3.8.4 di Glade e li ho compilati ed installati. Tutto ciò perchè passare a gtk3 (almeno fino ad Ubuntu 13.10) portava alle mie applicazioni più problemi che benefici.

Staremo a vedere con la nuova 14.04.

Progetto gestione ordini Tabacchi ver 1.0

E’ pronto per essere usato il mio primo progetto in Python + librerie PyGtK, dopo aver preparato l’ordine, viene regolarmente generato un file Excel pronto per essere importato sul portale Logista.

L’interfaccia è moderna e gradevole grazie alle librerie GTK 2.0 e tutto gira perfettamente su un netbook (Acer Aspire One D250) con una Ubuntu Netbook Remix!

Gestione ordine tabacchi in Python

Era un bel po’ che cercavo un progetto per per iniziare a conoscere il linguaggio Python.
Quale occasione migliore se non quella dell’automazione di un lavoro noioso e facilmente soggetto ad errori come gli ordini dei tabacchi?
Ho utilizzato le librerie PyGtk per la GUI e MySql come Database.
Sono arrivato ad una versione stabile che importa direttamente il listino on-line dal sito Logista (a patto di essere utenti registrati) e consente di impostare gli articoli in magazzino.

Android, vorrei ma non posso!

Android il progetto di una piattaforma libera per lo sviluppo di applicazioni su cellulare è entusiasmante, almeno per un nerd/geek/sviluppatore ;-)
Google, come sempre d’altronde, si muove in modo non convenzionale, fuori dagli schemi, con l’obbiettivo di cambiare uno degli assetti più “ingessati” del mondo tecnologico.

La chiave del successo di questa “alleanza” saranno i produttori di cellulari. Per ora nomi come Motorola, HTC, LG fanno ben sperare.

Naturalmente, solo gli italiani e uno sparuto gruppetto di nazioni non potranno partecipare al concorso per lo sviluppo di applicazioni innovative (in palio 10 milioni di $). Motivazione: troppa burocrazia per l’assegnazione dei premi.

E poi parlano di innovazione…

Java, smart card, biometria

Sono particolarmente orgoglioso di questo progetto.

Per conto dell’azienda per cui lavoro ho sviluppato un software dedicato alla gestione del ciclo di vita dei token (smart card, carte magnetiche, token usb) in Java 6.

Questa applicazione si occupa dell’inizializzazione dei token, dell’enrollment in un ambiente PKI Microsoft, del disegno di template grafici per le smart card e la stampa di quest’ultime (con possibilità di acquisizione immagini tramite web cam o macchina digitale).

Se autorizzato, pubblicherò in futuro anche il video demo. Per il momento potete vedere degli screenshot:

Init smart card Init smart card - Biometric pin