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

Beebox N3000 ed Ubuntu

Il mese scorso mi è stato “commissionato” un mediacenter di buona qualità, silenzioso, non eccessivamente costoso (il budget era massimo 200€), che potesse essere acceso ed usato con un telecomando e dove possibilmente poter riutilizzare un  disco SSD di piccola taglia.

Il Beebox N3000 mi è sembrato il candidato ideale, ad un prezzo di 133€ era imperdibile. Per 45€ ho aggiunto due moduli ram Corsair da 4GB DDR3 (1600 MHz, CL9), identici, per usufruire dei benefici della modalità dual-channel.

Beebox N3000

Beebox N3000

Questo piccolo gioiellino fanless si basa su un SoC Braswell con un Intel Dual-Core Celeron N3000 e dispone di una scheda di rete gigabit, un modulo WIFI 802.11ac e Bluetooth 4.0, 3 porte USB3, una porta USB3 Type C,  2 prese HDMI e 1 Displayport.

Inoltre, supporta dischi in formato mSata e anche Sata da 2,5″, insieme a due slot per memorie DDR3,  una presa ir e telecomando incluso.

Le mie istruzioni per realizzare un mediacenter basato su Ubuntu 16.04LTS con Kodi, si applicano a questo pc con poche modifiche, essenzialmente legate alla scheda video integrata nella CPU e un differente driver per la presa IR.

Senza entrare troppo nello specifico, posso testimoniare che questo mediacenter è in grado di visualizzare senza alcun affanno video full-hd con codifica H.265, per non parlare di H.264. Non ho potuto verificare con risoluzioni superiori (4K), ma forse siamo ai limiti delle potenzialità di questo ottimo sistema.

Macbook 2006 e Ubuntu 16.04 con EFI

In famiglia avevamo un vecchio Macbook 2006 che se ne stava in un cassetto già da molto tempo. Perchè non riportarlo a nuova vita?

Oltre OS X Lion 10.7.5 non era possibile andare e realisticamente con 1gb di ram e un hard-disk da 5400rpm eravamo già ai limiti.

Grazie ad un precedente “upgrade” del mio mediacenter avevo un disco SSD da 64gb che mi avanzava, un vecchio portatile con lo schermo rotto e mai riparato (antieconomico) avrebbe fornito la ram per espandere il Macbook al massimo di 3gb.

Ho deciso di passare ad Ubuntu 16.04.1 LTS.  Ho scelto la versione a 64bit, visto che il portatile ha una cpu Intel Core 2 Duo.

Anche in questo caso (vedi il mio post su Lenovo Ideapad Flex 10) mi sono trovato con un problema di sistemi EFI a 32bit con sistemi potenzialmente a 64bit.
Neanche l’immagine di sistema più recente (con Ubuntu 16.04.1) supporta questa situazione ibrida. Ho dovuto quindi cercare delle soluzioni per generare un’immagine da scrivere sulla mia chiavetta usb che potesse essere eseguita dal bios EFI a 32bit del Macbook.

Questa è la sequenza delle istruzioni (da eseguire con diritti di root su un sistema con una Ubuntu 16.04), che ho lievemente modificato rispetto all’originale dedicato ad un ASUS EeeBook:

apt install p7zip-full

wget http://releases.ubuntu.com/16.04/ubuntu-16.04.1-desktop-amd64.iso

# Suppongo che la chiavetta USB sia sul device  /dev/sdb
# La chiavetta non deve essere montata su nessuna directory
# ATTENZIONE! QUESTO CANCELLERA' TUTTO QUELLO CHE STA SU /dev/sdb !
sgdisk --zap-all /dev/sdb
sgdisk --new=1:0:0 --typecode=1:ef00 /dev/sdb
mkfs.vfat -F32 /dev/sdb1

# Monto la chiavetta sulla directory /mnt e
# ci copio il file ISO:
mount -t vfat /dev/sdb1 /mnt
7z x ubuntu-16.04.1-desktop-amd64.iso -o/mnt/ 

# Crea il bootloader a 32bit e lo scrive sulla chiavetta:
apt install git bison libopts25 libselinux1-dev autogen m4 autoconf help2man libopts25-dev 
apt install flex libfont-freetype-perl automake autotools-dev libfreetype6-dev texinfo
git clone git://git.savannah.gnu.org/grub.git
cd grub
./autogen.sh
./configure --with-platform=efi --target=i386 --program-prefix=''
make
cd grub-core
../grub-mkimage -d . -o bootia32.efi -O i386-efi -p /boot/grub \
  ntfs hfs appleldr boot cat efi_gop efi_uga elf fat hfsplus iso9660 linux keylayouts \
  memdisk minicmd part_apple ext2 extcmd xfs xnu part_bsd part_gpt search search_fs_file \
  chain btrfs loadbios loadenv lvm minix minix2 reiserfs memrw mmap msdospart scsi loopback \
  normal configfile gzio all_video efi_gop efi_uga gfxterm gettext echo boot chain eval
cp bootia32.efi /mnt/EFI/BOOT/
umount /mnt

Ricordandosi che è necessario premere il tasto “Option” in fase di boot, altrimenti non potremo scegliere il boot dalla partizione EFI della chiavetta.

Finalmente,  potremo installare una bella Ubuntu sul nostro Macbook, con tabella delle partizioni GPT e pronta per il boot da EFI.

Ma il lavoro, purtroppo, non è ancora finito.. Alla fine dell’installazione, ci accorgeremo che il boot da disco non funziona e che il solo modo per eseguire Ubuntu è affidarsi alla partizione di boot dalla nostra fidata chiavetta.

Qualcosa ancora non va. Finalmente, grazie ad un ottimo articolo ho trovato la spiegazione e la soluzione del mio problema: la partizione EFI deve essere formattata con il filesystem Mac HFS+ (e non Fat32) e devono essere presenti alcuni specifici files.

Nell’articolo appena menzionato, viene spiegato come installare Ubuntu su Mac recenti (dal 2012 in poi) che ovviamente non hanno i problemi che ho affrontato per creare il disco di installazione (sono sistemi full 64bit).

Supponendo che la partizione EFI sia la numero 1, riporto di seguito le istruzioni leggermente modificate per un EFI a 32 bit:

# Repository con utilities per Intel Mac (fan control, HFS bless, bootloader icon, etc..)
add-apt-repository ppa:detly/mactel-utils
apt update
apt install mactel-boot hfsprogs gdisk grub-efi-ia32

umount /dev/sda1 
# Modifica la partizione EFI /dev/sda1 
gdisk /dev/sda

Questi sono i comandi da eseguire con gdisk:

Command (? for help): d
Partition number (1-3): 1

Command (? for help): n
Partition number (1-128, default 1): 1
«accept defaults»

Current type is 'Linux filesystem'
Hex code or GUID (L to show codes, Enter = 8300): AF00
Changed type of partition to 'Apple HFS/HFS+'

Command (? for help): w

Formatta la partizione EFI e Aggiorna fstab:

mkfs.hfsplus /dev/sda1 -v Ubuntu
bash -c 'echo $(blkid -o export -s UUID /dev/sda1) /boot/efi auto defaults 0 0 >> /etc/fstab'
mount /boot/efi

Installa Grub:

mkdir -p "/boot/efi/EFI/$(lsb_release -ds)/"
bash -c 'echo "This file is required for booting" > "/boot/efi/EFI/$(lsb_release -ds)/mach_kernel"'
bash -c 'echo "This file is required for booting" > /boot/efi/mach_kernel'
grub-install --target i386-efi --boot-directory=/boot --efi-directory=/boot/efi --bootloader-id="$(lsb_release -ds)"
hfs-bless "/boot/efi/EFI/$(lsb_release -ds)/System/Library/CoreServices/boot.efi"

Kodi ed ubuntu – guida all’installazione

Ho appena pubblicato una pagina per la guida all’installazione del mediacenter Kodi su Ubuntu 16.04 LTS.

La guida è molto dettagliata, ma richiede un minimo di dimestichezza con Linux e la shell bash.
Prevede la configurazione con script Systemd per il boot automatico di Kodi e per sostituire Grub con Extlinux ed avere un dual boot con splash screen full HD.

Spiego come personalizzare il display LCD del mio case Antec Fusion Black ed usare il mediacenter con un telecomando Logitech Harmony usando ir-keytable al posto di Lirc.

Cercherò di tenerla aggiornata con le novità e i miglioramenti che si presenteranno in futuro.

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.

Ubuntu 14.10, Unity e driver NVidia: Flickering!

Un problema che Ubuntu con la sua interfaccia utente Unity si trascina da troppo tempo è lo “sfarfallio” dello schermo (o “flickering” per dirla in Inglese) usando i driver Nvidia più recenti.
Mi ero rassegnato ad usare la vecchia versione presente nel repository Ubuntu, scendendo a molti compromessi sulla gestione della grafica del mio portatile, ma poi ho scoperto questa semplice soluzione:

E’ sufficiente installare il pacchetto compizconfig-settings-manager.
Successivamente, eseguendo con il proprio utente, ccsm (da riga di comando oppure usando l’interfaccia grafica), scegliete la pagina workaround e quindi abilitate Force full screen redraws (buffer swap) on repaint”.

Non ho notato alcun problema di prestazioni, almeno con i driver NVidia proprietari 346.35 usciti pochi giorni fa.

Aggiornata pagina su Kodi e Ubuntu

Ho aggiornato la pagina che tratta la configurazione di Xbmc, per il passaggio ufficiale a Kodi.

Su Ubuntu 14.04 LTS si presenta un problema con lcdproc che non riconosce il display LCD Imon (o più esattamente, lo fa in modo irregolare).
Ho trovato un hack che risolve il problema tramite udev, fin quando non sarà aggiornato il modulo del kernel che gestisce IMON lcd.

USB – CEC Adapter

Se dovessi fare una rassegna dei gadget che ritengo più utili (e prima o poi la farò) l’adapter USB-CEC di Pulse-Height si piazzerebbe tra i primi.
Consente di pilotare il proprio mediacenter direttamente dal telecomando del televisore, ed è l’uovo di colombo! Tramite il cavo HDMI, i segnali inviati dal telecomando vengono re-interpretati e spediti al mediacenter. Oltre ai consueti tasti di navigazione, questo piccolo dispositivo permette l’accensione della tv dal mediacenter, oppure di spengere il tv insieme al mediacenter. E’ inutile dire che tutto ciò è perfettamente supportato e configurabile da XBMC (Kodi?). Non sono riuscito a trovare un rivenditore italiano, ma ho risolto ordinando direttamente dal loro sito e, nel giro di 10 giorni, mi è arrivato un pacchetto dall’Inghilterra.

Per configurazioni semplici, del tipo TV + mediacenter è l’ideale. Se si hanno esigenze più spinte (impianto home-cinema, PS3, etc..), è meglio rivolgersi a telecomandi universali, tipo il Logitech Harmony Smart Control, oppure il mio vecchio fidato Logitech Harmony 650 . Onestamente vorrei capire come mai sul mercato italiano non siano più disponibili questi articoli.. Probabilmente ritengono che l’italiano medio oltre al Meliconi non riesca ad andare?

Pulse Eight CEC

Pulse Eight CEC

Ubuntu 14.04 su Lenovo IdeaPad Flex 10

Non so voi, ma Windows non lo sopporto.
Sono riuscito a tollerare Windows XP e Windows 7 (per necessità), ma quando mi son trovato davanti Windows 8, ho avuto una reazione allergica tale che ho rimosso pure l’hard-disk ;-)

Ubuntu 14.04 su Lenovo Ideapad Flex 10

Ubuntu 14.04 su Lenovo Ideapad Flex 10

A parte gli scherzi (ma neanche tanto), dal mio Ideapad nuovo fiammante ho rimosso l’hard-disk originale da 320 GB, per un ben più prestante SSD Samsung 830 da 64gb (solo slim ).

Non esiste uno sportello o un modo agevole per rimuoverlo. Sul retro ci sono 7 viti visibili da rimuovere e due sotto i piedini di gomma (vedi manuale). Purtroppo questa operazione,  va a toccare la garanzia, quindi prestate attenzione se desiderate dare un po’ di sprint a questo giocattolino!

Adesso vengono le dolenti note.. Ubuntu 14.04 non si installa direttamente su questo portatile, pesantemente incatenato a Windows 8.x.
Ha un bios UEFI a 32 bit (ma la cpu Celeron N2805 è a 64 bit) e non riconosce dischi o dispositivi di boot che non siano partizionati con GPT. Grazie alle informazioni relative all’installazione su un dispositivo abbastanza simile, un Asus Transformer T100, ho potuto liberare questo portatile dal giogo Microsoft.
Queste sono le istruzioni riviste e corrette per installare Ubuntu 14.04 a 64bit su un IdeaPad Flex 10:

    1. Su un altro computer, lanciate GParted e partizionate una chiavetta usb (minimo 1GB) con GPT creando una sola partizione FAT32. Poi lanciare “Startup Disk Creator” usando l’ISO di Ubuntu 14.04 (64bit).
    2. Alla fine dell’installazione, montare la chiavetta, e copiare in /EFI/BOOT il bootloader a 32bit bootia32.efi proveniente dal sito di cui sopra e a cui vi rimando per spiegazioni o per compilarlo personalmente.
    3. Finalmente, dopo aver disabilitato “Secure Boot” dal bios del nostro Ideapad, potete eseguire il boot del portatile tramite la chiavetta usb. Scegliete “Try Ubuntu”. Appena avrete il desktop a disposizione, lanciate GParted e create una tabella delle partizioni GPT sul disco nel quale volete installare Ubuntu (nel mio caso un SSD).
    4. Eseguite di nuovo il boot da chiavetta, questa volta scegliendo “Install Ubuntu”. Nella creazione delle partizioni (Manual mode), ricordarsi di crearne una di tipo EFI di almeno 100Mb. Le altre secondo le  vostre preferenze. Per un uso “casalingo” possono bastare una di swap e un’altra per tutta la root in ext4.
    5. Alla fine dell’installazione di Ubuntu ancora non siamo in grado di effettuare il boot senza chiavetta visto che grub-efi a 32 bit non è installato per default (ma fortunatamente è tra i pacchetti a disposizione). Riavviare nuovamente. Al momento della comparsa del menu di Grub premere “c” e da riga di comando eseguire:
      linux (hd1, gpt3)/boot/vmlinuz-3.13.0-29-generic root=/dev/sda3
      initrd (hd1,gpt3)/boot/initrd.img-3.13.0-29-generic
      boot

      Questo esempio prevede che la partizione con il kernel e di root siano la terza (la prima è la partizione EFI e la seconda swap) e che hd0 sia la chiavetta usb. In ogni caso sfruttate sempre la tab-completion, vi potrà aiutare non poco.

    6. A questo punto appena avrete una console a disposizione eseguite:
      apt-get install grub-efi-ia32
      update-grub
      

Tutto qui.. In bocca al lupo!