Zanurkuj w Pythonie/Abstrakcyjne źródła wejścia

Abstrakcyjne źródła wejścia edytuj

Jedną z najważniejszych możliwości Pythona jest jego dynamiczne wiązanie, a jednym z najbardziej przydatnych przykładów wykorzystania tego jest obiekt plikopodobny (ang. file-like object).

Wiele funkcji, które wymagają jakiegoś źródła wejścia, mogłyby po prostu przyjmować jako argument nazwę pliku, następnie go otwierać, czytać, a na końcu go zamykać. Jednak tego nie robią. Zamiast działać w ten sposób, jako argument przyjmują obiekt pliku lub obiekt plikopodobny.

W najprostszym przypadku obiekt plikopodobny jest dowolnym obiektem z metodą read, która przyjmuje opcjonalny parametr wielkości, size, a następnie zwraca łańcuch znaków. Kiedy wywołujemy go bez parametru size, odczytuje wszystko, co jest do przeczytania ze źródła wejścia, a potem zwraca te wszystkie dane jako pojedynczy łańcuch znaków. Natomiast kiedy wywołamy metodę read z parametrem size, to odczyta ona tyle bajtów ze źródła wejścia, ile wynosi wartość size, a następnie zwróci te dane. Kiedy ponownie wywołamy tę metodę, zostanie odczytana i zwrócona dalsza porcja danych (czyli dane będą czytane od miejsca, w którym wcześniej skończono czytać).

Powyżej opisaliśmy, w jaki sposób działają prawdziwe pliki. Jednak nie musimy się ograniczać do prawdziwych plików. Źródłem wejścia może być wszystko: plik na dysku, strona internetowa, czy nawet jakiś łańcuch znaków. Dopóki przekazujemy do funkcji obiekt plikopodobny, a funkcja ta po prostu wywołuje metodę read, to funkcja może obsłużyć dowolny rodzaj wejścia, bez posiadania jakiegoś specjalnego kodu dla każdego rodzaju wejścia.

Może się zastanawiamy, co ma to wspólnego z przetwarzaniem XML-a? Otóż minidom.parse jest taką funkcją, do której możemy przekazać obiekt plikopodobny.

Przykład. Parsowanie XML-u z pliku
>>> from xml.dom import minidom
>>> fsock = open('binary.xml')    #(1)
>>> xmldoc = minidom.parse(fsock) #(2)
>>> fsock.close()                 #(3)
>>> print xmldoc.toxml()          #(4)
<?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. Najpierw otwieramy plik z dysku. Otrzymujemy przez to obiekt pliku.
  2. Przekazujemy obiekt pliku do funkcji minidom.parse, która wywołuje metodę read z fsock i czyta dokument XML z tego pliku.
  3. Koniecznie wywołujemy metodę close obiektu pliku, jak już skończyliśmy na nim pracę. minidom.parse nie zrobi tego za nas.
  4. Wywołując ze zwróconego dokumentu XML metodę toxml(), wypisujemy cały dokument.

Dobrze, to wszystko wygląda jak kolosalne marnotrawstwo czasu. W końcu już wcześniej widzieliśmy, że minidom.parse może przyjąć jako argument nazwę pliku i wykonać całą robotę z otwieraniem i zamykaniem automatycznie. Prawdą jest, że jeśli chcemy sparsować lokalny plik, możemy przekazać nazwę pliku do minidom.parse, a funkcja ta będzie umiała mądrze to wykorzystać. Lecz zauważmy jak podobne i łatwe jest także parsowanie dokumentu XML pochodzącego bezpośrednio z Internetu.

Przykład. Parsowanie XML-a z URL-a
>>> 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>
 
[...ciach...]
  1. Jak już zaobserwowaliśmy w poprzednim rozdziale, urlopen przyjmuje adres URL strony internetowej i zwraca obiekt plikopodobny. Ponadto, co jest bardzo ważne, obiekt ten posiada metodę read, która zwraca źródło danej strony internetowej.
  2. Teraz przekazujemy ten obiekt plikopodobny do minidom.parse, która posłusznie wywołuje metodę read i parsuje dane XML, które zostają zwrócone przez read. Fakt, że te dane przychodzą teraz bezpośrednio z Internetu, jest kompletnie nieistotny. minidom.parse nie ma o stronach internetowych żadnego pojęcia; on tylko wie coś o obiektach plikopodobnych.
  3. Jak tylko obiekt plikopodobny, który podarował nam urlopen, nie będzie potrzebny, koniecznie zamykamy go.
  4. Przy okazji, ten URL jest prawdziwy i on naprawdę jest dokumentem XML. Reprezentuje on aktualne nagłówki, techniczne newsy i plotki w Slashdocie.
Przykład. Parsowanie XML-a z łańcucha znaków (prosty sposób, ale mało elastyczny)
>>> 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 posiada metodę parseString, która przyjmuje cały dokument XML w postaci łańcucha znaków i parsuje go. Możemy ją wykorzystać zamiast minidom.parse, jeśli wiemy, że posiadamy cały dokument w formie łańcucha znaków.

OK, to możemy korzystać z funkcji minidom.parse zarówno do parsowania lokalnych plików jak i odległych URL-ów, ale do parsowania łańcuchów znaków wykorzystujemy... inną funkcję. Oznacza to, że jeśli chcielibyśmy, aby nasz program mógł dać wyjście z pliku, adresu URL lub łańcucha znaków, potrzebujemy specjalnej logiki, aby sprawdzić czy mamy do czynienia z łańcuchem znaków, a jeśli tak, to wywołać funkcję parseString zamiast parse. Jakie to niesatysfakcjonujące...

Gdyby tylko był sposób, aby zmienić łańcuch znaków na obiekt plikopodobny, to moglibyśmy po prostu przekazać ten obiekt do minidom.parse. I rzeczywiście, istnieje moduł specjalnie zaprojektowany do tego: StringIO.

Przykład. Wprowadzenie do 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. Moduł StringIO zawiera tylko jedną klasę, także nazwaną StringIO, która pozwala zamienić napis w obiekt plikopodobny. Klasa StringIO podczas tworzenia instancji przyjmuje jako parametr łańcuch znaków.
  2. Teraz już mamy obiekt plikopodobny i możemy robić wszystkie możliwe plikopodobne operacje. Na przykład read, która zwraca oryginalny łańcuch.
  3. Wywołując ponownie read otrzymamy pusty napis. W ten sposób działa prawdziwy obiekt pliku; kiedy już zostanie przeczytany cały plik, nie można czytać więcej bez wyraźnego przesunięcia do początku pliku. Obiekt StringIO pracuje w ten sam sposób.
  4. Możemy jawnie przesunąć się do początku napisu, podobnie jak możemy się przesunąć w pliku, wykorzystując metodę seek obiektu klasy StringIO.
  5. Możemy także czytać fragmentami łańcuch znaków, dzięki przekazaniu parametr wielkości size do metody read.
  6. Za każdym razem, kiedy wywołamy read, zostanie nam zwrócona pozostała część napisu, która nie została jeszcze przeczytana. W dokładnie ten sam sposób działa obiekt pliku.
Przykład. Parsowanie XML-a z łańcucha znaków (sposób z obiektem plikopodobnym)
>>> contents = "<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>"
>>> ssock = StringIO.StringIO(contents)
>>> xmldoc = minidom.parse(ssock) #(1)
>>> ssock.close()
>>> print xmldoc.toxml()
<?xml version="1.0" ?>
<grammar><ref id="bit"><p>0</p><p>1</p></ref></grammar>
  1. Teraz możemy przekazać obiekt plikopodobny (w rzeczywistości instancję StringIO) do funkcji minidom.parse, która z kolei wywoła metodę read z tego obiektu plikopodobnego i szczęśliwie wszystko przeparsuje, nie zdając sobie nawet sprawy, że wejście to pochodzi z łańcucha znaków.

To już wiemy, jak za pomocą pojedynczej funkcji, minidom.parse, sparsować dokument XML przechowywany na stronie internetowej, lokalnym pliku, czy w łańcuchu znaków. Dla strony internetowej wykorzystamy urlopen, aby dostać obiekt plikopodobny; dla lokalnego pliku, wykorzystamy open; a w przypadku łańcucha znaków skorzystamy z StringIO. Lecz teraz pójdźmy trochę do przodu i uogólnijmy też te różnice.

Przykład. openAnything
def openAnything(source):                  #(1)
    # próbuje otworzyć za pomocą urllib (jeśli source jest URL-em do http, ftp itp.)
    import urllib                         
    try:                                  
        return urllib.urlopen(source)      #(2)
    except (IOError, OSError):            
        pass                              
 
    # próbuje otworzyć za pomocą wbudowanej funkcji open (gdy source jest ścieżką do pliku)
    try:                                  
        return open(source)                #(3)
    except (IOError, OSError):            
        pass                              

    # traktuje source jako łańcuch znaków z danymi
    import StringIO                       
    return StringIO.StringIO(str(source))  #(4)
  1. Funkcja openAnything przyjmuje pojedynczy argument, source, i zwraca obiekt plikopodobny. source jest łańcuchem znaków o różnym charakterze. Może się odnosić do adresu URL (np. 'http://slashdot.org/slashdot.rdf'), może być globalną lub lokalną ścieżką do pliku (np. 'binary.xml'), czy też łańcuchem znaków przechowującym dokument XML, który ma zostać sparsowany.
  2. Najpierw sprawdzamy, czy source jest URL-em. Robimy to brutalnie: próbujemy otworzyć to jako URL i cicho pomijamy błędy spowodowane próbą otworzenia czegoś, co nie jest URL-em. Jest to właściwie eleganckie w tym sensie, że jeśli urllib będzie kiedyś obsługiwał nowe typy URL-i, nasz program także je obsłuży i to bez konieczności zmiany kodu. Jeśli urllib jest w stanie otworzyć source, to return spowoduje natychmiastowe opuszczenie funkcji, a kolejne instrukcje try nie zostaną nigdy wykonane.
  3. Jeśli jednak urllib nie był w stanie otworzyć source, stwierdzając że nie jest ono poprawnym URL-em, zakładamy że jest to ścieżka do pliku znajdującego się na dysku i próbujemy go otworzyć. Ponownie, nic nie robimy, by sprawdzić, czy source jest poprawną nazwą pliku (zasady określające poprawność nazwy pliku są znacząco różne na różnych platformach, dlatego prawdopodobnie i tak byśmy to źle zrobili). Zamiast tego, na ślepo otwieramy plik i cicho pomijamy wszystkie błędy.
  4. W tym miejscu zakładamy, że source jest łańcuchem znaków, który przechowuje dokument XML (ponieważ nic innego nie zadziałało), dlatego wykorzystujemy StringIO, aby utworzyć obiekt plikopodobny i zwracamy go. (Tak naprawdę, ponieważ wykorzystujemy funkcję str, source nie musi być nawet łańcuchem znaków; może być dowolnym obiektem, wykorzystana bowiem zostanie jego tekstowa reprezentacja, zdefiniowana przez specjalną metodę __str__.)

Teraz możemy wykorzystać funkcję openAnything w połączeniu z minidom.parse, aby utworzyć funkcję, która przyjmuje źródło source, które w jakiś sposób odwołuje się do dokumentu XML (może to robić za pomocą adresu URL, lokalnego pliku, czy też dokumentu przechowywanego jako łańcuch znaków), i parsuje je.

Przykład. Wykorzystanie openAnything w kgp.py
class KantGenerator:
    def _load(self, source):
        sock = toolbox.openAnything(source)
        xmldoc = minidom.parse(sock).documentElement
        sock.close()
        return xmldoc