Zanurkuj w Pythonie/Przetwarzanie HTML-a

Nurkujemy

edytuj

Na comp.lang.python często można zobaczyć pytania w stylu "jak można znaleźć wszystkie nagłówki/obrazki/linki w moim dokumencie HTML?", "jak mogę sparsować/przetłumaczyć/przerobić tekst mojego dokumentu HTML tak, aby zostawić znaczniki w spokoju?" lub też "jak mogę natychmiastowo dodać/usunąć/zacytować atrybuty z wszystkich znaczników mojego dokumentu HTML?". Rozdział ten odpowiada na wszystkie te pytania.

Poniżej przedstawiono w dwóch częściach całkowicie działający program. Pierwsza część, BaseHTMLProcessor.py jest ogólnym narzędziem, które przetwarza pliki HTML przechodząc przez wszystkie znaczniki i bloki tekstowe. Druga część, dialect.py, jest przykładem tego, jak wykorzystać BaseHTMLProcessor.py, aby przetłumaczyć tekst dokumentu HTML, lecz przy tym zostawiając znaczniki w spokoju. Przeczytaj notki dokumentacyjne i komentarze w celu zorientowania się, co się tutaj właściwie dzieje. Duża część tego kodu wygląda jak czarna magia, ponieważ nie jest oczywiste w jaki sposób dowolna z metod klasy jest wywoływana. Jednak nie martw się, wszystko zostanie wyjaśnione w odpowiednim czasie.

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

from sgmllib import SGMLParser
import htmlentitydefs
 
class BaseHTMLProcessor(SGMLParser):
    def reset(self):
        # dodatek (wywoływane przez SGMLParser.__init__)
        self.pieces = []
        SGMLParser.reset(self)
 
    def unknown_starttag(self, tag, attrs):
        # wywoływane dla każdego początkowego znacznika
        # attrs jest listą krotek (atrybut, wartość)
        # np. dla <pre class="screen"> będziemy mieli tag="pre", attrs=[("class", "screen")]
        # Chcielibyśmy zrekonstruować oryginalne znaczniki i atrybuty, ale
        # może się zdarzyć, że umieścimy w cudzysłowach wartości, które nie były zacytowane
        # w źródle dokumentu, a także możemy zmienić rodzaj cudzysłowów w wartości danego
        # atrybutu (pojedyncze cudzysłowy lub podwójne).
        # Dodajmy, że niepoprawnie osadzony kod nie-HTML-owy (np. kod JavaScript)
        # może zostać źle sparsowany przez klasę bazową, a to spowoduje błąd wykonania skryptu.
        # Cały nie-HTML musi być umieszczony w komentarzu HTML-a (<!-- kod -->),
        # aby parser zostawił ten niezmieniony (korzystając z handle_comment).
        strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs])
        self.pieces.append("<%(tag)s%(strattrs)s>" % locals())
 
    def unknown_endtag(self, tag):
        # wywoływane dla każdego znacznika końcowego np. dla </pre>, tag będzie równy "pre"
        # Rekonstruuje oryginalny znacznik końcowy w wyjściowym dokumencie
        self.pieces.append("</%(tag)s>" % locals())
 
    def handle_charref(self, ref):
        # wywoływane jest dla każdego odwołania znakowego np. dla "&#160;", ref będzie równe "160"
        # Rekonstruuje oryginalne odwołanie znakowe.
        self.pieces.append("&#%(ref)s;" % locals())
 
    def handle_entityref(self, ref):
        # wywoływane jest dla każdego odwołania do encji np. dla "&copy;", ref będzie równe "copy"
        # Rekonstruuje oryginalne odwołanie do encji.
        self.pieces.append("&%(ref)s" % locals())
        # standardowe encje HTML-a są zakończone średnikiem; pozostałe encje (encje spoza HTML-a)
        # nie są
        if htmlentitydefs.entitydefs.has_key(ref):
            self.pieces.append(";")
 
    def handle_data(self, text):
        # wywoływane dla każdego bloku czystego teksu np. dla danych spoza dowolnego
        # znacznika, w których nie występują żadne odwołania znakowe, czy odwołania do encji.
        # Przechowuje dosłownie oryginalny tekst.
        self.pieces.append(text)
 
    def handle_comment(self, text):
        # wywoływane dla każdego komentarza np. <!-- wpis kod JavaScript w tym miejscu -->
        # Rekonstruuje oryginalny komentarz.
        # Jest to szczególnie ważne, gdy dokument zawiera kod przeznaczony
        # dla przeglądarki (np. kod Javascript) wewnątrz komentarza, dzięki temu
        # parser może przejść przez ten kod bez zakłóceń;
        # więcej szczegółów w komentarzu metody unknown_starttag.
        self.pieces.append("<!--%(text)s-->" % locals())
 
    def handle_pi(self, text):
        # wywoływane dla każdej instrukcji przetwarzania np. <?instruction>
        # Rekonstruuje oryginalną instrukcję przetwarzania
        self.pieces.append("<?%(text)s>" % locals())
 
    def handle_decl(self, text):
        # wywoływane dla deklaracji typu dokumentu, jeśli występuje, np.
        # <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        #     "http://www.w3.org/TR/html4/loose.dtd">
        # Rekonstruuje oryginalną deklarację typu dokumentu
        self.pieces.append("<!%(text)s>" % locals())
 
    def output(self):
        u"""Zwraca przetworzony HTML jako pojedynczy łańcuch znaków"""
        return "".join(self.pieces)
 
if __name__ == "__main__":
    for k, v in globals().items():
        print k, "=", v
Przykład. dialect.py
#-*- coding: utf-8 -*-

import re
from BaseHTMLProcessor import BaseHTMLProcessor

class Dialectizer(BaseHTMLProcessor):
    subs = ()
    
    def reset(self):
        # dodatek (wywoływany przez __init__ klasy bazowej)
        # Resetuje wszystkie atrybuty
        self.verbatim = 0
        BaseHTMLProcessor.reset(self)
        
    def start_pre(self, attrs):
        # wywoływane dla każdego znacznika <pre> w źródle HTML
        # Zwiększa licznik trybu dosłowności verbatim, a następnie
        # obsługuje ten znacznik normalnie
        self.verbatim += 1
        self.unknown_starttag("pre", attrs)

    def end_pre(self):
        # wywoływane dla każdego znacznika </pre>
        # Zmniejsza licznik trybu dosłowności verbatim
        self.unknown_endtag("pre")
        self.verbatim -= 1

    def handle_data(self, text):
        # metoda nadpisana
        # wywoływane dla każdego bloku tekstu w źródle
        # Jeśli jest w trybie dosłownym, zapisuje tekst niezmieniony;
        # inaczej przetwarza tekst za pomocą szeregu podstawień
        self.pieces.append(self.verbatim and text or self.process(text))

    def process(self, text):
        # wywoływane z handle_data
        # Przetwarza każdy blok wykonując serie podstawień
        # za pomocą wyrażeń regularnych (podstawienia są definiowane przez klasy pochodne)
        for fromPattern, toPattern in self.subs:
            text = re.sub(fromPattern, toPattern, text)
        return text

class ChefDialectizer(Dialectizer):
    u"""konwertuje HTML na mowę szwedzkiego szefa kuchni

    oparte na klasycznym chef.x, copyright (c) 1992, 1993 John Hagerman
    """
    subs = ((r'a([nu])', r'u\1'),
            (r'A([nu])', r'U\1'),
            (r'a\B', r'e'),
            (r'A\B', r'E'),
            (r'en\b', r'ee'),
            (r'\Bew', r'oo'),
            (r'\Be\b', r'e-a'),
            (r'\be', r'i'),
            (r'\bE', r'I'),
            (r'\Bf', r'ff'),
            (r'\Bir', r'ur'),
            (r'(\w*?)i(\w*?)$', r'\1ee\2'),
            (r'\bow', r'oo'),
            (r'\bo', r'oo'),
            (r'\bO', r'Oo'),
            (r'the', r'zee'),
            (r'The', r'Zee'),
            (r'th\b', r't'),
            (r'\Btion', r'shun'),
            (r'\Bu', r'oo'),
            (r'\BU', r'Oo'),
            (r'v', r'f'),
            (r'V', r'F'),
            (r'w', r'w'),
            (r'W', r'W'),
            (r'([a-z])[.]', r'\1.  Bork Bork Bork!'))

class FuddDialectizer(Dialectizer):
    u"""konwertuje HTML na mowę Elmer Fudda"""
    subs = ((r'[rl]', r'w'),
            (r'qu', r'qw'),
            (r'th\b', r'f'),
            (r'th', r'd'),
            (r'n[.]', r'n, uh-hah-hah-hah.'))

class OldeDialectizer(Dialectizer):
    u"""konwertuje HTML na pozorowany język średnioangielski"""
    subs = ((r'i([bcdfghjklmnpqrstvwxyz])e\b', r'y\1'),
            (r'i([bcdfghjklmnpqrstvwxyz])e', r'y\1\1e'),
            (r'ick\b', r'yk'),
            (r'ia([bcdfghjklmnpqrstvwxyz])', r'e\1e'),
            (r'e[ea]([bcdfghjklmnpqrstvwxyz])', r'e\1e'),
            (r'([bcdfghjklmnpqrstvwxyz])y', r'\1ee'),
            (r'([bcdfghjklmnpqrstvwxyz])er', r'\1re'),
            (r'([aeiou])re\b', r'\1r'),
            (r'ia([bcdfghjklmnpqrstvwxyz])', r'i\1e'),
            (r'tion\b', r'cioun'),
            (r'ion\b', r'ioun'),
            (r'aid', r'ayde'),
            (r'ai', r'ey'),
            (r'ay\b', r'y'),
            (r'ay', r'ey'),
            (r'ant', r'aunt'),
            (r'ea', r'ee'),
            (r'oa', r'oo'),
            (r'ue', r'e'),
            (r'oe', r'o'),
            (r'ou', r'ow'),
            (r'ow', r'ou'),
            (r'\bhe', r'hi'),
            (r've\b', r'veth'),
            (r'se\b', r'e'),
            (r"'s\b", r'es'),
            (r'ic\b', r'ick'),
            (r'ics\b', r'icc'),
            (r'ical\b', r'ick'),
            (r'tle\b', r'til'),
            (r'll\b', r'l'),
            (r'ould\b', r'olde'),
            (r'own\b', r'oune'),
            (r'un\b', r'onne'),
            (r'rry\b', r'rye'),
            (r'est\b', r'este'),
            (r'pt\b', r'pte'),
            (r'th\b', r'the'),
            (r'ch\b', r'che'),
            (r'ss\b', r'sse'),
            (r'([wybdp])\b', r'\1e'),
            (r'([rnt])\b', r'\1\1e'),
            (r'from', r'fro'),
            (r'when', r'whan'))

def translate(url, dialectName="chef"):
    u"""pobiera plik na podstawie URL-a
    i tłumaczy korzystając z dialektu, gdzie
    dialekt in ("chef", "fudd", "olde")"""
    import urllib
    sock = urllib.urlopen(url)
    htmlSource = sock.read()
    sock.close()
    parserName = "%sDialectizer" % dialectName.capitalize()
    parserClass = globals()[parserName]
    parser = parserClass()
    parser.feed(htmlSource)
    parser.close()
    return parser.output()

def test(url):
    u"""testuje wszystkie dialekty na pewnym URL-u"""
    for dialect in ("chef", "fudd", "olde"):
        outfile = "%s.html" % dialect
        fsock = open(outfile, "wb")
        fsock.write(translate(url, dialect))
        fsock.close()
        import webbrowser
        webbrowser.open_new(outfile)
    
if __name__ == "__main__":
    test("http://diveintopython.org/odbchelper_list.html")

Uruchamiając ten skrypt, przetłumaczymy podrozdział 3.2, z książki "Dive Into Python", na pozorowany szwedzki kuchmistrza z Muppetów, udawany język Elmer Fudda (z kreskówek Królik Bugs) i pozorowany język średnioangielski (luźno oparty na "Chaucer's The Canterbury Tales"). Jeśli spojrzymy na źródło HTML wyjściowej strony, zobaczymy, że znaczniki i atrybuty zostały nietknięte, lecz tekst między znacznikami został "przetłumaczony" na udawany język. Jeśli przyglądniemy się jeszcze bardziej, zobaczymy, że tylko tytuły i akapity zostały przetłumaczone. Przedstawione kody i wyniki działania programu zostały niezmienione.

Przykład. Wyjście z dialect.py
<div class="abstract">
<p>Lists awe <span class="application">Pydon</span>'s wowkhowse datatype.
If youw onwy expewience wif wists is awways in
<span class="application">Visuaw Basic</span> ow (God fowbid) de datastowe
in <span class="application">Powewbuiwdew</span>, bwace youwsewf fow
<span class="application">Pydon</span> wists.</p>
</div>