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.

[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.

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


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()
Condividi:
Subscribe
Notificami
0 Commenti
Inline Feedbacks
View all comments