Sviluppiamo un feed reader con python (Parte 4)

A grande richiesta ;-) prosegue la serie di articoli riguardanti la costruzione di un feed reader per appunti digitali in python.

Nella scorsa puntata abbiamo visto come sfruttare il meccanismo di autoconnect per la gestione degli eventi con le PyQt. Abbiamo visto come caricare un url all’interno di una webview.

Il risultato, come ha potuto vedere chi si è cimentato nell’implementazione, non è dei migliori: nella webview viene caricata l’intera pagina mentre sarebbe auspicabile mostrare soltanto il contenuto dell’articolo.

Per farlo mi sono appoggiato a 2 librerie:

  • urllib2 per lo scaricamento della pagina HTML
  • lxml per il parsing del contenuto

La prima, essendo inclusa nella python standard library, è disponibile senza bisogno di installazione. La seconda va installata separatamente.

Chi utilizza una distribuzione GNU/Linux debian based (come Ubuntu) deve semplicemente installare il pacchetto python-lxml.

Come al solito rimando alle istruzioni del sito ufficiale per l’installazione su Windows e Mac OS X.

Per mantenere un minimo ordinato il codice ho deciso di implementare la classe in un nuovo file che ho chiamato con estrema fantasia articleparser.py.

import urllib2
import lxml.html
from lxml.html.clean import clean_html

class ArticleParser(object):
    """ Appunti Digitali article parser class

    Attributes:
        opener: urllib2 url opener
    """
    def __init__(self):
        """ Setup url opener and set friendly user agent """
        self.opener = urllib2.build_opener()
        self.opener.addheaders = [('User-agent', 'Mozilla/5.0')]

    def __save_article_to_file(self, content):
        """ Save html string to file """
        out = open("article.txt", 'w')
        out.write(content)
        out.close()

    def get_article(self, url):
        """ Download, parse, filter an return post from a given URL

        Args:
            url: Article webpage url as string
        Returns:
            A string with parsed and cleaned Article's content
        """
        response = self.opener.open(url)
        doc = lxml.html.document_fromstring(response.read())
        content = doc.find_class("post")[0]    # Select content by CSS class
        cleaned_content = clean_html(content)
        str_cleaned_content = lxml.html.tostring(cleaned_content)
        #self.__save_article_to_file(str_cleaned_content)
        return str_cleaned_content

Come al solito in alto abbiamo l’importazione dei vari moduli che andremo ad utilizzare.

Andiamo ad analizzare il costruttore della nostra classe:

def __init__(self):
    """ Setup url opener and set friendly user agent """
    self.opener = urllib2.build_opener()
    self.opener.addheaders = [('User-agent', 'Mozilla/5.0')]

Quello che faremo sarà creare un oggetto per inviare richieste http “mascherate” per far credere al server che provengano da un browser web. Il mascheramento si ottiene impostando uno User Agent di un browser noto (in questo caso ho utilizzato quello di un generico firefox). Questo passo è necessario poiché il server su cui gira appunti digitali è configurato per servire pagine esclusivamente a browser web. Almeno questa è la spiegazione che mi sono dato.

Subito dopo potete vedere il metodo _save_article_to_file:

def __save_article_to_file(self, content):
    """ Save html string to file """
    out = open("article.txt", 'w')
    out.write(content)
    out.close()

In realtà questo pezzo di codice non serve a niente.Ho utilizzato questa funzione solo per fare alcune prove durante la stesura del codice. Ho deciso però di lasciarlo perché mi da la possibilità di evidenziare due cose interessanti.

La prima come potete vedere è l’estrema semplicità nella gestione della scrittura su file. La seconda riguarda una caratteristica del linguaggio python: come avrete notato il nome del metodo inizia con due underscore.

Questa convenzione serve per definire metodi privati (accessibili solo all’interno della classe). In realtà python non rende realmente inaccessibile il metodo ma lo rinomina in _ArticleParser__save_article_to_file. Questo poiché in python non esistono metodi o attributi privati. La linea di pensiero pythonica può essere facilmente riassunta con questa fantastica frase:

“We’re all consenting adults here”

E visto che siamo adulti consenzienti sappiamo che chi ha scritto quella classe vuole che quel metodo non venga chiamato al di fuori di essa. Se siete sviluppatori java probabilmente a questo punto avrete spento il PC e sarete scappati di fronte a questa “feature”. :P

Tralasciamo comunque queste due osservazioni per passare al metodo centrale della nostra classe:

def get_article(self, url):
    """ Download, parse, filter an return post from a given URL

    Args:
        url: Article webpage url as string
    Returns:
        A string with parsed and cleaned Article's content
    """
    response = self.opener.open(url)
    doc = lxml.html.document_fromstring(response.read())
    content = doc.find_class("post")[0]    # Select content by CSS class
    cleaned_content = clean_html(content)
    str_cleaned_content = lxml.html.tostring(cleaned_content)
    #self.__save_article_to_file(str_cleaned_content)
    return str_cleaned_content

Questo metodo si occuperà, dato come argomento un determinato url, di:

  • scaricare la pagina html con l’opener istanziato nel costruttore
  • convertire la stringa in un documento di tipo element tree
  • utilizzare il metodo find_class per estrarre il div del documento che ha class=”post” (attributo che ho individuato osservando il sorgente html di alcuni post)
  • ripulire il contenuto del div estratto per togliere riferimenti a javascript vari
  • riconvertire e restituire un stringa contente il codice HTML da renderizzare

A questo punto la nostra classe è completa e non dobbiamo fare altro che utilizzarla.

Basterà quindi rimettere mano al file principale (adreader.py), importare il codice appena scritto:

from articleparser import *

Istanziare un oggetto ArticleParser all’interno del costruttore della classe Main (riga 9):

def __init__(self):
	    QMainWindow.__init__(self, parent=None)
	    # Setup pyuic generated code
	    self.ui = Ui_MainWindow()
	    self.ui.setupUi(self)
	    #Parse RSS feed
	    ad = feedparser.parse("https://www.appuntidigitali.it/feed")
	    #Create article parser
	    self.ap = ArticleParser()
	    #Populate TreeWiget with feed elements
	    for entry in ad.entries:
	        item = QTreeWidgetItem([
	            entry.title,
	            entry.author,
	            entry.slash_comments,
	            entry.link
	            ])
	        self.ui.treeWidget.addTopLevelItem(item)

E modificare il pezzo di codice che verrà eseguito al doppio click su un item:

def on_treeWidget_itemDoubleClicked(self):
    """ Load article's content in the webview on doubleclick
    """
    url = str(self.ui.treeWidget.currentItem().text(3))
    self.ui.webView.setHtml(self.ap.get_article(url))

Il nostro feed reader, sebbene estremamente lontano da essere considerato accettabile, adesso svolge abbastanza bene il suo lavoro.
Con questa puntata concludo la serie di articoli sperando che sia stato un aperitivo gradito per invogliarvi a scoprire le meraviglie del linguaggio python.

Segnalo inoltre che ho caricato il codice funzionante su un repository su google code in modo da renderlo disponibile per chi volesse modificarlo.

http://code.google.com/p/adreader/

Per avere il codice aggiornato all’ultima revisione sulla vostra macchina vi basterà installare mercurial e clonare in locale i sorgenti con il comando:

hg clone https://adreader.googlecode.com/hg/ adreader

Press ESC to close