Zanurkuj w Pythonie/Abstrakcyjne źródła wejścia
Abstrakcyjne źródła wejścia
edytujJedną 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ęread
zfsock
i czyta dokument XML z tego pliku. - Koniecznie wywołujemy metodę
close
obiektu pliku, jak już skończyliśmy na nim pracę.minidom.parse
nie 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,
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. - 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 przezread
. 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. - 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>
minidom
posiada 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ł
StringIO
zawiera tylko jedną klasę, także nazwanąStringIO
, która pozwala zamienić napis w obiekt plikopodobny. KlasaStringIO
podczas 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
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. ObiektStringIO
pracuje 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ę
seek
obiektu klasyStringIO
. - Możemy także czytać fragmentami łańcuch znaków, dzięki przekazaniu parametr wielkości
size
do 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ę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.
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
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. - 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śliurllib
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śliurllib
jest w stanie otworzyćsource
, toreturn
spowoduje natychmiastowe opuszczenie funkcji, a kolejne instrukcjetry
nie zostaną nigdy wykonane. - 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ć, czysource
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. - 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 wykorzystujemyStringIO
, 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.
openAnything
w kgp.py
class KantGenerator:
def _load(self, source):
sock = toolbox.openAnything(source)
xmldoc = minidom.parse(sock).documentElement
sock.close()
return xmldoc