Zanurkuj w Pythonie/Źródła/kgp/kgp.py

#-*- coding: utf-8 -*-

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ć

Ten program jest częścią książki "Zanurkuj w Pythonie", podręcznika
o Pythonie dla doświadczonych programistów. Najnowszą wersję można
znaleźć tu: http://pl.wikibooks.org/wiki/Zanurkuj_w_Pythonie.
 
Program ten został oparty na przykładach zawartych w książce
"Dive Into Python", a dostępnej stąd: http://www.diveintopython.org.
"""

__author__ = "Mark Pilgrim (mark@diveintopython.org)"
__version__ = "$Revision: 1.4 $"
__date__ = "$Date: 2004/05/05 21:57:19 $"
__copyright__ = "Copyright (c) 2001 Mark Pilgrim"
__license__ = "Python"

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łow 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 (przynajmnie dla nas), ale
        jego jedyne dziecko, node.documentElement jest: 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
        pierwszą literę 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:])