Zanurkuj w Pythonie/Przetwarzanie HTML-a
Nurkujemy
edytujNa 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 " ", 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 "©", 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
#-*- 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.
<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>