Версия для печати темы
Нажмите сюда для просмотра этой темы в оригинальном формате
Форум программистов > Java: Общие вопросы > HTML Parser HTMLEditorKit


Автор: Platon 20.9.2008, 13:11
Здравствуйте, уважаемые.

Java HTML Parser: Введение

Очередное утро - очередная задача - очередная порция новой информации - очередной FAQ.
На этот раз взбрело мне в голову ковыряться в содержимом html документа не с помощью RegExp, хотя возможно он был бы менее ресурсозатратным(?), а с помощью более подходящего для этих целей инструмента HTMLEditorKit
Сразу оговорю, чем HTMLEditorKit выгодно отличается, например, от того же http://forum.vingrad.ru/faq/topic-158003.html, он может работать с неправильным html кодом, т.е. скрадывает ошибки, допущенные верстальщиком при написании html кода.

HTMLEditorKit  сравнил с SAXParser не спроста. Дело в том, что оба этих инструмента используют похожую модель обработки данных. Так, что если вы знакомы с SAXParser, то вам не составит труда познать таинства HTMLEditorKit.

HTMLEditorKit поставляется с jdk5 и используется для разбора html кода в компоненте JEditorPane

HTMLEditorKit работает со стандартным DTD (HTML докуметами), но также может принять и пользовательский DTD, тем самым становится мощным и гибким инструментом для работы с частично неправильными xml документами

Уверенным программистам рекомендую почитать информацию, предоставленную Sun http://java.sun.com/products/jfc/tsc/articles/bookmarks/

Java HTML Parser: Задача.

Из содержимого html документа извлечь все ссылки(часто встречаемая в быту).

Java HTML Parser: Теоретическая часть.

Сердцем всего HTML парсера является класс HTMLEditorKit.ParserCallback окинем взглядом его методы:
Код

    public void flush() throws BadLocationException
    public void handleText(char[] data, int pos)
    public void handleComment(char[] data, int pos)
    public void handleStartTag(HTML.Tag t, 
                               MutableAttributeSet a, int pos)
    public void handleEndTag(HTML.Tag t, int pos)
    public void handleSimpleTag(HTML.Tag t, 
                                MutableAttributeSet a, int pos)
    public void handleError(String errorMsg, int pos)
    public void handleEndOfLineString(String eol)


flush() - как и обычный потоковый flush(), этот метод сигнализирует нам, что парсинг HTML документа закончен и необходимо завершить все вычисления, слить все промежуточные данные.
handleText(char[] data, int pos) - обработка текста <a href="...">текст</a> <p>текст</p> и т.д. data содержит текст, pos позиция в документе
handleComment(char[] data, int pos) - обработка встречаемых комментариев. data содержит текст, pos позиция в документе
handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) - обработка открытия тега, пример <a href="test.html" rel="nofollow"> t - тег (в нашем примере HTML.Tag.A), a содержит набор атрибутов (в нашем случае HTML.Attribute.HREF и HTML.Attribute.REL)
handleEndTag(HTML.Tag t, int pos) - сигнализатор закрытия тега
handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) - обработка тега, не предусматривающего закрытие (одиночки), пример <img src="img.png" border="0" />, t - тег (в нашем примере HTML.Tag.IMG), a содержит набор атрибутов (в нашем случае HTML.Attribute.SRC и HTML.Attribute.BORDER)
handleError(String errorMsg, int pos) - обрабатывается ошибка, пример "end.missing div ? ?", pos - позиция в документе, где определена ошибка
handleEndOfLineString(String eol) - обработка новой строки в документе.

Есть пара моментов для уточнения:
1. handleText и handleEndOfLineString. handleText получает в качестве параметра символьный массив, в котором последовательность так называемых Whitespace символов заменяется на 1 пробельный символ. handleEndOfLineString поможет нам обработать перенос строки, склеив с текстом, а также поможет вести статистику строка/позиция, а не позиция.
2. handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos). Интересен тут следующий факт: обычно в передаваемом объекте MutableAttributeSet содержатся ссылки на объекты HTML.Attribute, такие как HTML.Attribute.HREF, HTML.Attribute.SRC, ... если в html документе встречаются недокументированные атрибуты, они передаются в MutableAttributeSet как строки, т.е. <a href="test.html" myattr="Доктор Хаус уже на экранах!">, в данном случае MutableAttributeSet будет содержать 2 атрибута HTTP.Attribute.HREF@4564(href) => "test.html" и String@4565(myattr) => "Доктор Хаус уже на экранах!"

Java HTML Parser: Основная часть.


Засекаем секундомеры.
LinkCollector.java
Код

package ru.vingrad.platon.htmlparser;

import javax.swing.text.MutableAttributeSet;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.parser.ParserDelegator;
import javax.swing.text.BadLocationException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;

public class LinkCollector extends HTMLEditorKit.ParserCallback {

    private Link currentLink;

    private LinkObjectStream objectStream;


    public LinkCollector(LinkObjectStream objectStream) {
        this.objectStream = objectStream;
    }

    public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) {
        if (HTML.Tag.A == t)
            currentLink = new Link("", (String)a.getAttribute(HTML.Attribute.HREF), (String)a.getAttribute(HTML.Attribute.ALT),
                    (String)a.getAttribute(HTML.Attribute.TITLE), isNoFollow(a.getAttribute(HTML.Attribute.REL)));
    }

    private static boolean isNoFollow(Object attribute) {
        return ("nofollow".equals(attribute));
    }


    public void handleEndTag(HTML.Tag t, int pos) {
        if (HTML.Tag.A == t) {
            objectStream.write(currentLink);
            currentLink = null;
        }
    }

    public void handleText(char[] data, int pos) {
        if (currentLink != null)
            currentLink.text += new String(data);
    }

    public void flush() throws BadLocationException {
        if (currentLink != null) {
            objectStream.write(currentLink);
            currentLink = null;
        }
    }

    public static void main(String[] args) throws IOException {
        new ParserDelegator().parse(new InputStreamReader(new URL("http://vingrad.ru").openStream()), new LinkCollector(new ConsoleStream()), true);
    }
}

LinkObjectStream.java
Код

package ru.vingrad.platon.htmlparser;

public interface LinkObjectStream {
    void write(Link currentLink);
}

Link.java
Код

package ru.vingrad.platon.htmlparser;

public class Link {
    public String text;
    public String path;
    public String alt;
    public String title;
    public boolean noFollow;


    public Link(String text, String path, String alt, String title, boolean noFollow) {
        this.text = text;
        this.path = path;
        this.alt = alt;
        this.title = title;
        this.noFollow = noFollow;
    }


    public Link() {
    }
}


ConsoleStream.java
Код

package ru.vingrad.platon.htmlparser;

public class ConsoleStream implements LinkObjectStream {

    public void write(Link currentLink) {
        System.out.println("Link: " + currentLink.path + "; description: " + currentLink.text);
    }
}


LinkCollector - сердце нашей программы, в мы определяем поведение и состояние сборки ссылок.
3 метода делают всю работу:

Код

    public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) {
        if (HTML.Tag.A == t)
            currentLink = new Link("", (String)a.getAttribute(HTML.Attribute.HREF), (String)a.getAttribute(HTML.Attribute.ALT),
                    (String)a.getAttribute(HTML.Attribute.TITLE), isNoFollow(a.getAttribute(HTML.Attribute.REL)));
    }

    public void handleEndTag(HTML.Tag t, int pos) {
        if (HTML.Tag.A == t) {
            objectStream.write(currentLink);
            currentLink = null;
        }
    }

    public void handleText(char[] data, int pos) {
        if (currentLink != null)
            currentLink.text += new String(data);
    }

Сначала мы создаем новую ссылку(Link), устанавливаем доступные нам атрибуты, за исключением текста, он пока остается пустым. затем, если будет текст внутри ссылки(в html коде), то вызывается метод обработки текста handleText и присваивается к тексту объекта текущей ссылки(Link). Как только мы получаем команду закрытия тега (а она будет, даже если тег в HTML тексте не прописан), мы сливаем объект ссылки (Link) высокоуровневому обработчику LinkObjectStream, или его конкретной реализации ConsoleStream.

Результат работы программы над винградовской главной страницей:

Цитата

Link: /index.php; description: 
Link: /reg.php; description: Регистрация
Link: ; description: Восстановить пароль
Link: /index.php; description: ГЛАВНАЯ
Link: /shop.php; description: МАГАЗИН
Link: http://forum.vingrad.ru; description: ФОРУМ ПРОГРАММИСТОВ
Link: http://base.vingrad.ru; description: БАЗА ЗНАНИЙ
Link: /list.php?section=15; description: Менеджер проектов (8)
Link: /list.php?section=16; description: Менеджер по продажам (2)
Link: /list.php?section=17; description: Арт-директор (2)
Link: /list.php?section=18; description: Менеджер по персоналу (1)
Link: /list.php?section=19; description: Менеджмент проектов (8)
Link: /list.php?section=20; description: Сайт "под ключ" (17)
Link: /list.php?section=21; description: Копирайтинг (3)
Link: /list.php?section=22; description: Дизайн сайтов (10)
Link: /list.php?section=23; description: Верстка (14)
Link: /list.php?section=24; description: Веб-программирование (27)
Link: /list.php?section=25; description: Системы администрирования (CMS) (11)
Link: /list.php?section=26; description: QA (тестирование) (1)
Link: /list.php?section=27; description: Wap/PDA-сайты (1)
Link: /list.php?section=28; description: Флеш-сайты (4)
Link: /list.php?section=29; description: Логотипы (3)
Link: /list.php?section=30; description: Фирменный стиль (2)
Link: /list.php?section=31; description: Баннеры (2)
Link: /list.php?section=32; description: Рисунки и иллюстрации (6)
Link: /list.php?section=33; description: Технический дизайн (2)
Link: /list.php?section=34; description: Иконки (4)
Link: /list.php?section=35; description: Анимация (4)
Link: /list.php?section=36; description: Полиграфия (1)
Link: /list.php?section=37; description: Дизайн упаковки (1)
Link: /list.php?section=38; description: Наружная реклама (2)
Link: /list.php?section=39; description: Дизайн выставочных стендов (0)
Link: /list.php?section=40; description: Презентации (2)
Link: /list.php?section=41; description: Дизайн интерьеров (0)
Link: /list.php?section=42; description: Ландшафтный дизайн (0)
Link: /list.php?section=43; description: Интерфейсы (0)
Link: /list.php?section=44; description: Промышленный дизайн (0)
Link: /list.php?section=45; description: Разработка шрифтов (0)
Link: /list.php?section=46; description: Пиксел-арт (0)
Link: /list.php?section=47; description: Комиксы (1)
Link: /list.php?section=48; description: Чертежи (0)
Link: /list.php?section=49; description: Полиграфическая верстка (1)
Link: /list.php?section=50; description: 2D Персонажи (2)
Link: /list.php?section=51; description: Векторная графика (4)
Link: /list.php?section=52; description: Предпечатная подготовка (0)
Link: /list.php?section=53; description: Архитектура/Инжиниринг (1)
Link: /list.php?section=54; description: Флеш-сайты (3)
Link: /list.php?section=55; description: Флеш-графика (5)
Link: /list.php?section=56; description: Анимация (4)
Link: /list.php?section=57; description: Flash/Flex-программирование (5)
Link: /list.php?section=58; description: Баннеры (3)
Link: /list.php?section=59; description: Веб-программирование (27)
Link: /list.php?section=60; description: Базы данных (32)
Link: /list.php?section=61; description: Прикладное программирование (44)
Link: /list.php?section=62; description: Системное программирование (21)
Link: /list.php?section=63; description: 1С-программирование (2)
<...>


http://sespider.ru

Автор: Platon 23.9.2008, 14:45
Также часто встречаемое решение по сбору ссылок в документе:

Код

import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;

public class FullDocumentParser {
    static HTMLDocument doc = null;
    public static void main(String[] args) throws IOException {

        HTMLEditorKit kit = new HTMLEditorKit();
        URL ura = new URL("http://vingrad.ru");

        try {
            InputStream in = ura.openStream();
            doc = (HTMLDocument) kit.createDefaultDocument();
            doc.putProperty("IgnoreCharsetDirective",Boolean.TRUE);
            kit.read(in, doc, 0);
        } catch (BadLocationException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        }

        //Получение атрибутов тега <A>
        HTMLDocument.Iterator it = doc.getIterator(HTML.Tag.A);
        while (it.isValid()) {
            AttributeSet attrs = it.getAttributes();
            Object linkAttr = attrs.getAttribute(HTML.Attribute.HREF);
            System.out.println(linkAttr);
            it.next();
        }
    }
}


Но этот подход не может обрабатывать ссылки "на лету", т.е. документ придется загрузить полностью + этот подход генерирует всё дерево тегов в документе, что отразится на увеличении потребляемой памяти.

Автор: unkis 23.9.2008, 19:09
Есть один вопрос, что по вашему мнению будет работать быстрее, парсинье страниц с помощью RegEx или же с помощью HTMLEditorKit?

Автор: Platon 24.9.2008, 15:25
Цитата(Platon @  20.9.2008,  14:11 Найти цитируемый пост)
RegExp, хотя возможно он был бы менее ресурсозатратным(?)

Я об этом упомянул, честно сказать не проверял.

Но тут дело даже в другом - представьте себе такую схему: <body><a href="">test me</body>, просто напросто забыл закрыть тег a, в итоге, ваше регулярное выражение аля <a.*?href="(.*?)".*?>(.+?)</a> уже не сможет учесть эту ссылку. А HTMLEditorKit как раз-таки сможет.

Проверил быстродействие, да HTMLEditorKit проигрывает по скорости в 2-3 раза. Его средний замер 350, средний замер регулярного выражения 125.


Автор: fotig 25.9.2008, 04:17
LinkCollector.java в процессе компиляции выдает сообщение:

--------------------Configuration: <Default>--------------------
C:\LinkCollector.java:45: cannot find symbol
symbol  : class BadLocationException
location: class ru.vingrad.platon.htmlparser.LinkCollector
    public void flush() throws BadLocationException {
                               ^
1 error

Process completed.


Автор: nerezus 25.9.2008, 08:20
Цитата

Есть один вопрос, что по вашему мнению будет работать быстрее, парсинье страниц с помощью RegEx или же с помощью HTMLEditorKit?
 Алгоритм на State Machine(типа указанного здесь). Т.к. он будет работать, а решение на регекспах не будет. Следовательно он быстрее, ибо нен с кем соревноваться в скорости  ;)

Автор: LSD 25.9.2008, 12:07
Цитата(fotig @  25.9.2008,  05:17 Найти цитируемый пост)
LinkCollector.java в процессе компиляции выдает сообщение:

Какая версия JDK используется? Чтобы скомпилировать этот код нужна 1.5 и старше.

Автор: fotig 25.9.2008, 13:49
Цитата(LSD @  25.9.2008,  12:07 Найти цитируемый пост)
Какая версия JDK используется? Чтобы скомпилировать этот код нужна 1.5 и старше. 


компиляция делается на JCreator 4 c JDK 1.6.0_07

Автор: Platon 25.9.2008, 14:01
LSD, уважаемый, ну что ж вы пособничаете. Я писал письмо, чтоб все посты fotig в отдельную тему отцепить :(

Автор: LSD 25.9.2008, 16:54
Цитата(Platon @ 25.9.2008,  15:01)
LSD, уважаемый, ну что ж вы пособничаете. Я писал письмо, чтоб все посты fotig в отдельную тему отцепить :(

Ну прям чуствую себя преступником smile

Для того обсуждение статьи и сделано, чтобы ее можно было улучшить или исправить ошибки в ней. В данном случае в статье действительно есть ошибка, в классе LinkCollector нехватает импорта для javax.swing.text.BadLocationException.

Автор: armixx 6.9.2010, 10:43
Всем добрый день.
Уважаемый Platon.
А можно ли с помощью описанных выше методов решить следующую проблему:
JEditorPane при выводе HTML-документа не выделяет ссылки. Так же в режиме 
setEditable(false) отсутствует клавиатурная навигация.
Я бы мог делать это сам, если бы знал соответствие начальную и конечную CaretPosition расположения текста ссылок в JEditorPane.
Подозреваю, что решение где-то рядом, но моего опыта общения с JEditorPane, увы, пока не хватает.
Или эту проблему можно решить как-то иначе?
Документ в JEditorPane я загружаю так:
       
        ByteArrayInputStream bas=new ByteArrayInputStream(br);
        ep.read(bas,new HTMLDocument());
        ew.setEditable(false);

Powered by Invision Power Board (http://www.invisionboard.com)
© Invision Power Services (http://www.invisionpower.com)