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.
>>> 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>
- Najpierw otwieramy plik z dysku. Otrzymujemy przez to obiekt pliku.
- Przekazujemy obiekt pliku do funkcji
minidom.parse, która wywołuje metodęreadzfsocki czyta dokument XML z tego pliku. - Koniecznie wywołujemy metodę
closeobiektu pliku, jak już skończyliśmy na nim pracę.minidom.parsenie zrobi tego za nas. - 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.
>>> 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...]
- Jak już zaobserwowaliśmy w poprzednim rozdziale,
urlopenprzyjmuje 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. - Teraz przekazujemy ten obiekt plikopodobny do
minidom.parse, która posłusznie wywołuje metodęreadi parsuje dane XML, które zostają zwrócone przezread. Fakt, że te dane przychodzą teraz bezpośrednio z Internetu, jest kompletnie nieistotny.minidom.parsenie ma o stronach internetowych żadnego pojęcia; on tylko wie coś o obiektach plikopodobnych. - Jak tylko obiekt plikopodobny, który podarował nam
urlopen, nie będzie potrzebny, koniecznie zamykamy go. - Przy okazji, ten URL jest prawdziwy i on naprawdę jest dokumentem XML. Reprezentuje on aktualne nagłówki, techniczne newsy i plotki w Slashdocie.
>>> 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>
minidomposiada metodęparseString, która przyjmuje cały dokument XML w postaci łańcucha znaków i parsuje go. Możemy ją wykorzystać zamiastminidom.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.
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)
- Moduł
StringIOzawiera tylko jedną klasę, także nazwanąStringIO, która pozwala zamienić napis w obiekt plikopodobny. KlasaStringIOpodczas tworzenia instancji przyjmuje jako parametr łańcuch znaków. - Teraz już mamy obiekt plikopodobny i możemy robić wszystkie możliwe plikopodobne operacje. Na przykład
read, która zwraca oryginalny łańcuch. - Wywołując ponownie
readotrzymamy 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. ObiektStringIOpracuje w ten sam sposób. - Możemy jawnie przesunąć się do początku napisu, podobnie jak możemy się przesunąć w pliku, wykorzystując metodę
seekobiektu klasyStringIO. - Możemy także czytać fragmentami łańcuch znaków, dzięki przekazaniu parametr wielkości
sizedo metodyread. - 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.
>>> 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>
- Teraz możemy przekazać obiekt plikopodobny (w rzeczywistości instancję
StringIO) do funkcjiminidom.parse, która z kolei wywoła metodęreadz 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.
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)
- Funkcja
openAnythingprzyjmuje pojedynczy argument,source, i zwraca obiekt plikopodobny.sourcejest ł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. - Najpierw sprawdzamy, czy
sourcejest 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śliurllibbędzie kiedyś obsługiwał nowe typy URL-i, nasz program także je obsłuży i to bez konieczności zmiany kodu. Jeśliurllibjest w stanie otworzyćsource, toreturnspowoduje natychmiastowe opuszczenie funkcji, a kolejne instrukcjetrynie zostaną nigdy wykonane. - Jeśli jednak
urllibnie 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ć, czysourcejest 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. - W tym miejscu zakładamy, że
sourcejest łańcuchem znaków, który przechowuje dokument XML (ponieważ nic innego nie zadziałało), dlatego wykorzystujemyStringIO, aby utworzyć obiekt plikopodobny i zwracamy go. (Tak naprawdę, ponieważ wykorzystujemy funkcjęstr,sourcenie 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.
openAnything w kgp.py
class KantGenerator:
def _load(self, source):
sock = toolbox.openAnything(source)
xmldoc = minidom.parse(sock).documentElement
sock.close()
return xmldoc