W trakcie rozwijania i testowania, jak mi się wtedy wydawało, finalnej wersji HTMLBeans, światło dzienne ujrzała biblioteka HTMLCleaner 2.1, będąca odpowiednikiem JTidy. Temat zainteresował mnie na tyle, iż postanowiłem zorganizować tzw. “parse off”, czyli małe zawody parser’ów HTML, żeby porównać, czy pachnący świeżością HTMLCleaner istotnie jest lepszy od wiekowego już nieco JTidy.
W skład zawodów wchodzi seria testów, badająca:
- łatwość integracji w aplikacji (import bibliotek, łatwość użycia, 1 pkt.);
- szybkość porządkowania i parsowania kodu HTML do DOM (2 pkt.);
- dokładność i użyteczność wygenerowanego modelu DOM (2 pkt.);
Przejdźmy zatem do rzeczy.
Runda 1: Integracja z aplikacją.
W swoich codziennych zmaganiach z kodem Java’y używam Maven’a 2 do budowania projektów oraz odpowiednio Eclipse lub NetBeans jako edytorów końcowych. W tym przypadku użyję połączenia NetBeans+Maven 2, ponieważ aby zaimportować projekt Maven’a do NetBeans nie trzeba żadnych dodatkowych kroków (poza instalacją odpowieniego plugin’a).
Pierwsze wrażenia z intergracji obu bibliotek z samym projektem: HTMLCleaner nie istnieje w repozytoriach Maven’a, co faktycznie utrudniło integrację. Żeby dołączyć bibliotekę poprzez Maven’a, musiałem stworzyć na szybko “ściemnionego” POM’a i umieścić go w lokalnym repozytorium Maven’a na komputerze. W przypadku JTidy nie było problemów, ponieważ egzystuje sobie ono radośnie w repo Maven’a w grupie: org.hibernate. Punkt dla JTidy.
Kolejnym etapem jest łatwość implemetacji, czyli: ile trzeba się namęczyć, aby mając plik wejściowy (w naszym przypadku, ale możemy użyć także dowolnej innej implementacji rozszerzającej klasę abstrakcyjną java.io.InputStream) dokonać przeszukania za pomocą XPath. Dla uproszczenia używać będę najprostszych możliwych sposób uzyskania pożądanych rezultatów.
W przypadku HTMLCleaner’a ograniczamy się do (zakładam, że mamy zdefiniowaną i zainicjalizowaną zmienną input typu java.io.FileInputStream oraz zmienna xpathExpr typu java.lang.String zawierającą jakieś zapytanie XPath):
1 2 3 4 5 6 7 8 | //instancjonujemy nowy obiekt HTMLCleaner'a org.htmlcleaner.HTMLCleaner cleaner = new org.htmlcleaner.HTMLCleaner(); //dokonujemy czyszczenia i parsowania org.htmlcleaner.TagNode node = cleaner.clean(input); //wykonujemy przykładowe zapytanie XPath Object[] xpathResult = node.evaluateXPath(xpathExpr); |
Jak widać: są zalety oraz wady. Zaletą jest ilość kodu potrzebna, aby wykonać wyszukiwanie: w najprostszym przypadku zaledwie 3 linijki. Wadą natomiast jest zwracany rezultat. Tablica zmiennych typu java.lang.Object raczej nie powie nam za wiele o tym, co możemy otrzymać, a sam JavaDoc nawet nie wspomina nic o tym, czego możemy się w tym miejscu spodziewać. Wspomina natomiast o ograniczeniach zastosowanego parsera XPath, co także działa na niekorzyść HTMLCleaner’a. Według mnie raczej kiepskie rozwiązanie, ale przejdźmy teraz do JTidy (założenia na temat zmiennych input i xpathExpr podobne do tych z HTMLCleaner’a):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | //instancjonujemy nowy obiekt JTidy org.w3c.tidy.Tidy tidy = new org.w3c.tidy.Tidy(); //wyciszamy JTidy, żeby nie śmiecił nam komunikatami na konsoli tidy.setQuite(true); tidy.setShowWarnings(true); tidy.setShowErrors(0); //dokonujemy czyszczenia i parsowania org.w3c.dom.Document parseDOM = tidy.parseDOM(input, null); //tworzymy i kompilujemy nowe wyrażenie XPath javax.xml.xpath.XPathExpression xpath = javax.xml.xpath.XPathFactory.newInstance().newXPath().compile(xpathExpr); //wykonujemy zapytanie XPath String evaluate = xpath.evaluate(parseDOM); |
Jak widać JTidy też wcale nie jest ciężkie w użyciu, gdyż potrzeba tylko c linijkę więcej aby wykonać zapytanie XPath. Jeżeli jednak chcemy czyste środowisko wykonania, musimy “wyciszyć” JTidy trzema dodatkowymi instrukcjami. Wynikiem samego parsowania jest org.w3c.Document, który – będąc interfejsem ogólnie dostępnym w każdej implementacji Java’y – jest wykorzystywany także w innych bibliotekach (a co za tym idzie: łatwiejsza z nimi integracja, w porówaniu do HTMLCleaner’a, który używa własnych typów). Poza tym JTidy jako wynik zapytania XPath zwraca nam java.lang.String, czyli łańcuch znaków, które zawarte są między tag’ami sprecyzowanymi w samym zapytaniu. Jest to zaleta w przypadku, gdy mamy bardzo ściśle określone zapytania XPath lub wada w przypadku, gdy wynik chcemy poddać dalszej obróbce (ponownemu parsowaniu lub kolejnemu zapytaniu XPath). W rzeczywistości jednak zapytania XPath są ściśle sprecyzowane, więc JTidy pomimo jednej linijki więcej, zwraca nam jednoznaczy, łatwy w użyciu rezultat. Punkt dla JTidy.
Podsumowując: w kwestii łatwości implementacji i integracji z innymi rozwiązaniami prawie jednoznacznie prowadzi JTidy. Wszystko jest intuicyjne i jasno określone, bez użycia typów generycznych. HTMLCleaner z kolei wymaga o jedną linijkę mniej kodu oraz użycia (przynajmniej pozornie) mniejszej liczby typów, zawodzi jednak w przypadku bardziej zaawansowanych zapytań XPath. Nie wiemy też jakież to konkretnie obiekty zwraca nam metoda evaluateXPath(). Rundę I kończymy zwycięstwem JTidy – 2:0 (punktacja wewnętrzna danej rundy). Nie mogę jednak powiedzieć, żeby to zwycięstwo nie pozostawiało wątpliwości, przejdźmy więc dalej.
Runda 2: Wydajność parsowania.
Runda druga to wydajność, czyli jeden z ważniejszych czynników podczas programowania (dla nas, maniaków szybkości, poza poprawnością kodu, czasami nawet najistotniejsza). Test polegał na prostym uruchomieniu parsowania paru przykładowych plików:
- prostych plików HTML i XHTML (po jednym na rodzaj);
- skomplikowanych plikow HTML i XHTML (także po jednym na rodzaj);
Przebieg testu był prosty:
- Stworzenie obiektów Tidy i HtmlCleaner oraz obiektu XPathExpression na początku, żeby skoncentrować się na pomiarze czasu samego wyszukiwania i parsowania rezultatu zapytania XPath.
- Uruchomienie 10000 razy parsowania poszczególnych plików najpierw pierwszą, potem drugą biblioteką, zbierając przy tym minimalne, maksymalne i średnie czasy przetwarzania (w nanosekundach).
- Wyświetlenie rezultatów.
Wyniki prezentują się następująco (czasy dla pojedyńczych wywołań, podane w milisekundach):
| Plik | HtmlCleaner | JTidy | ||||
|---|---|---|---|---|---|---|
| Czasy: | Min. | Maks. | Średni | Min. | Maks. | Średni |
| easy_test.html – prosty plik HTML | 0,1124 | 25,0544 | 0,2166 | 0,3651 | 45,5050 | 0,5831 |
| easy_test.xhtml – prosty plik XHTML | 0,0937 | 48,5305 | 0,2626 | —* | —* | —* |
| hard_test.html – złożony plik HTML** | 0,2691 | 5,0093 | 0,3002 | 0,5792 | 45,3211 | 0,7178 |
| hard_test.xhtml – złożony plik XHTML** | 0,2854 | 2,0102 | 0,3075 | —* | —* | —* |
* – JTidy nie przeżyło testów XHTML’a, ponieważ w momencie, kiedy na początku dokumentu pojawi się znacznik XML’a: <?xml … ?>, JTidy wyrzuca spektakularne ArrayIndexOutOfBoundsException: -1;
** – plik tak naprawdę nie był aż tak bardzo złożony, ale na pewno był bardziej skomplikowany, co widać po dłuższych czasach wykonania;
Patrząc na czasy maksymalne, można by dojść do wniosku, iż HtmlCleaner bardziej lubi skomplikowane pliki, na szczęście średnie czasy pokazują, że nie jest to prawda. Przyznam szczerze, że nie takich rezultatów się spodziewałem. O ile znana mi była wcześniej ułomność JTidy, jeżeli chodzi o XHTML, o tyle spodziewałem się, że jako ta nieco dokładniejsza biblioteka, będzie też wydajniejsza. Okazało się jednak, iż w wydajności, praktycznie na każdym etapie prowadzi HtmlCleaner ze średnimi czasami ponad 2 razy mniejszymi niż JTidy. Poza tym JTidy nie sprawdzi się w miejscach, gdzie spodziewać się możemy XHTML’a.
Podsumowując: wydajnościowo wygrywa HTMLCleaner – 4:0. Czas na ostani test, czyli…
Runda 3: Poprawność parsowania.
Przetestowaliśmy już łatwość implementacji oraz prędkość parsowania. O ile do tej pory HTMLCleaner prowadzi 2:1 (tak, jak pisałem na początku, I runda – 1 pkt., II runda – 2 pkt.), o tyle ostatnia runda zadecyduje o wszystkim. Zajmiemy się poprawnością utworzonego modelu DOM oraz sposobem interpretacji błędów HTML/XHTML przez obie biblioteki.
W ramach testu sprawdzimy najczęściej występujące w dokumentach HTML błędy, czyli:
- niedomknięte tagi;
- zagubione sekwencje tagów (czyli otworzenie taga a, potem b i nie zamknięcie ich w odpowiedniej kolejności);
- nieprawidłowe rozmieszczenie tagów (czyli umieszczenie w tagu a, taga b, którego umieścić tam nie można);
Test został przeprowadzony na domyślnych ustawieniach obu bibliotek, a rezultaty są dość interesujące. W przypadku pierwszego testu sekwencja:
1 | <tr><td><font size="1">Unclosed tags</td></tr> |
została przez obydwie biblioteki zamieniona na poprawny ciąg:
1 | <tr><td><font size="1">Unclosed tags</font></td></tr> |
Drugi test – zagubione sekwencje tagów – także nie sprawił tym bibliotekom problemu, ponieważ z sekwencji:
1 | <tr><td>Misplaces tags</tr></td> |
obie poprawnie utworzyły kod:
1 | <tr><td>Misplaces tags</td></tr> |
Natomiast z trzecim testem było najciekawiej. Uogólniając: obie biblioteki poradziły sobie z tym przypadkiem całkiem nieźle, jednak każda nieco inaczej. Mając ciąg:
1 2 3 | <tr><td><font size="2"> <table><tr><td>Wrong nested tags</td></tr></table> </font></td></tr> |
HtmlCleaner zwrócił następujący kod:
1 2 3 | <tr><td><font size="2"> </font><font size="2" /><table><tbody><tr><td>Wrong nested tags</td></tr></tbody></table> </td></tr> |
JTidy natomiast zwróciło:
1 2 3 4 5 6 7 8 9 | <tr> <td> <table> <tr> <td>Wrong nested tags</td> </tr> </table> </td> </tr> |
Jak więc widać, żaden z rezultatów nie jest do końca poprawny. HtmlCleaner dokonał “rozszczepienia” źle zagnieżdżonych tagów, kopiując przy okacji niepotrzebnie tag font. JTidy natomiast w ogóle usunął tag font i zostawił tylko jego zawartość.
Jako że obie biblioteki zachowały mniej więcej treść kodu HTML, który parsowały, a zmiany których dokonały w samym kodzie to już umowna kwestia interpretacji (czyli komu wygodniej użyć danych rezultatów), rundę uznaję za remis, czyli podział punktów.
Podsumowanie
| Runda (test) | Punkty | |
|---|---|---|
| Punkty: | HtmlCleaner | JTidy |
| Runda I (integracja i implementacja) | 0 | 1 |
| Runda II (wydajność) | 2 | 0 |
| Runda III (poprawność) | 1 | 1 |
| W sumie | 3 | 2 |
Minimalne zwycięstwo HtmlCleaner’a. Jak widać każda biblioteka ma swoje plusy i minusy, więc w gestii czytelnika pozostawiam już decyzję której biblioteki użyć. Wiecie dokładnie czego się po nich spodziewać. Kod, którego użyłem do stworzenia tego artykułu możecie znaleźć tutaj. Jest to gotowy do zbudowania projekt w Maven 2 oraz ściemniona struktura maven’owa dla HtmlCleaner’a (którego, jak wcześniej wspomniałem, nie ma w repozytoriach).
Have fun!


1/ – Oba przypadki z sa zle.
- JTidy pominelo taga (robia np aplikacje zamieniajaca wszystkie upiekszacze HTML’a do CSS pomieniecie moze byc “klopotliwe”).
- HtmlCleaner poradzil sobie znacznie lepiej. Powielil taga co nie jest fajne, ale az tak zle tez nie jest. Dane dotyczace wielkosci fonta mamy, to jest plus.
2/ – Testy na pojedynczych plikach sa fajne… i malo uzyteczne. Oczywiscie sa aplikacji, w ktorych bedziemy wczytywali pojedyncze pliki, ale jaka bylaby wydajnosc w przetwarzaniu powiedzmy 100 plikow? TEST!
3/ – Wielkosc plikow tez ma znaczenie dla parserow/cleanerow/magikow od HTML. TEST!
Osobiscie zaczne uzywac HTMLCleaner’a
Oj tam, San0, czepiasz się…
1. Oczywiście, żadna z nich nie poradziła sobie w 100%, ale chodziło tutaj głównie o pokazanie czego potencjalny użytkownik danej biblioteki może się spodziewać. W sumie obie biblioteki nie są doskonałe.
2. 1, 10 czy 100 plików i tak w obu bibliotekach musisz je przetwarzać pojedyńczo, nie ma żadnej masówki. Z tego punktu widzenia w każdym teście przetworzonych zostało po 10000 plików (a raczej po 10 tyś ten sam plik).
3. Owszem, ma, ale w teście szybkości chodziło o to, która biblioteka zrobi to szybciej, a nie jaka będzie ich ogólna wydajność. Sam wydajność zależeć będzie od wielu czynników: struktury dokumentu, poziomu zagnieżdżenia, liczby użytych atrubutów, liczby będów w HTML’u, etc., etc…
Szczerze mówiąc: HtmlCleaner też mi po tym teście jakoś bardziej podchodzi. Chociaż ten ograniczony XPath…
Całkiem niezle Ci idzie ‘sprzedawanie’ swojej wiedzy.
)
Pozdrow.
dev_friendly_tester (pewnie wiesz kto
Jakże mógłbym nie wiedzieć?
Co do tego ‘sprzedawania’… żebym ja jescze coś z tego miał…
(W sumie to mam satysfakcję dzielenia się wiedzą, no i oczywiście świadomość, że ktoś autentycznie to czyta… xD).
Pozdrowienia.