Zanurkuj w Pythonie/Wprowadzenie do BaseHTMLProcessor.py

SGMLParser nie tworzy niczego samodzielnie. On po prostu parsuje, parsuje i parsuje i wywołuje metodę dla każdej interesującej rzeczy jaką znajdzie, ale te metody nie wykonują niczego. SGMLParser jest konsumentem HTML-a: bierze HTML-a i rozkłada go na małe, strukturalne części. Jak już widzieliśmy w poprzednim podrozdziale, możemy dziedziczyć po klasie SGMLParser, aby zdefiniować klasy, które przechwycą poszczególne znaczniki i jakoś to pożytecznie wykorzystają, np. stworzą listę odnośników na danej stronie internetowej. Teraz pójdziemy krok dalej i zdefiniujemy klasę, która przechwyci wszystko, co zgłosi SGMLParser i zrekonstruuje kompletny dokument HTML. Używając terminologii technicznej nasza klasa będzie producentem HTML-a.

BaseHTMLProcessor dziedziczy po SGMLParser i dostarcza 8 istotnych metod obsługi: unknown_starttag, unknown_endtag, handle_charref, handle_entityref, handle_comment, handle_pi, handle_decl i handle_data.

Przykład. Wprowadzenie do BaseHTMLProcessor.py
 class BaseHTMLProcessor(SGMLParser):
     def reset(self):                        #(1)
         self.pieces = []
         SGMLParser.reset(self)
 
     def unknown_starttag(self, tag, attrs): #(2)
         strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs])
         self.pieces.append("<%(tag)s%(strattrs)s>" % locals())
 
     def unknown_endtag(self, tag):          #(3)
         self.pieces.append("</%(tag)s>" % locals())
 
     def handle_charref(self, ref):          #(4)
         self.pieces.append("&#%(ref)s;" % locals())
 
     def handle_entityref(self, ref):        #(5)
         self.pieces.append("&%(ref)s" % locals())
         if htmlentitydefs.entitydefs.has_key(ref):
             self.pieces.append(";")
 
     def handle_data(self, text):            #(6)
         self.pieces.append(text)
 
     def handle_comment(self, text):         #(7)
         self.pieces.append("<!--%(text)s-->" % locals())
 
     def handle_pi(self, text):              #(8)
         self.pieces.append("<?%(text)s>" % locals())
 
     def handle_decl(self, text):
         self.pieces.append("<!%(text)s>" % locals())
  1. reset, wołany przez SGMLParser.__init__, inicjalizuje self.pieces jako pustą listę przed wywołaniem metody klasy przodka. self.pieces jest atrybutem, który będzie przechowywał części konstruowanego dokumentu HTML. Każda metoda będzie rekonstruować HTML parsowany przez SGMLParser i każda z tych metod będzie dodawać jakiś tekst do self.pieces. Zauważmy, że self.pieces jest listą. Moglibyśmy ulec pokusie, aby zdefiniować ten atrybut jako obiekt łańcucha znaków i po prostu dołączać do niego kolejne kawałki tekstu. To także by działało, ale Python jest dużo bardziej wydajny pracując z listami[1].
  2. Ponieważ BaseHTMLProcessor nie definiuje żadnej metody dla poszczególnych znaczników (jak np. metoda start_a w URLLister), SGMLParser będzie wywoływał dla każdego początkowego znacznika metodę unknown_starttag. Ta metoda przyjmuje na wejściu znacznik (argument tag) i listę par postaci nazwa atrybutu/wartość atrybutu (argument attrs), a następnie rekonstruuje oryginalnego HTML-a i dodaje do self.pieces. Napis formatujący jest tutaj nieco dziwny; rozwikłamy to później w tym rozdziale (a także tą dziwnie wyglądającą funkcję locals).
  3. Rekonstrukcja znaczników końcowych jest dużo prostsza; po prostu pobieramy nazwę znacznika i opakowujemy nawiasami ostrymi </...>.
  4. Gdy SGMLParser napotka odwołanie znakowe wywołuje metodę handle_charref i przekazuje jej samą wartość odwołania. Jeśli dokument HTML zawiera  , ref przyjmie wartość 160. Rekonstrukcja oryginalnego kompletnego odwołania znakowego wymaga po prostu dodania znaków &#...;.
  5. Odwołanie do encji jest podobne do odwołania znakowego, ale nie zawiera znaku kratki (#). Rekonstrukcja oryginalnego odwołania do encji wymaga dodania znaków &;...;. (Właściwie, jak wskazał na to pewien czytelnik, jest to nieco bardziej skomplikowane. Tylko niektóre standardowe encje HTML-a kończą się znakiem średnika; inne podobnie wyglądające encje już nie. Na szczęście dla nas zbiór standardowych encji HTML-a zdefiniowany jest w Pythonie w słowniku w module o nazwie htmlentitydefs. Stąd ta dodatkowa instrukcja if.)
  6. Bloki tekstu są po prostu dołączane do self.pieces bez żadnych zmian, w postaci dosłownej.
  7. Komentarze HTML-a opakowywane są znakami <!--...-->.
  8. Instrukcje przetwarzania wstawiane są pomiędzy znakami <?...>.
Przykład. BaseHTMLProcessor i jego metoda output
     def output(self):               #(1)
         u"""Zwraca przetworzony HTML jako pojedynczy łańcuch znaków"""
         return "".join(self.pieces)
  1. To jest jedyna metoda, która nie jest wołana przez klasę przodka, czyli klasę SGMLParser. Ponieważ pozostałe metody umieszczają swoje zrekonstruowane kawałki HTML-a w self.pieces, ta funkcja jest potrzebna, aby połączyć wszystkie te kawałki w jeden napis. Gdyż jak już wspomniano wcześniej Python jest świetny w obsłudze list i z reguły mierny w obsłudze napisów, kompletny napis wyjściowy tworzony jest tylko wtedy, gdy ktoś o to wyraźnie poprosi.

Materiały dodatkowe

edytuj

Przypisy

  1. Powodem dla którego Python jest lepszy w pracy z listami niż napisami, jest fakt iż listy są modyfikowalne (mutable), a napisy są niemodyfikowalne (immutable). Co oznacza, że zwiększeniem listy jest dodanie do niej po prostu nowego elementu i zaktualizowanie indeksu. Natomiast ponieważ napis nie może być zmieniony po utworzeniu, z reguły kod s = s + nowy utworzy całkowicie nowy napis powstały z połączenia oryginalnego napisu s i napisu nowy, a oryginalny napis zostanie zniszczony. To wymaga wielu kosztownych operacji zarządzania pamięcią, a wielkość zaangażowanego wysiłku rośnie wraz z długością napisu, a więc wykonywanie kodu s = s + nowy w pętli jest zabójcze. W terminologii technicznej dodanie n elementów do listy oznacza złożoność O(n), podczas gdy dodanie n elementów do napisu złożoność O(n2). Z drugiej strony Python korzysta z prostej optymalizacji, polegającej na tym, że jeśli dany łańcuch znaków posiada tylko jedno odwołanie, to nie tworzy nowego łańcucha, tylko rozszerza stary i akurat w tym przypadku byłoby nieco szybciej na łańcuchach znaków (wówczas złożoność byłaby O(n)).