5.4. Introdurre BaseHTMLProcessor.py

SGMLParser non produce nulla da solo. Analizza, analizza e analizza, e chiama un metodo per ogni cosa interessante che trova, ma il metodo non fa nulla. SGMLParser è un consuma HTML: prende il codice HTML e lo divide in piccoli pezzi strutturati. Come avete visto nella sezione precedente, potete creare una sottoclasse di SGMLParser per definire classi che catturano tag specifici e producono cose utili, come la lista dei collegamenti delle pagine web. Adesso facciamo un ulteriore passo avanti, per definire la classe che cattura ogni cosa che SGMLParser rilascia, e quindi ricostruire l'intero documento HTML. In termini tecnici, questa classe sarà un produttore di codice HTML.

BaseHTMLProcessor crea una sottoclasse SGMLParser e fornisce gli 8 metodi handler essenziali: unknown_starttag, unknown_endtag, handle_charref, handle_entityref, handle_comment, handle_pi, handle_decl, e handle_data.

Esempio 5.8. Introdurre BaseHTMLProcessor


class BaseHTMLProcessor(SGMLParser):
    def reset(self):                        1
        self.pieces = []
        SGMLParser.reset(self)

    def unknown_starttag(self, tag, attrs): 2
        strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs])
        self.pieces.append("<%(tag)s%(strattrs)s>" % locals())

    def unknown_endtag(self, tag):          3
        self.pieces.append("</%(tag)s>" % locals())

    def handle_charref(self, ref):          4
        self.pieces.append("&#%(ref)s;" % locals())

    def handle_entityref(self, ref):        5
        self.pieces.append("&%(ref)s" % locals())
        if htmlentitydefs.entitydefs.has_key(ref):
            self.pieces.append(";")

    def handle_data(self, text):            6
        self.pieces.append(text)

    def handle_comment(self, text):         7
        self.pieces.append("<!--%(text)s-->" % locals())

    def handle_pi(self, text):              8
        self.pieces.append("<?%(text)s>" % locals())

    def handle_decl(self, text):
        self.pieces.append("<!%(text)s>" % locals())
1 reset, chiamata da SGMLParser.__init__, inizializza self.pieces come una lista vuota prima di chiamare il metodo padre. self.pieces è un attributo data che conterrà i pezzi del documento HTML che stiamo costruendo. Ogni metodo handler ricostruirà il codice HTML analizzato da SGMLParser, ed ogni metodo aggiungerà quella stringa a self.pieces. Notate che self.pieces è una lista. Potreste essere tentati di definirla come una stringa e semplicemente aggiungere ogni pezzo alla fine. Funzionerebbe, ma Python è molto più efficiente quando ha a che fare con le liste. [8]
2 Poiché BaseHTMLProcessorBaseHTMLProcessor non definisce alcun metodo per particolari tag (come il metodo start_a in URLLister), SGMLParser chiamerà unknown_starttag per ogni inizio di tag. Questo metodo prende il tag (tag) e la lista degli attributi con le coppie nome/valore (attrs), ricostruisce il codice HTML originale e lo aggiunge a self.pieces. La formattazione della stringa qui è un po' strana; la spiegheremo nella prossima sezione.
3 Ricostruire la fine dei tag è molto più semplice; prendete semplicemente il nome del tag ed inseritelo fra le parentesi </...>.
4 Quando SGMLParser trova un riferimento ad un carattere, chiama handle_charref con il puro riferimento. Se il documento HTML contiene il riferimento &#160;, ref varrà 160. Per ricostruire il riferimento completo al carattere è sufficiente inserire ref tra i caratteri &#...;.
5 I riferimenti alle entità sono simili a quelli ai caratteri, ma senza il carattere # (hash ndt). Per ricostruire il riferimento all'entità originale bisogna inserire ref fra i caratteri &...;. (Effettivamente, come mi ha fatto notare un erudito lettore, è un po' piu' complicato di così. Solo alcune entità HTML standard terminano con una semicolonna; altre entità somiglianti non lo fanno. Per nostra fortuna, il set di entità HTML standard è definito in un dizionario di un modulo Python chiamato htmlentitydefs. Da qui l'istruzione if.)
6 I blocchi di testo sono semplicemente aggiunti inalterati in fondo a self.pieces.
7 I commenti HTML sono inseriti fra i caratteri <!--...-->.
8 Le istruzioni da trattare sono inserite fra i caratteri <?...>.
Importante
La specifica HTML richiede che tutto ciò che non è HTML (come JavaScript lato client) debba essere racchiuso tra commenti HTML, ma non in tutte le pagine ciò è stato fatto (e tutti i moderni browsers chiudono un occhio in merito). BaseHTMLProcessor non perdona; se uno script è inserito in modo improprio, verrà analizzato come se fosse codice HTML. Per esempio, se lo script contiene i simboli di minore e maggiore, SGMLParser potrebbe pensare, sbagliando, di aver trovato tag e attributi. SGMLParser converte sempre i tag e i nomi degli attributi in lettere minuscole, il che potrebbe bloccare lo script, e BaseHTMLProcessor racchiude sempre i valori tra doppi apici (anche se originariamente il documento HTML usava apici singoli o niente del tutto), cosa che di sicuro interromperebbe lo script. Proteggete sempre i vostri script lato client inserendoli dentro commenti HTML.

Esempio 5.9. BaseHTMLProcessor output

    def output(self):               1
        """Return processed HTML as a single string"""
        return "".join(self.pieces) 2
1 Questo è il metodo in BaseHTMLProcessor che non viene mai chiamato dal padre SGMLParser. Mentre gli altri metodi handler salvano il loro codice HTML ricostruito in self.pieces, questa funzione serve a unire tutti questi pezzi in una stringa. Come è stato fatto notare prima, Python lavora benissimo con le liste, ma meno bene con le stringhe, e quindi creiamo la stringa completa solo quando qualcuno ce lo chiede esplicitamente.
2 Se preferite, potete utilizzare il metodo join del modulo string: string.join(self.pieces, "").

Ulteriori letture

Footnotes

[8] La ragione per cui Python è più efficiente con le liste che con le stringhe è che le liste sono oggetti modificabili, mentre le stringhe no. Questo significa che eseguire un 'append' ad una lista significa semplicemente aggiungere l'elemento ed aggiornare l'indice. Dato che invece le stringhe non possono essere cambiate, occorre usare codice come s = s + newpiece, che crea dapprima una nuova stringa dalla concatenazione della vecchia e del nuovo pezzo, per poi buttar via la vecchia. Questo richiede un sacco di costosa gestione della memoria, e la quantità di lavoro necessario aumenta con il crescere della dimensione della stringa, per cui fare s = s + newpiece in un ciclo è micidiale. In termini tecnici, aggiungere n elementi ad una lista è un algoritmo di classe O(n), mentre aggiungere n elementi ad una stringa è un algoritmo di classe O(n2).