Home
Navigation
Impressum
Coder Welten - Programmierung und Optimierung

HTML-Formatparser – Einzelheiten und Code des Scripts

Das Script mit der Klasse Formatparser

Übersicht / Seite:

  1. HTML-Formatparser (Vorwort zur Beschreibung)
  2. HTML-Formatparser Klasse (Das Script mit der Klasse Formatparser)
  3. Ausgewählte Beispiele (Beispiele für Notierungen im HTML-Quelltext)
  4. Beispiel Quelltext (Ein längeres Beispiel für erste Tests)

In HTML wird zwischen Block- und Inlineelementen unterschieden, wobei Block­elemente unterschiedliche Inline­elemente enthalten können. Es sei an dieser Stelle angemerkt, die Einteilung der Elemente wurde in HTML5 über­arbeitet und neu kate­gorisiert. Zum leichteren Verständnis möchten wir an dieser Stelle bei der alten Einteilung bleiben.
Als Beispiel soll ein p-Element dienen (p steht für Paragraph und erzeugt einen neuen Absatz). Innerhalb eines HTML-Quelltextes wird der Beginn eines Absatzes mit einem öffnenden <p> notiert und der Absatz endet erst mit einem schließenden </p>. Ein Browser erkennt, wenn innerhalb eines Absatzes Inlineelemente wie z.B. <span ...> bis </span> notiert wurde.
Anders bei unseren selbst zu definierenden Tags für ein Tkinter Text-Widget. Eine Unterscheidung zwischen Block- und Inlinetags würde es nicht geben und ein mit dem Tag-Namen "p" notierter Tag würde mit dem nächsten von uns definierten Tag enden, also z.B. bei einem Tag mit dem Namen "br" oder "strong".

Am einfachsten erwies es sich bei HTML-Inlineelementen, die schließenden Elemente-Tags beim Einlesen in [end...] zu wandeln und vor der Ausgabe diese Platzhalter [end...] zum Trennen mit Split zu verwenden. Alles was zwischen Tag-Beginn und [end...] im eigelesenen String bzw. in der Variablen "data" enthalten ist, gehört dann zum Bereich des Inlineelementes, was nach [end...] folgt wieder zum Tag "normal" oder zum Tag "zitate".
Ein anderes Problem betrifft die Erkennung der Position. So sollen z.B. span-Tags, für die innerhalb von Tkinter die Namen der CSS-Klassen benutzt werden, am Zeilenbeginn keine Leerzeichen vorangestellt werden, innerhalb einer Zeile hingegen immer, damit Worte nicht ohne Zwischenraum miteinander verschmelzen.

Aus dem HTML-Quelltext wurden als Bezeichner für Tkinter-Tags die Bezeichner der CSS-Klassen "boldbraun", "mittig", "bildnotizen", "quotes" und "zitate" über­nommen. Die Namen und Formatierungen für die span-Tags können geändert werden, auch ist es möglich diesen neue CSS-Eigenschaften zuzuweisen oder weitere CSS-Klassen zu definieren. Immer sollte dabei bedacht werden, wird ein CSS Class-Name im HTML-Quelltext geändert, so muss auch der Name im Script und der zugehörige Tkinter-Tag angepasst und geändert werden. Etwas einfacher verhält es sich bei den Tags für <strong>, <em>, <u>, <li>, da diese in HTML-Quelltext und innerhalb von Tkinter eine sich gleichende Formatierung besitzen.

Weitere Einzelheiten sind den Kommentaren zu entnehmen.

Das Script mit der Klasse Formatparser:

#!/usr/local/bin/python
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# App-Name:    HTML-Formatparser
# Description: Diese Anwendung ermöglicht eine formatierte Ausgabe von 
#              HTML-Seiten in einem Tkinter Text-Widget, wenn der Quelltext der 
#              HTML-Seiten auf diese Verwendung abgestimmt wird.
# Autor:       Horst Müller
# Version:     1.0
# Datum:       20. Oktober 2017
# Lizenz:      GPLv3, einsehbar unter http://www.gnu.org/licenses/
# -----------------------------------------------------------------------------
from tkinter import Tk, Text, Scrollbar, PhotoImage, END
from PIL import Image, ImageTk
from html.parser import HTMLParser

class Formatparser(HTMLParser):
    """
    Diese Klasse ermöglicht eine formatierte Ausgabe von HTML-Seiten in einem 
    Tkinter Text-Widget, wenn der Quelltext der HTML-Seiten auf diese 
    Verwendung abgestimmt wird.
    """
    def __init__(self):
        HTMLParser.__init__(self)
        self.fenster = Tk()
        self.images = []

        self.ausschluss  = ["html", "head", "title", "style", "body"]
        self.auswahltags = [
            "boldbraun", "boldgrau", "strong", "em", "underline", "p"
            ]
        self.starttag = None
        self.attrs = None
        self.letzter_starttag = None
        self.letzte_datalaenge = 1
        self.letzter_bereich = None

    def layout(self):
        self.fenster.title("Lesetest")
        self.fenster.config(bg="#d9cda3")

        self.textfeld = Text(
            self.fenster, height=48, pady=12, padx=12, wrap="word") 
        scrollbar = Scrollbar(self.fenster)
        scrollbar.config(command = self.textfeld.yview)
        self.textfeld.config(yscrollcommand = scrollbar.set)
        scrollbar.pack(side="right", fill="y")
        self.textfeld.pack(pady=24, padx=24)

        self.textfeld.tag_config(
            "h1", font=("cambria", 22),
            foreground="#c7a621", spacing3=10)
        self.textfeld.tag_config(
            "h2", font=("cambria", 18),
            foreground="#98732e", spacing3=28)
        self.textfeld.tag_config(
            "h3", font=("cambria", 12, "bold"),
            foreground="#74521c", spacing1=22)
        self.textfeld.tag_config(
            "p", font=("cambria", 12))
        self.textfeld.tag_config(
            "br", font=("cambria", 12))
        self.textfeld.tag_config(
            "normal", font=("cambria", 12))
        self.textfeld.tag_config(
            "klein", font=("cambria", 6))
        self.textfeld.tag_config(
            "strong", font=("cambria", 12, "bold"))
        self.textfeld.tag_config(
            "em", font=("cambria", 12, "italic"))
        self.textfeld.tag_config(
            "underline", font=("cambria", 12, "underline"))
        self.textfeld.tag_config(
            "mittig", font=("cambria", 12), justify="center")
        self.textfeld.tag_config(
            "img", justify="center", spacing1=6)
        self.textfeld.tag_config(
            "bildnotizen", font=("cambria", 10, "italic"),
            foreground="#867a44", justify="center")
        self.textfeld.tag_config(
            "boldgrau", font=("cambria", 12, "bold"),
            foreground="#404040")
        self.textfeld.tag_config(
            "boldbraun", font=("cambria", 12, "bold"),
            foreground="#9b4c21")
        self.textfeld.tag_config(
            "liste", font=("cambria", 12), rmargin=20, lmargin2=20)
        self.textfeld.tag_config(
            "fehler",  font=("cambria", 12),
            foreground="#ff8808", justify="center")

        # Tags für Div-Bereiche mit class "quotes"
        self.textfeld.tag_config(
            "quotes", background="#e6e5da")
        self.textfeld.tag_config(
            "zitate",  font=("cambria", 12, "italic"),
            background="#e6e5da", justify="center",
            rmargin=20, lmargin1=20)
        self.textfeld.tag_config(
            "strongzitate", font=("cambria", 12, "bold italic"),
            background="#e6e5da", justify="center",
            rmargin=20, lmargin1=20)
        self.textfeld.tag_config(
            "underzitate", font=("cambria", 12, "italic underline"),
            background="#e6e5da", justify="center",
            rmargin=20, lmargin1=20)
        self.textfeld.tag_config(
            "boldbraunzitate", font=("cambria", 12, "bold italic"),
            foreground="#9b4c21", background="#e6e5da", justify="center",
            rmargin=20, lmargin1=20)
        self.textfeld.tag_config(
            "boldgrauzitate", font=("cambria", 12, "bold italic"),
            foreground="#404040", background="#e6e5da", justify="center",
            rmargin=20, lmargin1=20)

    def oeffne_htmlseite(self, filename):
        try:
            with open(filename) as datei:
                dateistring = datei.read().replace(
                              "</span>", "[endspan]").replace(
                              "</strong>", "[endstrong]").replace(
                              "</em>", "[endem]").replace(
                              "</u>", "[endunderline]")
                self.feed(dateistring)
        except FileNotFoundError:
            self.feed("<h3>Die Datei wurde nicht gefunden!</h3>")

    def insert_content(self, anfang, content, ende, tag):
        self.textfeld.insert(END, "{0}{1}{2}"
            .format(anfang, content, ende), tag)

    # Für <strong> oder <span class="name"> würde je nach Postion eine
    # Kombination aus "", " " und "\n" am Anfang und Ende genügen, nicht
    # hingegen für <u>. Bei <u> würden Leerzeichen " " am Anfang oder Ende mit
    # unterstrichen werden. Um dieses zu verhindern wurde " " durch "" und
    # jeweils einem zusätzlichen "folgetag" für Leerzeichen " " ersetzt.

    def liefere_leerzeichen(self, folgetag):
        self.insert_content("", " ", "", folgetag)

    def fuelle_inlinetags(self, folgetag, content, tagauswahl):
        self.liefere_leerzeichen(folgetag)
        self.insert_content("", content, "", tagauswahl)
        self.liefere_leerzeichen(folgetag)

    def formatiere_inlinetags(
        self, data, htmlstarttag, bereichstag, htmlendtag):
        elementedata = data.split(htmlendtag)
        content = " ".join(elementedata[0].split())
        folgetag = ("normal" if self.letzter_bereich != "quotes" else "zitate")
        tagauswahl = htmlstarttag
        if self.letzter_bereich == "quotes":
            tagauswahl = bereichstag

        # Content vor [endtag]
        if ((self.letzter_starttag == htmlstarttag or
            self.letzter_starttag == "br") and
            self.letzte_datalaenge < 2):
            self.insert_content("", content, "", tagauswahl)
            self.liefere_leerzeichen(folgetag)
        elif (self.letzter_starttag in self.auswahltags and
            self.letzte_datalaenge < 2):
            self.insert_content("", content, "", tagauswahl)
            self.liefere_leerzeichen(folgetag)
        else:
            self.fuelle_inlinetags(folgetag, content, tagauswahl)

        # Content hinter [endtag]
        if len(elementedata) > 1:
            content = " ".join(elementedata[1].split())
            if self.letzte_datalaenge < 2:
                self.insert_content("", content, "", folgetag)
                self.liefere_leerzeichen(folgetag)
            else:
                self.fuelle_inlinetags(folgetag, content, folgetag)
        self.letzte_datalaenge = len(elementedata)
        self.letzter_starttag = htmlstarttag

    def handle_starttag(self, tag, attrs):
        self.starttag = tag
        self.attrs = attrs
        # Vor und hinter dem Image ein Leerzeichen einfügen, um es mit den
        # Leerzeichen in Reihe mittig mit dem "img"-Tag auszurichten.
        if tag == "img":
            try:
                image = Image.open(dict(attrs)["src"])
                image = ImageTk.PhotoImage(image)
                self.insert_content("\n", " ", "", "img")
                self.textfeld.image_create(END, image=image)
                self.insert_content(""," ", "\n", "img")
                self.images.append(image)
                self.letzter_starttag = tag
            except FileNotFoundError:
                self.textfeld.insert(
                    END, "Das Image wurde nicht gefunden.\n\n",
                    "fehler")
        # Für quotes/zitate wird der Beginn eines Div-Bereiches in Tkinter
        # nachgebildet und in der Methode "handle_endtag" geschlossen.
        elif self.starttag == "div":
            if "class" in dict(self.attrs):
                if dict(self.attrs)["class"] == "quotes":
                    self.insert_content("", " ", "\n\n", "normal")
                    self.insert_content("", " ", "\n", "zitate")
                    self.letzter_bereich = "quotes"

    def handle_endtag(self, tag):
        if tag == "div" and self.letzter_bereich == "quotes":
            anfang = ""
            if self.letzter_starttag in (
                "boldbraun", "boldgrau", "strong", "u"):
                anfang = "\n"
            self.insert_content(anfang, " ", "\n", "zitate")
            self.insert_content("", "  ", "", "normal")
            self.letzter_bereich = "endquotes"

        # Einen Bereich wieder verlassen und zurückzusetzen.
        if tag in ("h3", "p", "li"):
            if self.letzter_bereich == "endquotes":
                self.letzter_bereich = None

    def handle_data(self, data):
        if self.starttag not in self.ausschluss:
            # Die Einrückungen aus dem HTML-Quelltext werden entfernt, da das 
            # Textfeld diese Aufgabe und den Zeilenumbruch übernimmt.
            
            if self.starttag in ("h1", "h2"):
                content = data.replace("  ", "")
                self.insert_content("", content, "", self.starttag)
                
            elif self.starttag == "h3":
                anfang = "\n"
                if (self.letzter_bereich == "endquotes" and
                    self.letzter_starttag == "br"):
                    anfang = ""
                content = data.replace("  ", "")
                if len(content) > 2:
                    self.insert_content(anfang, content, "", self.starttag)
                # Die Zeile zwischen h3 und dem nachfolgenden Content.
                else:
                   self.insert_content("\n", content, "", "klein")
                self.letzte_datalaenge = len(content)
                letzter_starttag = self.starttag

            elif self.starttag == "p":
                content = " ".join(data.split())
                # Nach einem "quotes"-Bereich nur "\n" wenn der letzte Tag kein
                # br-Tag war.
                if not "class" in dict(self.attrs):
                    if (self.letzter_bereich == "endquotes" and
                        self.letzter_starttag in self.auswahltags):
                        self.insert_content("\n", content, "", "normal")
                    else:
                         self.insert_content("", content, "", "normal")

                # Bei class "mittig"  ebenfalls kein "\n" nach br.
                elif dict(self.attrs)["class"] == "mittig":
                    anfang = ("\n" if self.letzter_starttag != "br" else "")
                    self.insert_content(anfang, content, "", "mittig")
                elif dict(self.attrs)["class"] == "zitate":
                    if len(content):
                        self.insert_content("", content, "\n", "zitate")
                self.letzte_datalaenge = len(content)
                self.letzter_starttag = self.starttag

            elif self.starttag == "br":
                content = " ".join(data.split())
                if (self.letzter_starttag != "img" and
                    self.letzter_bereich != "quotes"):
                    self.insert_content("\n", content, "", "normal")

                # Wenn letzter_bereich gleich "quotes", dann für "br" als
                # Tkinter-Tag "zitate" verwenden.
                elif self.letzter_bereich == "quotes":
                    anfang = ("\n" if self.letzter_starttag != "p" else "")
                    self.insert_content(anfang, content, "", "zitate")
                self.letzte_datalaenge = len(content)
                self.letzter_starttag = self.starttag

            # Wenn ein 'class name' aus dem HTML-Dokument verwendet werden soll,
            # wird dieser abgefragt und als Tkinter-Tag angelegt.
            # Inline-Tags ohne "\n" am Ende, mit Ausnahme von "bildnotizen".
            elif self.starttag == "span" and dict(self.attrs)["class"]:
                tag = dict(self.attrs)["class"]
                spandata = data.split("[endspan]")

                if tag == "boldbraun":
                    self.formatiere_inlinetags(
                        data, "boldbraun", "boldbraunzitate", "[endspan]")
                elif tag == "boldgrau":
                    self.formatiere_inlinetags(
                        data, "boldgrau", "boldgrauzitate", "[endspan]")
                elif tag == "bildnotizen":
                    content = " ".join(spandata[0].split())
                    if len(spandata)> 1:
                        self.insert_content("", content, "\n\n", tag)
                self.letzte_datalaenge = len(spandata)
                self.letzter_starttag = tag

            # Für "em" kein eigener Tag "emzitate", da "zitate" bereits in
            # Schrägschrift formatiert werden.
            elif self.starttag == "strong":
                self.formatiere_inlinetags(
                    data, "strong", "strongzitate", "[endstrong]")
            elif self.starttag == "em":
                self.formatiere_inlinetags(
                    data, "em", "zitate", "[endem]")
            elif self.starttag == "u":
                self.formatiere_inlinetags(
                    data, "underline", "underzitate", "[endunderline]")

            elif self.starttag == "li":
                content = data.replace("  ", "")
                bullet = "  \u2022 "
                if self.letzter_starttag != "li":
                    bullet = "\n\n  \u2022 "
                if len(content) > 2:
                    self.insert_content(bullet, content, "\n", "liste")
                self.letzte_datalaenge = len(content)
                self.letzter_starttag = self.starttag

def main():
    parser = Formatparser()
    parser.layout()
    parser.oeffne_htmlseite("lorem-ipsum.html")
    parser.fenster.mainloop()

if __name__ == "__main__":
    main()

Weiterlesen » Ausgewählte Beispiele

 
Navigation

Einstieg in Python

 


OOP mit Python

 


Codes & Tutorials

 

Weitere Themen

Kleines Projekt

 


Copyright © Verlag Horst Müller - Stendal - 2006 - Impressum - Datenschutz - Nutzungsbedingungen