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()