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.
[simterm]apt install gir1.2-webkit2-4.0[/simterm]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.

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
start_url = "http://www.google.it"
google_input_class = 'gLFyf gsfi'
google_form_id = 'tsf'
# 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.getElementsByClassName('{0}')[0].value = 'Il blog di Francesco Guarnieri';
document.getElementById('{1}').submit();""".format(google_input_class, google_form_id)
]
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()
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
webview.run_javascript_finish(task)
except Exception as e:
print("JAVASCRIPT ERROR MSG: {0}".format(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(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()