6.7. Astrarre le sorgenti di ingresso

Uno dei punti di forza di Python è il suo binding dinamico, ed uno degli usi più potenti del binding dinamico sono gli oggetti file (ovvero, che si comportano come dei file).

Molte funzioni che richiedono una sorgente di ingresso potrebbero semplicemente prendere un nome di un file, aprirle il file in lettura, leggerlo e chiuderlo quando hanno finito. Ma non lo fanno. Prendono, invece, degli oggetti file.

Nel caso più semplice, un oggetto file è un oggetto dotato di un metodo read con un parametro opzionale size, che ritorna una stringa. Quando viene chiamato senza il parametro size, legge tutto ciò che c'è da leggere dalla sorgente di ingresso e ritorna tutti i dati come una sola stringa. Quando viene chiamato con il parametro size, legge quella quantità dalla sorgente di ingresso e ritorna quella quantità di dati; quando viene chiamato nuovamente, riprende da dove aveva lasciato e ritorna il prossimo spezzone di dati.

È così che funziona la lettura dai veri file; la differenza è che non ci stiamo limitando ai veri file. La sorgente di ingresso potrebbe essere qualunque cosa: un file su disco, una pagina web, anche una stringa hard-coded. Fino a quando passiamo un oggetto file-like alla funzione e la funzione chiama semplicemente il metodo read dell'oggetto, la funzione può gestire ogni genere di sorgente di ingresso senza la necessità di codice specifico per ogni tipo.

Nel caso vi stiate chiedendo cosa ha a che vedere con l'analisi dell'XML, minidom.parse è una di quelle funzioni che può prendere un oggetto file.

Esempio 6.25. Analizzare XML da file

>>> from xml.dom import minidom
>>> fsock = open('binary.xml')    1
>>> xmldoc = minidom.parse(fsock) 2
>>> fsock.close()                 3
>>> print xmldoc
<?xml version="1.0" ?>
<grammar>
<ref id="bit">
  <p>0</p>
  <p>1</p>
</ref>
<ref id="byte">
  <p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>
</ref>
</grammar>
1 Per prima cosa, apriamo il file su disco. Questo ci restituisce un oggetto di tipo file.
2 Passiamo l'oggetto file a minidom.parse, che chiama il metodo read di fsock e legge il documento XML dal file su disco.
3 Siate sicuri di chiamare il metodo close sull'oggetto file quando avete finito. minidom.parse non lo farà per voi.

Bene, tutto questo sembra una colossale perdita di tempo. Dopo tutto, abbiamo già visto che minidom.parse può semplicemente prendere il nome del file ed effettuare tutte le operazioni di apertura e chiusura autonomamente. Ed è vero che se sapete di dover analizzare un file locale, potete passargli il nome del file e minidom.parse è sufficientemente intelligente da Fare La Cosa Giusta ™. Ma notate quanto sia simile, e facile, analizzare un documento XML direttamente da Internet.

Esempio 6.26. Analizzare XML da una URL

>>> import urllib
>>> usock = urllib.urlopen('http://slashdot.org/slashdot.rdf') 1
>>> xmldoc = minidom.parse(usock)                              2
>>> usock.close()                                              3
>>> print xmldoc.toxml()                                       4
<?xml version="1.0" ?>
<rdf:RDF xmlns="http://my.netscape.com/rdf/simple/0.9/"
 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">

<channel>
<title>Slashdot</title>
<link>http://slashdot.org/</link>
<description>News for nerds, stuff that matters</description>
</channel>

<image>
<title>Slashdot</title>
<url>http://images.slashdot.org/topics/topicslashdot.gif</url>
<link>http://slashdot.org/</link>
</image>

<item>
<title>To HDTV or Not to HDTV?</title>
<link>http://slashdot.org/article.pl?sid=01/12/28/0421241</link>
</item>

[...snip...]
1 Come abbiamo visto nel capitolo precedente, urlopen prende una pagina web URL e restituisce un oggetto file. Più importante, questo oggetto ha un metodo read che restituisce il sorgente HTML della pagina web.
2 Ora passiamo l'oggetto file a minidom.parse, che obbedientemente chiama il metodo read dell'oggetto ed analizza i dati XML che il metodo read restituisce. Il fatto che questi dati XML stiano arrivando da una pagina web è completamente irrilevante. minidom.parse non sa niente di pagine web e non gli importa nulla delle pagine web; sa solo come usare gli oggetti file.
3 Non appena avete finito, siate certi di chiudere l'oggetto file-like che urlopen restituisce.
4 A proposito, questa URL è reale e contiene davvero XML. È una rappresentazione XML delle attuali intestazioni su Slashdot, un sito di tecnica e pettegolezzi.

Esempio 6.27. Analizzare XML da una stringa (il metodo facile ma inflessibile)

>>> contents = "<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>"
>>> xmldoc = minidom.parseString(contents) 1
>>> print xmldoc.toxml()
<?xml version="1.0" ?>
<grammar><ref id="bit"><p>0</p><p>1</p></ref></grammar>
1 minidom ha un metodo, parseString, che prende un intero documento XML come stringa e lo analizza. Potete usare questo metodo al posto di minidom.parse se sapete già di avere il vostro documento XML tutto in una stringa.

Ok, allora possiamo usare la funzione minidom.parse per analizzare sia file locali che URL remote, ma per analizzare stringhe, usiamo ... un'altra funzione. Significa che se vogliamo essere in grado di leggere un file, una URL o una stringa, abbiamo bisogno di una logica speciale che controlli se è una stringa, e chiamare la funzione parseString al suo posto. Davvero insoddisfacente.

Se ci fosse un modo per trasformare una stringa in un oggetto file-like, allora potremmo semplicemente passare tale oggetto a minidom.parse. Ed infatti, c'è un modulo specificamente disegnato per svolgere questo compito: StringIO.

Esempio 6.28. Introduzione a StringIO

>>> contents = "<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>"
>>> import StringIO
>>> ssock = StringIO.StringIO(contents)   1
>>> ssock.read()                          2
"<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>"
>>> ssock.read()                          3
''
>>> ssock.seek(0)                         4
>>> ssock.read(15)                        5
'<grammar><ref i'
>>> ssock.read(15)
"d='bit'><p>0</p"
>>> ssock.read()
'><p>1</p></ref></grammar>'
>>> ssock.close()                         6
1 Il modulo StringIO contiene una sola classe, anch'essa chiamata StringIO, che vi permette di trasformare una stringa in un oggetto file-like. La classe StringIO prende una stringa come parametro quando crea un'istanza.
2 Ora abbiamo un oggetto file-like e possiamo fare ogni genere di cose, come con i file, con esso. Come invocare read, che ritorna la stringa originale.
3 Chiamando nuovamente read restituisce una stringa vuota. È così che i veri oggetti di tipo file funzionano; una volta letto l'intero file, non potete leggere di più senza spostarvi esplicitamente all'inizio del file. L'oggetto StringIO funziona nello stesso modo.
4 Potete esplicitamente spostarvi all'inizio della stringa, proprio come fareste con un file, utilizzando il metodo seek dell'oggetto StringIO.
5 Potete anche leggere la stringa in spezzoni, passando un parametro size al metodo read.
6 In qualunque momento, read ritornerà il resto della stringa che non avete ancora letto. Tutto questo rispecchia il modo di funzionamento dei file; da cui il termine oggetti file-like.

Esempio 6.29. Analizzare XML da una stringa (alla maniera degli oggetti file)

>>> contents = "<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>"
>>> ssock = StringIO.StringIO(contents)
>>> xmldoc = minidom.parse(ssock) 1
>>> print xmldoc.toxml()
<?xml version="1.0" ?>
<grammar><ref id="bit"><p>0</p><p>1</p></ref></grammar>
1 Ora possiamo passare l'oggetto file (in realtà una StringIO) a minidom.parse, che chiamerà il metodo read dell'oggetto e lo analizzerà felicemente, senza mai sapere che il suo input arrivava da una stringa hard-coded.

Così ora sappiamo come usare una singola funzione, minidom.parse, per analizzare un documento XML memorizzato in una pagina web, in un file locale o in una stringa hard-coded. Per una pagina web, usiamo urlopen per ottenere un oggetto file; per un file locale, usiamo open; per una stringa, usiamo StringIO. Proseguiamo ora di un ulteriore passo per generalizzare meglio questa differenza.

Esempio 6.30. openAnything


def openAnything(source):                  1
    # try to open with urllib (if source is http, ftp, or file URL)
    import urllib                         
    try:                                  
        return urllib.urlopen(source)      2
    except (IOError, OSError):            
        pass                              

    # try to open with native open function (if source is pathname)
    try:                                  
        return open(source)                3
    except (IOError, OSError):            
        pass                              

    # treat source as string
    import StringIO                       
    return StringIO.StringIO(str(source))  4
1 La funzione openAnything prende un solo parametro, source, e ritorna un oggetto file. source è una stringa di qualche tipo; può essere una URL (come 'http://slashdot.org/slashdot.rdf'), un percorso completo o parziale verso un file locale (come 'binary.xml'), o una stringa che contiene i dati XML da analizzare.
2 Per prima cosa, vediamo se source è una URL. Lo facciamo usando la forza bruta: proviamo ad aprirla come una URL e silenziosamente ignoriamo gli errori causati cercando di aprire qualcosa che non è una URL. È elegante nel senso che, se urllib mai supporterà nuovi tipi di URL in futuro, li supporteremo anche noi senza problemi.
3 Se urllib ci sgrida sostenendo che source non è una URL valida, assumiamo che sia il percorso di un file su disco e proviamo ad aprirlo. Ancora, non facciamo nulla per controllare se source è un nome di file valido o meno (le regole sulla validità del nome di un file variano molto tra diverse piattaforme, dunque le sbaglieremmo comunque). Invece, proviamo ciecamente ad aprire il file e silenziosamente catturiamo gli errori.
4 A questo punto, dobbiamo assumere che source sia una stringa con dei dati precodificati all'interno (visto che nient'altro ha funzionato), allora usiamo StringIO per creare un oggetto file da essa e lo ritorniamo. (Infatti, siccome stiamo usando la funzione str, source non deve necessariamente essere una stringa; potrebbe essere qualunque oggetto e noi useremo la sua rappresentazione sotto forma di stringa, come definita dal metodo speciale __str__.)

Ora possiamo usare la funzione openAnything in congiunzione con minidom.parse per creare una funzione, che prende un parametro source facente riferimento in qualche modo ad un documento XML (sia una URL, od il nome di un file locale o una stringa contenente un documento XML) e lo analizza.

Esempio 6.31. Usare openAnything in kgp.py


class KantGenerator:
    def _load(self, source):
        sock = toolbox.openAnything(source)
        xmldoc = minidom.parse(sock).documentElement
        sock.close()
        return xmldoc