Zanurkuj w Pythonie/Przetwarzanie XML-a

Nurkujemy

edytuj

Kolejne 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.

Przykład. kgp.py
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:])
Przykład. toolbox.py
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.

Przykład. Przykładowe wyjście kgp.py
[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.

Przykład. Proste wyjście kgp.py
[you@localhost kgp]$ python kgp.py -g binary.xml
00101001
[you@localhost kgp]$ python kgp.py -g binary.xml

10110100