Zanurkuj w Pythonie/Przetwarzanie XML-a
Nurkujemy
edytujKolejne dwa rozdziały są na temat przetwarzania XML-a w Pythonie. Będzie to przydatne, jeśli już wiesz, jak wyglądają dokumenty XML, a które są wykonane ze strukturalnych znaczników określających hierarchię elementów itp. Jeśli nic z tego nie rozumiesz, możesz przeczytać coś na ten temat na Wikipedii.
Nawet jeśli nie interesuje Ciebie temat XML-a i tak dobrze by było przeczytać te rozdziały, ponieważ omawiają one wiele ważnych tematów jak pakiety, argumenty linii poleceń, a także jak wykorzystywać getattr
jako pośrednik metod.
Bycie magistrem filozofii nie jest wymagane, chociaż jeśli kiedyś spotkaliśmy się z tekstami napisanymi przez Immanuel Kanta, lepiej zrozumiemy przykładowy program.
Mamy dwa sposoby pracy z XML-em. Jeden jest nazywany SAX (Simple API for XML), który działa w ten sposób, że czyta przez chwilę dokument XML i wywołuje dla każdego odnalezionego elementu odpowiednie metody. (Jeśli przeczytaliśmy rozdział 8, powinno to wyglądać znajomo, ponieważ w taki sposób pracuje moduł sgmllib
.) Inny jest nazywany DOM (Document Object Model), a pracuje w ten sposób, że jednorazowo czyta cały dokument XML i tworzy wewnętrzną reprezentację, wykorzystując klasy Pythona powiązane w strukturę drzewa. Python posiada standardowe moduły do obydwu sposobów parsowania, ale rozdział ten opisze tylko, jak wykorzystywać DOM.
Poniżej znajduje się kompletny program Pythona, który generuje pseudolosowe wyjście oparte na gramatyce bezkontekstowej zdefiniowanej w formacie XML. Nie przejmujmy się, jeśli nie zrozumieliśmy, co to znaczy. Będziemy głębiej badać zarówno wejście programu, jak i jego wyjście w tym i następnym rozdziale.
u"""Generator Kanta dla Pythona
Generuje pseudofilozofię opartą na gramatyce bezkontekstowej
Użycie: python kgp.py [options] [source]
Opcje:
-g ..., --grammar=... używa określonego pliku gramatyki lub adres URL
-h, --help wyświetla ten komunikat pomocy
-d wyświetla informacje debugowania podczas parsowania
Przykłady:
kgp.py generuje kilka akapitów z filozofią Kanta
kgp.py -g husserl.xml generuje kilka akapitów z filozofią Husserla
kpg.py "<xref id='paragraph'/>" generuje akapit Kanta
kgp.py template.xml czyta template.xml, aby określić, co ma generować
"""
from xml.dom import minidom
import random
import toolbox
import sys
import getopt
_debug = 0
class NoSourceError(Exception): pass
class KantGenerator(object):
u"""generuje pseudofilozofię opartą na gramatyce bezkontekstowej"""
def __init__(self, grammar, source=None):
self.loadGrammar(grammar)
self.loadSource(source and source or self.getDefaultSource())
self.refresh()
def _load(self, source):
u"""wczytuje XML-owe źródło wejścia, zwraca sparsowany dokument XML
- adres URL z plikiem XML ("http://diveintopython.org/kant.xml")
- nazwę lokalnego pliku XML ("~/diveintopython/common/py/kant.xml")
- standardowe wejście ("-")
- bieżący dokument XML w postaci łańcucha znaków
"""
sock = toolbox.openAnything(source)
xmldoc = minidom.parse(sock).documentElement
sock.close()
return xmldoc
def loadGrammar(self, grammar):
u"""wczytuje gramatykę bezkontekstową"""
self.grammar = self._load(grammar)
self.refs = {}
for ref in self.grammar.getElementsByTagName("ref"):
self.refs[ref.attributes["id"].value] = ref
def loadSource(self, source):
u"""wczytuje źródło source"""
self.source = self._load(source)
def getDefaultSource(self):
u"""zgaduje domyślne źródło bieżącej gramatyki
Domyślnym źródłem będzie jeden z <ref>-ów, do którego nic się
nie odwołuje. Może brzmi to skomplikowanie, ale tak naprawdę nie jest.
Przykład: Domyślnym źródłem dla kant.xml jest
"<ref id='section'/>", ponieważ 'section' jest jednym <ref>-em, który
nie jest nigdzie <xref>-em w gramatyce.
W wielu gramatykach, domyślne źródło będzie tworzyło
najdłuższe (i najbardziej interesujące) wyjście.
"""
xrefs = {}
for xref in self.grammar.getElementsByTagName("xref"):
xrefs[xref.attributes["id"].value] = 1
xrefs = xrefs.keys()
standaloneXrefs = [e for e in self.refs.keys() if e not in xrefs]
if not standaloneXrefs:
raise NoSourceError, "can't guess source, and no source specified"
return '<xref id="%s"/>' % random.choice(standaloneXrefs)
def reset(self):
u"""resetuje parser"""
self.pieces = []
self.capitalizeNextWord = 0
def refresh(self):
u"""resetuje bufor wyjściowy, ponownie parsuje cały plik źródłowy i zwraca wyjście
Ponieważ parsowanie dosyć dużo korzysta z przypadkowości, jest to
łatwy sposób, aby otrzymać nowe wyjście bez potrzeby ponownego wczytywania
pliku gramatyki.
"""
self.reset()
self.parse(self.source)
return self.output()
def output(self):
u"""wyjściowy, wygenerowany tekst"""
return "".join(self.pieces)
def randomChildElement(self, node):
u"""wybiera przypadkowy potomek węzła
Jest to użyteczna funkcja wykorzystywana w do_xref i do_choice.
"""
choices = [e for e in node.childNodes
if e.nodeType == e.ELEMENT_NODE]
chosen = random.choice(choices)
if _debug:
sys.stderr.write('%s available choices: %s\n' % \
(len(choices), [e.toxml() for e in choices]))
sys.stderr.write('Chosen: %s\n' % chosen.toxml())
return chosen
def parse(self, node):
u"""parsuje pojedynczy węzeł XML
Parsowany dokument XML (from minidom.parse) jest drzewem węzłów
złożonym z różnych typów. Każdy węzeł reprezentuje instancję
odpowiadającej jej klasy Pythona (Element dla znacznika, Text
dla danych tekstowych, Document dla dokumentu). Poniższe wyrażenie
konstruuje nazwę klasy opartej na typie węzła, który parsujemy
("parse_Element" dla węzła o typie Element,
"parse_Text" dla węzła o typie Text itp.), a następnie wywołuje te metody.
"""
parseMethod = getattr(self, "parse_%s" % node.__class__.__name__)
parseMethod(node)
def parse_Document(self, node):
u"""parsuje węzeł dokumentu
Węzeł dokument sam w sobie nie jest interesujący (przynajmniej dla nas), ale
jego jedyne dziecko, node.documentElement jest głównym węzłem gramatyki.
"""
self.parse(node.documentElement)
def parse_Text(self, node):
u"""parsuje węzeł tekstowy
Tekst węzła tekstowego jest zazwyczaj dodawany bez zmiany do wyjściowego bufora.
Jedynym wyjątkiem jest to, że <p class='sentence'> ustawia flagę, aby
pierwsza litera następnego słowa była wielka. Jeśli ta flaga jest ustawiona,
pierwszą literę tekstu robimy wielką i resetujemy tę flagę.
"""
text = node.data
if self.capitalizeNextWord:
self.pieces.append(text[0].upper())
self.pieces.append(text[1:])
self.capitalizeNextWord = 0
else:
self.pieces.append(text)
def parse_Element(self, node):
u"""parsuje element
XML-owy element odpowiada bieżącemu znacznikowi źródła:
<xref id='...'>, <p chance='...'>, <choice> itp.
Każdy typ elementu jest obsługiwany za pomocą odpowiedniej, własnej metody.
Podobnie jak to robiliśmy w parse(), konstruujemy nazwę metody
opartej na nazwie elementu ("do_xref" dla znacznika <xref> itp.), a potem
wywołujemy tę metodę.
"""
handlerMethod = getattr(self, "do_%s" % node.tagName)
handlerMethod(node)
def parse_Comment(self, node):
u"""parsuje komentarz
Gramatyka może zawierać komentarze XML, ale my je pominiemy
"""
pass
def do_xref(self, node):
u"""obsługuje znacznik <xref id='...'>
Znacznik <xref id='...'> jest odwołaniem do znacznika <ref id='...'>.
Znacznik <xref id='sentence'/> powoduje to, że zostaje wybrany w przypadkowy sposób
potomek znacznika <ref id='sentence'>.
"""
id = node.attributes["id"].value
self.parse(self.randomChildElement(self.refs[id]))
def do_p(self, node):
u"""obsługuje znacznik <p>
Znacznik <p> jest jądrem gramatyki. Może zawierać niemal
wszystko: tekst w dowolnej formie, znaczniki <choice>, znaczniki <xref>, a nawet
inne znaczniki <p>. Jeśli atrybut "class='sentence'" zostanie znaleziony, flaga
zostaje ustawiona i następne słowo będzie zapisane dużą literą. Jeśli zostanie
znaleziony atrybut "chance='X'", to mamy X% szansy, że znacznik zostanie wykorzystany
(i mamy (100-X)% szansy, że zostanie całkowicie pominięty)
"""
keys = node.attributes.keys()
if "class" in keys:
if node.attributes["class"].value == "sentence":
self.capitalizeNextWord = 1
if "chance" in keys:
chance = int(node.attributes["chance"].value)
doit = (chance > random.randrange(100))
else:
doit = 1
if doit:
for child in node.childNodes: self.parse(child)
def do_choice(self, node):
u"""obsługuje znacznik <choice>
Znacznik <choice> zawiera jeden lub więcej znaczników <p>. Jeden znacznik <p>
zostaje wybrany przypadkowo i jest następnie wykorzystywany do generowania
tekstu wyjściowego.
"""
self.parse(self.randomChildElement(node))
def usage():
print __doc__
def main(argv):
grammar = "kant.xml"
try:
opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="])
except getopt.GetoptError:
usage()
sys.exit(2)
for opt, arg in opts:
if opt in ("-h", "--help"):
usage()
sys.exit()
elif opt == '-d':
global _debug
_debug = 1
elif opt in ("-g", "--grammar"):
grammar = arg
source = "".join(args)
k = KantGenerator(grammar, source)
print k.output()
if __name__ == "__main__":
main(sys.argv[1:])
u"""Różnorodne użyteczne funkcje"""
def openAnything(source):
u"""URI, nazwa pliku lub łańcuch znaków --> strumień
Funkcja ta pozwala zdefiniować parser, który przyjmuje dowolne źródło wejścia
(URL, ścieżkę do lokalnego pliku lub znajdującego się gdzieś w sieci,
czy też bieżące dane w postaci łańcucha znaków)
i traktuje je w odpowiedni sposób. Zwracany obiekt będzie zawierał
wszystkie podstawowe metody odczytu (read, readline, readlines).
Kiedy już obiekt nie będzie potrzebny, należy go
zamknąć za pomocą metody .close().
Przykłady:
>>> from xml.dom import minidom
>>> sock = openAnything("http://localhost/kant.xml")
>>> doc = minidom.parse(sock)
>>> sock.close()
>>> sock = openAnything("c:\\inetpub\\wwwroot\\kant.xml")
>>> doc = minidom.parse(sock)
>>> sock.close()
>>> sock = openAnything("<ref id='conjunction'><text>and</text><text>or</text></ref>")
>>> doc = minidom.parse(sock)
>>> sock.close()
"""
if hasattr(source, "read"):
return source
if source == "-":
import sys
return sys.stdin
# próbuje otworzyć za pomocą modułu urllib (gdy source jest plikiem dostępnym z http,
# ftp lub URL-a)
import urllib
try:
return urllib.urlopen(source)
except (IOError, OSError):
pass
# próbuje otworzyć za pomocą wbudowanej funkcji open (jeśli source jest ścieżką
# do lokalnego pliku)
try:
return open(source)
except (IOError, OSError):
pass
# traktuje source jako łańcuch znaków
import StringIO
return StringIO.StringIO(str(source))
Uruchom sam program kgp.py
, który będzie parsował domyślną, opartą na XML gramatykę w kant.xml, a następnie wypisze kilka filozoficznych akapitów w stylu Immanuela Kanta.
[you@localhost kgp]$ python kgp.py
As is shown in the writings of Hume, our a priori concepts, in
reference to ends, abstract from all content of knowledge; in the study
of space, the discipline of human reason, in accordance with the
principles of philosophy, is the clue to the discovery of the
Transcendental Deduction. The transcendental aesthetic, in all
theoretical sciences, occupies part of the sphere of human reason
concerning the existence of our ideas in general; still, the
never-ending regress in the series of empirical conditions constitutes
the whole content for the transcendental unity of apperception. What
we have alone been able to show is that, even as this relates to the
architectonic of human reason, the Ideal may not contradict itself, but
it is still possible that it may be in contradictions with the
employment of the pure employment of our hypothetical judgements, but
natural causes (and I assert that this is the case) prove the validity
of the discipline of pure reason. As we have already seen, time (and
it is obvious that this is true) proves the validity of time, and the
architectonic of human reason, in the full sense of these terms,
abstracts from all content of knowledge. I assert, in the case of the
discipline of practical reason, that the Antinomies are just as
necessary as natural causes, since knowledge of the phenomena is a
posteriori.
The discipline of human reason, as I have elsewhere shown, is by
its very nature contradictory, but our ideas exclude the possibility of
the Antinomies. We can deduce that, on the contrary, the pure
employment of philosophy, on the contrary, is by its very nature
contradictory, but our sense perceptions are a representation of, in
the case of space, metaphysics. The thing in itself is a
representation of philosophy. Applied logic is the clue to the
discovery of natural causes. However, what we have alone been able to
show is that our ideas, in other words, should only be used as a canon
for the Ideal, because of our necessary ignorance of the conditions.
[...ciach...]
Jest to oczywiście kompletny bełkot. No dobra, nie całkowity bełkot. Jest składniowo i gramatycznie poprawny (chociaż bardzo wielomówny). Niektóre fragmenty mogą być rzeczywiście prawdą (lub przy najmniej z niektórymi Kant by się zgodził), a niektóre są ewidentnie nieprawdziwe, a wiele fragmentów jest po prostu niespójnych. Lecz wszystko jest w stylu Immanuela Kanta.
Interesującą rzeczą w tym programie jest to, że nie ma tu nic, co określa Kanta. Cała zawartość poprzedniego przykładu pochodzi z pliku gramatyki, kant.xml. Jeśli każemy programowi wykorzystać inny plik gramatyki (który możemy określić z linii poleceń), wyjście będzie kompletnie różne.
[you@localhost kgp]$ python kgp.py -g binary.xml
00101001
[you@localhost kgp]$ python kgp.py -g binary.xml
10110100