zoukankan      html  css  js  c++  java
  • htmlparser

    htmlparser使用指南

    需要做一个垂直搜索引擎,比较了nekohtml和htmlparser 的功能,尽管nekohtml在容错性、性能等方面的口碑好像比htmlparser好(htmlunit也用的是nekohtml),但感觉 nekohtml的测试用例和文档都比htmlparser都少,而且htmlparser基本上能够满足垂直搜索引擎页面处理分析的需求,因此先研究一 下htmlparser的使用,有空再研究nekohtml和mozilla html parser的使用。

        html的功能还是官方说得最为清楚,

        HTML Parser is a Java library used to parse HTML in either a linear or nested fashion. Primarily used for transformation or extraction, it features filters, visitors, custom tags and easy to use JavaBeans. It is a fast, robust and well tested package.

        The two fundamental use-cases that are handled by the parser are extraction and transformation (the syntheses use-case, where HTML pages are created from scratch, is better handled by other tools closer to the source of data). While prior versions concentrated on data extraction from web pages, Version 1.4 of the HTMLParser has substantial improvements in the area of transforming web pages, with simplified tag creation and editing, and verbatim toHtml() method output.

        研究的重点还是extraction的使用,有空再研究transformation的使用。

    1、htmlparser对html页面处理的数据结构

    如图所示,HtmlParser采用了经典的Composite模式,通过RemarkNode、TextNode、TagNode、AbstractNode和Tag来描述HTML页面各元素。

    • org.htmlparser.Node:

        Node接口定义了进行树形结构节点操作的各种典型操作方法,包括:

        节点到html文本、text文本的方法:toPlainTextString、toHtml

       典型树形结构遍历的方法:getParent、getChildren、getFirstChild、getLastChild、getPreviousSibling、getNextSibling、getText

        获取节点对应的树形结构结构的顶级节点Page对象方法:getPage

        获取节点起始位置的方法:getStartPosition、getEndPosition

       Visitor方法遍历节点时候方法:accept (NodeVisitor visitor)

        Filter方法:collectInto (NodeList list, NodeFilter filter)

        Object方法:toString、clone

    • org.htmlparser.nodes.AbstractNode

        AbstractNode是形成HTML树形结构抽象基类,实现了Node接口。

        在htmlparser中,Node分成三类:

        RemarkNode:代表Html中的注释

        TagNode:标签节点。

        TextNode:文本节点

        这三类节点都继承AbstractNode。

    • org.htmlparser.nodes.TagNode:

        TagNode包含了对HTML处理的核心的各个类,是所有TAG的基类,其中有分为包含其他TAG的复合节点ComositeTag和不包含其他TAG的叶子节点Tag。

        复合节点CompositeTag:   

            AppletTag,BodyTag,Bullet,BulletList,DefinitionList,DefinitionListBullet,Div,FormTag,FrameSetTag,HeadingTag,

            HeadTag,Html,LabelTag,LinkTag,ObjectTag,ParagraphTag,ScriptTag,SelectTag,Span,StyleTag,TableColumn,

           TableHeader,TableRow,TableTag,TextareaTag,TitleTag

        叶子节点TAG:

            BaseHrefTag,DoctypeTag,FrameTag,ImageTag,InputTag,JspTag,MetaTag,ProcessingInstructionTag,

    2、htmlparser对html页面处理的算法

    主要是如下几种方式

    • 采用Visitor方式访问Html

    try {
        Parser parser = new Parser();
        parser.setURL(”http://www.google.com")/;
        parser.setEncoding(parser.getEncoding());
        NodeVisitor visitor = new NodeVisitor() {
            public void visitTag(Tag tag) {
                logger.fatal(”testVisitorAll()  Tag name is :”
                        + tag.getTagName() + ” Class is :”
                        + tag.getClass());
            }

        };

        parser.visitAllNodesWith(visitor);
    } catch (ParserException e) {
        e.printStackTrace();
    }

    • 采用Filter方式访问html

    try {

        NodeFilter filter = new NodeClassFilter(LinkTag.class);
        Parser parser = new Parser();
        parser.setURL(”http://www.google.com")/;
        parser.setEncoding(parser.getEncoding());
        NodeList list = parser.extractAllNodesThatMatch(filter);
        for (int i = 0; i < list.size(); i++) {
            LinkTag node = (LinkTag) list.elementAt(i);
            logger.fatal(”testLinkTag() Link is :” + node.extractLink());
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

    • 采用org.htmlparser.beans方式

    另外htmlparser 还在org.htmlparser.beans中对一些常用的方法进行了封装,以简化操作,例如:

    Parser parser = new Parser();

    LinkBean linkBean = new LinkBean();
    linkBean.setURL(”http://www.google.com")/;
    URL[] urls = linkBean.getLinks();

    for (int i = 0; i < urls.length; i++) {
        URL url = urls[i];
        logger.fatal(”testLinkBean() -url  is :” + url);
    }

    3、htmlparser关键包结构说明

        htmlparser其实核心代码并不多,好好研究一下其代码,弥补文档不足的问题。同时htmlparser的代码注释和单元测试用例还是很齐全的,也有助于了解htmlparser的用法。

     

    3.1、org.htmlparser

        定义了htmlparser的一些基础类。其中最为重要的是Parser类。

        Parser是htmlparser的最核心的类,其构造函数提供了如下:Parser.createParser (String html, String charset)、 Parser ()、Parser (Lexer lexer, ParserFeedback fb)、Parser (URLConnection connection, ParserFeedback fb)、Parser (String resource, ParserFeedback feedback)、 Parser (String resource)

      各构造函数的具体用法及含义可以查看其代码,很容易理解。

      Parser常用的几个方法:

    •   elements获取元素

        Parser parser = new Parser (”http://www.google.com")/;
        for (NodeIterator i = parser.elements (); i.hasMoreElements (); )
          processMyNodes (i.nextNode ());

    • parse (NodeFilter filter):通过NodeFilter方式获取
    • visitAllNodesWith (NodeVisitor visitor):通过Nodevisitor方式
    • extractAllNodesThatMatch (NodeFilter filter):通过NodeFilter方式

    3.2、org.htmlparser.beans

        对Visitor和Filter的方法进行了封装,定义了针对一些常用html元素操作的bean,简化对常用元素的提取操作。

        包括:FilterBean、HTMLLinkBean、HTMLTextBean、LinkBean、StringBean、BeanyBaby等。

    3.3、org.htmlparser.nodes

        定义了基础的node,包括:AbstractNode、RemarkNode、TagNode、TextNode等。

    3.4、org.htmlparser.tags

        定义了htmlparser的各种tag。

    3.5、org.htmlparser.filters

        定义了htmlparser所提供的各种filter,主要通过extractAllNodesThatMatch (NodeFilter filter)来对html页面指定类型的元素进行过滤,包括:AndFilter、CssSelectorNodeFilter、 HasAttributeFilter、HasChildFilter、HasParentFilter、HasSiblingFilter、 IsEqualFilter、LinkRegexFilter、LinkStringFilter、NodeClassFilter、 NotFilter、OrFilter、RegexFilter、StringFilter、TagNameFilter、XorFilter

    3.6、org.htmlparser.visitors

       定义了htmlparser所提供的各种visitor,主要通过visitAllNodesWith (NodeVisitor visitor)来对html页面元素进行遍历,包括:HtmlPage、LinkFindingVisitor、NodeVisitor、 ObjectFindingVisitor、StringFindingVisitor、TagFindingVisitor、 TextExtractingVisitor、UrlModifyingVisitor

    3.7、org.htmlparser.parserapplications

       定义了一些实用的工具,包括LinkExtractor、SiteCapturer、StringExtractor、WikiCapturer,这几个类也可以作为htmlparser使用样例。

    3.8、org.htmlparser.tests

       对各种功能的单元测试用例,也可以作为htmlparser使用的样例。

    4、htmlparser的使用样例

    import java.net.URL;

    import junit.framework.TestCase;

    import org.apache.log4j.Logger;
    import org.htmlparser.Node;
    import org.htmlparser.NodeFilter;
    import org.htmlparser.Parser;
    import org.htmlparser.Tag;
    import org.htmlparser.beans.LinkBean;
    import org.htmlparser.filters.NodeClassFilter;
    import org.htmlparser.filters.OrFilter;
    import org.htmlparser.filters.TagNameFilter;
    import org.htmlparser.tags.HeadTag;
    import org.htmlparser.tags.ImageTag;
    import org.htmlparser.tags.InputTag;
    import org.htmlparser.tags.LinkTag;
    import org.htmlparser.tags.OptionTag;
    import org.htmlparser.tags.SelectTag;
    import org.htmlparser.tags.TableColumn;
    import org.htmlparser.tags.TableRow;
    import org.htmlparser.tags.TableTag;
    import org.htmlparser.tags.TitleTag;
    import org.htmlparser.util.NodeIterator;
    import org.htmlparser.util.NodeList;
    import org.htmlparser.util.ParserException;
    import org.htmlparser.visitors.HtmlPage;
    import org.htmlparser.visitors.NodeVisitor;
    import org.htmlparser.visitors.ObjectFindingVisitor;

    public class ParserTestCase extends TestCase {

        private static final Logger logger = Logger.getLogger(ParserTestCase.class);

        public ParserTestCase(String name) {
            super(name);
        }
        /*
         * 测试ObjectFindVisitor的用法
         */
        public void testImageVisitor() {
            try {
                ImageTag imgLink;
                ObjectFindingVisitor visitor = new ObjectFindingVisitor(
                        ImageTag.class);
                Parser parser = new Parser();
                parser.setURL(”http://www.google.com")/;
                parser.setEncoding(parser.getEncoding());
                parser.visitAllNodesWith(visitor);
                Node[] nodes = visitor.getTags();
                for (int i = 0; i < nodes.length; i++) {
                    imgLink = (ImageTag) nodes[i];
                    logger.fatal(”testImageVisitor() ImageURL = “
                            + imgLink.getImageURL());
                    logger.fatal(”testImageVisitor() ImageLocation = “
                            + imgLink.extractImageLocn());
                    logger.fatal(”testImageVisitor() SRC = “
                            + imgLink.getAttribute(”SRC”));
                }
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }
        /*
         * 测试TagNameFilter用法
         */
        public void testNodeFilter() {
            try {
                NodeFilter filter = new TagNameFilter(”IMG”);
                Parser parser = new Parser();
                parser.setURL(”http://www.google.com")/;
                parser.setEncoding(parser.getEncoding());
                NodeList list = parser.extractAllNodesThatMatch(filter);
                for (int i = 0; i < list.size(); i++) {
                    logger.fatal(”testNodeFilter() ” + list.elementAt(i).toHtml());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
        /*
         * 测试NodeClassFilter用法
         */
        public void testLinkTag() {
            try {

                NodeFilter filter = new NodeClassFilter(LinkTag.class);
                Parser parser = new Parser();
                parser.setURL(”http://www.google.com")/;
                parser.setEncoding(parser.getEncoding());
                NodeList list = parser.extractAllNodesThatMatch(filter);
                for (int i = 0; i < list.size(); i++) {
                    LinkTag node = (LinkTag) list.elementAt(i);
                    logger.fatal(”testLinkTag() Link is :” + node.extractLink());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
        /*
         * 测试<link href=” text=’text/css’ rel=’stylesheet’ />用法
         */
        public void testLinkCSS() {
            try {

                Parser parser = new Parser();
                parser
                        .setInputHTML(”<head><title>Link Test</title>”
                                + “<link href=’/test01/css.css’ text=’text/css’ rel=’stylesheet’ />”
                                + “<link href=’/test02/css.css’ text=’text/css’ rel=’stylesheet’ />”
                                + “</head>” + “<body>”);
                parser.setEncoding(parser.getEncoding());
                NodeList nodeList = null;

                for (NodeIterator e = parser.elements(); e.hasMoreNodes();) {
                    Node node = e.nextNode();
                    logger
                            .fatal(”testLinkCSS()” + node.getText()
                                    + node.getClass());

                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        /*
         * 测试OrFilter的用法
         */
        public void testOrFilter() {
            NodeFilter inputFilter = new NodeClassFilter(InputTag.class);
            NodeFilter selectFilter = new NodeClassFilter(SelectTag.class);
            Parser myParser;
            NodeList nodeList = null;

            try {
                Parser parser = new Parser();
                parser
                        .setInputHTML(”<head><title>OrFilter Test</title>”
                                + “<link href=’/test01/css.css’ text=’text/css’ rel=’stylesheet’ />”
                                + “<link href=’/test02/css.css’ text=’text/css’ rel=’stylesheet’ />”
                                + “</head>”
                                + “<body>”
                                + “<input type=’text’ value=’text1′ name=’text1′/>”
                                + “<input type=’text’ value=’text2′ name=’text2′/>”
                                + “<select><option id=’1′>1</option><option id=’2′>2</option><option id=’3′></option></select>”
                                + “<a href=’http://www.yeeach.com'/>yeeach.com</a>”
                                + “</body>”);

                parser.setEncoding(parser.getEncoding());
                OrFilter lastFilter = new OrFilter();
                lastFilter.setPredicates(new NodeFilter[] { selectFilter,
                        inputFilter });
                nodeList = parser.parse(lastFilter);
                for (int i = 0; i <= nodeList.size(); i++) {
                    if (nodeList.elementAt(i) instanceof InputTag) {
                        InputTag tag = (InputTag) nodeList.elementAt(i);
                        logger.fatal(”OrFilter tag name is :” + tag.getTagName()
                                + ” ,tag value is:” + tag.getAttribute(”value”));
                    }
                    if (nodeList.elementAt(i) instanceof SelectTag) {
                        SelectTag tag = (SelectTag) nodeList.elementAt(i);
                        NodeList list = tag.getChildren();

                        for (int j = 0; j < list.size(); j++) {
                            OptionTag option = (OptionTag) list.elementAt(j);
                            logger
                                    .fatal(”OrFilter Option”
                                            + option.getOptionText());
                        }

                    }
                }

            } catch (ParserException e) {
                e.printStackTrace();
            }
        }
        /*
         * 测试对<table><tr><td></td></tr></table>的解析
         */
        public void testTable() {
            Parser myParser;
            NodeList nodeList = null;
            myParser = Parser.createParser(”<body> ” + “<table id=’table1′ >”
                    + “<tr><td>1-11</td><td>1-12</td><td>1-13</td>”
                    + “<tr><td>1-21</td><td>1-22</td><td>1-23</td>”
                    + “<tr><td>1-31</td><td>1-32</td><td>1-33</td></table>”
                    + “<table id=’table2′ >”
                    + “<tr><td>2-11</td><td>2-12</td><td>2-13</td>”
                    + “<tr><td>2-21</td><td>2-22</td><td>2-23</td>”
                    + “<tr><td>2-31</td><td>2-32</td><td>2-33</td></table>”
                    + “</body>”, “GBK”);
            NodeFilter tableFilter = new NodeClassFilter(TableTag.class);
            OrFilter lastFilter = new OrFilter();
            lastFilter.setPredicates(new NodeFilter[] { tableFilter });
            try {
                nodeList = myParser.parse(lastFilter);
                for (int i = 0; i <= nodeList.size(); i++) {
                    if (nodeList.elementAt(i) instanceof TableTag) {
                        TableTag tag = (TableTag) nodeList.elementAt(i);
                        TableRow[] rows = tag.getRows();

                        for (int j = 0; j < rows.length; j++) {
                            TableRow tr = (TableRow) rows[j];
                            TableColumn[] td = tr.getColumns();
                            for (int k = 0; k < td.length; k++) {
                                logger.fatal(”<td>” + td[k].toPlainTextString());
                            }

                        }

                    }
                }

            } catch (ParserException e) {
                e.printStackTrace();
            }
        }
        /*
         * 测试NodeVisitor的用法,遍历所有节点
         */
        public void testVisitorAll() {
            try {
                Parser parser = new Parser();
                parser.setURL(”http://www.google.com")/;
                parser.setEncoding(parser.getEncoding());
                NodeVisitor visitor = new NodeVisitor() {
                    public void visitTag(Tag tag) {
                        logger.fatal(”testVisitorAll()  Tag name is :”
                                + tag.getTagName() + ” Class is :”
                                + tag.getClass());
                    }

                };

                parser.visitAllNodesWith(visitor);
            } catch (ParserException e) {
                e.printStackTrace();
            }
        }
        /*
         * 测试对指定Tag的NodeVisitor的用法
         */
        public void testTagVisitor() {
            try {

                Parser parser = new Parser(
                        “<head><title>dddd</title>”
                                + “<link href=’/test01/css.css’ text=’text/css’ rel=’stylesheet’ />”
                                + “<link href=’/test02/css.css’ text=’text/css’ rel=’stylesheet’ />”
                                + “</head>” + “<body>”
                                + “<a href=’http://www.yeeach.com'/>yeeach.com</a>”
                                + “</body>”);
                NodeVisitor visitor = new NodeVisitor() {
                    public void visitTag(Tag tag) {
                        if (tag instanceof HeadTag) {
                            logger.fatal(”visitTag() HeadTag : Tag name is :”
                                    + tag.getTagName() + ” Class is :”
                                    + tag.getClass() + “ Text is :”
                                    + tag.getText());
                        } else if (tag instanceof TitleTag) {
                            logger.fatal(”visitTag() TitleTag : Tag name is :”
                                    + tag.getTagName() + ” Class is :”
                                    + tag.getClass() + “ Text is :”
                                    + tag.getText());

                        } else if (tag instanceof LinkTag) {
                            logger.fatal(”visitTag() LinkTag : Tag name is :”
                                    + tag.getTagName() + ” Class is :”
                                    + tag.getClass() + “ Text is :”
                                    + tag.getText() + ” getAttribute is :”
                                    + tag.getAttribute(”href”));
                        } else {
                            logger.fatal(”visitTag() : Tag name is :”
                                    + tag.getTagName() + ” Class is :”
                                    + tag.getClass() + “ Text is :”
                                    + tag.getText());
                        }

                    }

                };

                parser.visitAllNodesWith(visitor);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        /*
         * 测试HtmlPage的用法
         */
        public void testHtmlPage() {
            String inputHTML = “<html>” + “<head>”
                    + “<title>Welcome to the HTMLParser website</title>”
                    + “</head>” + “<body>” + “Welcome to HTMLParser”
                    + “<table id=’table1′ >”
                    + “<tr><td>1-11</td><td>1-12</td><td>1-13</td>”
                    + “<tr><td>1-21</td><td>1-22</td><td>1-23</td>”
                    + “<tr><td>1-31</td><td>1-32</td><td>1-33</td></table>”
                    + “<table id=’table2′ >”
                    + “<tr><td>2-11</td><td>2-12</td><td>2-13</td>”
                    + “<tr><td>2-21</td><td>2-22</td><td>2-23</td>”
                    + “<tr><td>2-31</td><td>2-32</td><td>2-33</td></table>”
                    + “</body>” + “</html>”;
            Parser parser = new Parser();
            try {
                parser.setInputHTML(inputHTML);
                parser.setEncoding(parser.getURL());
                HtmlPage page = new HtmlPage(parser);
                parser.visitAllNodesWith(page);
                logger.fatal(”testHtmlPage -title is :” + page.getTitle());
                NodeList list = page.getBody();

                for (NodeIterator iterator = list.elements(); iterator
                        .hasMoreNodes();) {
                    Node node = iterator.nextNode();
                    logger.fatal(”testHtmlPage -node  is :” + node.toHtml());
                }

            } catch (ParserException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        /*
         * 测试LinkBean的用法
         */
        public void testLinkBean() {
            Parser parser = new Parser();

            LinkBean linkBean = new LinkBean();
            linkBean.setURL(”http://www.google.com")/;
            URL[] urls = linkBean.getLinks();

            for (int i = 0; i < urls.length; i++) {
                URL url = urls[i];
                logger.fatal(”testLinkBean() -url  is :” + url);
            }

        }

    }

    5、相关的项目

    nekohtml :评价比htmlparser好,把html正规化标准的xml文档,用xerces处理,但文档较少。

    mozilla htmlparserhttp://www.dapper.net/网站采用的html解析器,开源了,基于mozilla的解析器,值得研究一下。

    http://jerichohtml.sourceforge.net/

    http://htmlcleaner.sourceforge.net/

    http://html.xamjwg.org/cobra.jsp

    http://jrex.mozdev.org/

    https://xhtmlrenderer.dev.java.net/

    其他一些html parser可以参考相关的汇总文章:

    http://www.manageability.org/blog/stuff/screen-scraping-tools-written-in-java/view

    http://java-source.net/open-source/html-parsers

    http://www.open-open.com/30.htm

    6、参考文档

    http://www.blogjava.net/lostfire/archive/2006/07/02/56212.html

    http://blog.csdn.net/scud/archive/2005/08/11/451397.aspx

    http://chasethedevil.blogspot.com/2006/05/java-html-parsing-example-with.html

    http://javaboutique.internet.com/tutorials/HTMLParser/

    HTMLParser使用

    from @汀芷,  感谢作者!

    HTMLParser具有小巧,快速的优点,缺点是相关文档比较少(英文的也少),很多功能需要自己摸索。对于初学者还是要费一些功夫的,而一旦上手以后,会发现HTMLParser的结构设计很巧妙,非常实用,基本你的各种需求都可以满足。
        这里我根据自己这几个月来的经验,写了一点入门的东西,希望能对新学习HTMLParser的朋友们有所帮助。(不过当年高考本人语文只比及格高一分,所以文法方面的问题还希望大家多多担待)
        
        HTMLParser的核心模块是org.htmlparser.Parser类,这个类实际完成了对于HTML页面的分析工作。这个类有下面几个构造函数:
        public Parser ();
        public Parser (Lexer lexer, ParserFeedback fb);
       public Parser (URLConnection connection, ParserFeedback fb) throws ParserException;
        public Parser (String resource, ParserFeedback feedback) throws ParserException;
       public Parser (String resource) throws ParserException;
        public Parser (Lexer lexer);
        public Parser (URLConnection connection) throws ParserException;
        和一个静态类public static Parser createParser (String html, String charset);

        对于大多数使用者来说,使用最多的是通过一个URLConnection或者一个保存有网页内容的字符串来初始化Parser,或者使用静态函数来生成一个Parser对象。ParserFeedback的代码很简单,是针对调试和跟踪分析过程的,一般不需要改变。而使用Lexer则是一个相对比较高级的话题,放到以后再讨论吧。
        这里比较有趣的一点是,如果需要设置页面的编码方式的话,不使用Lexer就只有静态函数一个方法了。对于大多数中文页面来说,好像这是应该用得比较多的一个方法。

       下面是初始化Parser的例子。


    package com.baizeju.htmlparsertester;
    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    import java.io.FileInputStream;
    import java.io.File;
    import java.net.HttpURLConnection;
    import java.net.URL;

    import org.htmlparser.visitors.TextExtractingVisitor;

    import org.htmlparser.Parser;

    /**
    * @author www.baizeju.com
    */
    public class Main {
        private static String ENCODE = "GBK";
        private static void message( String szMsg ) {
            try{System.out.println(new String(szMsg.getBytes(ENCODE), System.getProperty("file.encoding"))); } catch(Exception e ){}
        }
        public static String openFile( String szFileName ) {
            try {
                BufferedReader bis = new BufferedReader(new InputStreamReader(new FileInputStream( new File(szFileName)), ENCODE) );
                String szContent="";
                String szTemp;
                
                while ( (szTemp = bis.readLine()) != null) {
                    szContent+=szTemp+" ";
                }
                bis.close();
                return szContent;
            }
            catch( Exception e ) {
                return "";
            }
        }
        
       public static void main(String[] args) {
            
            String szContent = openFile( "E:/My Sites/HTMLParserTester.html");
            
            try{
                //Parser parser = Parser.createParser(szContent, ENCODE);
                //Parser parser = new Parser( szContent );
              
     Parser parser = new Parser( (HttpURLConnection) (new URL("http://127.0.0.1:8080/HTMLParserTester.html")).openConnection() );
            
                TextExtractingVisitor visitor = new TextExtractingVisitor();
                parser.visitAllNodesWith(visitor);
                String textInPage = visitor.getExtractedText();

                message(textInPage);
            }
            catch( Exception e ) {            
            }
        }
    }
    加重的部分测试了几种不同的初始化方法,后面的显示了结果。大家看到能Parser出内容就可以了,如何操作访问Parser的内容我们在后面讨论。

    HTMLParser将解析过的信息保存为一个树的结构。Node是信息保存的数据类型基础。
    请看Node的定义:
    public interface Node extends Cloneable;

    Node中包含的方法有几类:
    对于树型结构进行遍历的函数,这些函数最容易理解:
    Node getParent ():取得父节点
    NodeList getChildren ():取得子节点的列表
    Node getFirstChild ():取得第一个子节点
    Node getLastChild ():取得最后一个子节点
    Node getPreviousSibling ():取得前一个兄弟(不好意思,英文是兄弟姐妹,直译太麻烦而且不符合习惯,对不起女同胞了)
    Node getNextSibling ():取得下一个兄弟节点
    取得Node内容的函数
    String getText ():取得文本
    String toPlainTextString():取得纯文本信息。
    String toHtml () :取得HTML信息(原始HTML)
    String toHtml (boolean verbatim):取得HTML信息(原始HTML)
    String toString ():取得字符串信息(原始HTML)
    Page getPage ():取得这个Node对应的Page对象
    int getStartPosition ():取得这个Node在HTML页面中的起始位置
    int getEndPosition ():取得这个Node在HTML页面中的结束位置
    用于Filter过滤的函数:
    void collectInto (NodeList list, NodeFilter filter):基于filter的条件对于这个节点进行过滤,符合条件的节点放到list中。
    用于Visitor遍历的函数:
    void accept (NodeVisitor visitor):对这个Node应用visitor
    用于修改内容的函数,这类用得比较少
    void setPage (Page page):设置这个Node对应的Page对象
    void setText (String text):设置文本
    void setChildren (NodeList children):设置子节点列表
    其他函数
    void doSemanticAction ():执行这个Node对应的操作(只有少数Tag有对应的操作)
    Object clone ():接口Clone的抽象函数。

    实际我们用HTMLParser最多的是处理HTML页面,Filter或Visitor相关的函数是必须的,然后第一类和第二类函数是用得最多的。第一类函数比较容易理解,下面用例子说明一下第二类函数。
    下面是用于测试的HTML文件:
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>白泽居-www.baizeju.com</title></head>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <body >
    <div id="top_main">
        <div id="logoindex">
            <!--这是注释-->
            白泽居-www.baizeju.com
    <a href="http://www.baizeju.com">白泽居-www.baizeju.com</a>
        </div>
        白泽居-www.baizeju.com
    </div>
    </body>
    </html>

    测试代码:
    /**
    * @author www.baizeju.com
    */

    package com.baizeju.htmlparsertester;
    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    import java.io.FileInputStream;
    import java.io.File;
    import java.net.HttpURLConnection;
    import java.net.URL;

    import org.htmlparser.Node;
    import org.htmlparser.util.NodeIterator;
    import org.htmlparser.Parser;

    /**
    * @author www.baizeju.com
    */
    public class Main {
        private static String ENCODE = "GBK";
        private static void message( String szMsg ) {
            try{ System.out.println(new String(szMsg.getBytes(ENCODE), System.getProperty("file.encoding"))); }     catch(Exception e ){}
        }
        public static String openFile( String szFileName ) {
            try {
                BufferedReader bis = new BufferedReader(new InputStreamReader(new FileInputStream( new File(szFileName)),    ENCODE) );
                String szContent="";
                String szTemp;
                
                while ( (szTemp = bis.readLine()) != null) {
                    szContent+=szTemp+" ";
                }
                bis.close();
                return szContent;
            }
            catch( Exception e ) {
                return "";
            }
        }
        
       public static void main(String[] args) {
            
            try{
                Parser parser = new Parser( (HttpURLConnection) (new URL("http://127.0.0.1:8080/HTMLParserTester.html")).openConnection() );
            
                for (NodeIterator i = parser.elements (); i.hasMoreNodes(); ) {
                    Node node = i.nextNode();
                    message("getText:"+node.getText());
                    message("getPlainText:"+node.toPlainTextString());
                    message("toHtml:"+node.toHtml());
                    message("toHtml(true):"+node.toHtml(true));
                    message("toHtml(false):"+node.toHtml(false));
                    message("toString:"+node.toString());
                    message("=================================================");
                }            
            }
            catch( Exception e ) {     
                System.out.println( "Exception:"+e );
            }
        }
    }

    输出结果:
    getText:!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
    getPlainText:
    toHtml:<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    toHtml(true):<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    toHtml(false):<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    toString:Doctype Tag : !DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd; begins at : 0; ends at : 121
    =================================================
    getText:

    getPlainText:

    toHtml:

    toHtml(true):

    toHtml(false):

    toString:Txt (121[0,121],123[1,0]):
    =================================================
    getText:head
    getPlainText:白泽居-www.baizeju.com
    toHtml:<head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>白泽居-www.baizeju.com</title></head>
    toHtml(true):<head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>白泽居-www.baizeju.com</title></head>
    toHtml(false):<head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>白泽居-www.baizeju.com</title></head>
    toString:HEAD: Tag (123[1,0],129[1,6]): head
    Tag (129[1,6],197[1,74]): meta http-equiv="Content-Type" content="text/html; ...
    Tag (197[1,74],204[1,81]): title
        Txt (204[1,81],223[1,100]): 白泽居-www.baizeju.com
        End (223[1,100],231[1,108]): /title
    End (231[1,108],238[1,115]): /head

    =================================================
    getText:

    getPlainText:

    toHtml:

    toHtml(true):

    toHtml(false):

    toString:Txt (238[1,115],240[2,0]):
    =================================================
    getText:html xmlns="http://www.w3.org/1999/xhtml"
    getPlainText:


            
                    
                    白泽居-www.baizeju.com
    白泽居-www.baizeju.com
            
            白泽居-www.baizeju.com



    toHtml:<html xmlns="http://www.w3.org/1999/xhtml">
    <body >
    <div id="top_main">
            <div id="logoindex">
                    <!--这是注释-->
                    白泽居-www.baizeju.com
    <a href="http://www.baizeju.com">白泽居-www.baizeju.com</a>
            </div>
            白泽居-www.baizeju.com
    </div>
    </body>
    </html>
    toHtml(true):<html xmlns="http://www.w3.org/1999/xhtml">
    <body >
    <div id="top_main">
            <div id="logoindex">
                    <!--这是注释-->
                    白泽居-www.baizeju.com
    <a href="http://www.baizeju.com">白泽居-www.baizeju.com</a>
            </div>
            白泽居-www.baizeju.com
    </div>
    </body>
    </html>
    toHtml(false):<html xmlns="http://www.w3.org/1999/xhtml">
    <body >
    <div id="top_main">
            <div id="logoindex">
                    <!--这是注释-->
                    白泽居-www.baizeju.com
    <a href="http://www.baizeju.com">白泽居-www.baizeju.com</a>
            </div>
            白泽居-www.baizeju.com
    </div>
    </body>
    </html>
    toString:Tag (240[2,0],283[2,43]): html xmlns="http://www.w3.org/1999/xhtml"
    Txt (283[2,43],285[3,0]):
    Tag (285[3,0],292[3,7]): body 
        Txt (292[3,7],294[4,0]):
        Tag (294[4,0],313[4,19]): div id="top_main"
          Txt (313[4,19],316[5,1]):
          Tag (316[5,1],336[5,21]): div id="logoindex"
            Txt (336[5,21],340[6,2]):
            Rem (340[6,2],351[6,13]): 这是注释
            Txt (351[6,13],376[8,0]): 白泽居-www.baizeju.com
            Tag (376[8,0],409[8,33]): a href="http://www.baizeju.com"
              Txt (409[8,33],428[8,52]): 白泽居-www.baizeju.com
              End (428[8,52],432[8,56]): /a
            Txt (432[8,56],435[9,1]):
            End (435[9,1],441[9,7]): /div
          Txt (441[9,7],465[11,0]): 白泽居-www.baizeju.com
          End (465[11,0],471[11,6]): /div
        Txt (471[11,6],473[12,0]):
        End (473[12,0],480[12,7]): /body
    Txt (480[12,7],482[13,0]):
    End (482[13,0],489[13,7]): /html

    =================================================


    对于第一个Node的内容,对应的就是第一行<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">,这个比较好理解。
    从这个输出结果中,也可以看出内容的树状结构。或者说是树林结构。在Page内容的第一层Tag,如DOCTYPE,head和html,分别形成了一个最高层的Node节点(很多人可能对第二个和第四个Node的内容有点奇怪。实际上这两个Node就是两个换行符号。HTMLParser把HTML页面内容中的所有换行,空格,Tab等都转换成了相应的Tag,所以就出现了这样的Node。虽然内容少但是级别高,呵呵)
    getPlainTextString是把用户可以看到的内容都包含了。有趣的有两点,一是<head>标签中的Title内容是在plainText中的,可能在标题中可见的也算可见吧。另外就是象前面说的,HTML内容中的换行符什么的,也都成了plainText,这个逻辑上好像有点问题。

    另外可能大家发现toHtml,toHtml(true)和toHtml(false)的结果没什么区别。实际也是这样的,如果跟踪HTMLParser的代码就可以发现,Node的子类是AbstractNode,其中实现了toHtml()的代码,直接调用toHtml(false),而AbstractNode的三个子类RemarkNode,TagNode和TextNode中,toHtml(boolean verbatim)的实现中,都没有处理verbatim参数,所以三个函数的结果是一模一样的。如果你不需要实现你自己的什么特殊处理,简单使用toHtml就可以了。

    HTML的Node类继承关系如下图(这个是从别的文章Copy的):

     

     

    AbstractNodes是Node的直接子类,也是一个抽象类。它的三个直接子类实现是RemarkNode,用于保存注释。在输出结果的toString部分中可以看到有一个"Rem (345[6,2],356[6,13]): 这是注释",就是一个RemarkNode。TextNode也很简单,就是用户可见的文字信息。TagNode是最复杂的,包含了HTML语言中的所有标签,而且可以扩展(扩展 HTMLParser 对自定义标签的处理能力)。TagNode包含两类,一类是简单的Tag,实际就是不能包含其他Tag的标签,只能做叶子节点。另一类是CompositeTag,就是可以包含其他Tag,是分支节点

    HTMLParser遍历了网页的内容以后,以树(森林)结构保存了结果。HTMLParser访问结果内容的方法有两种。使用Filter和使用Visitor。

    (一)Filter
    顾名思义,Filter就是对于结果进行过滤,取得需要的内容。HTMLParser在org.htmlparser.filters包之内一共定义了16个不同的Filter,也可以分为几类。
    判断类Filter
    TagNameFilter
    HasAttributeFilter
    HasChildFilter
    HasParentFilter
    HasSiblingFilter
    IsEqualFilter
    逻辑运算Filter
    AndFilter
    NotFilter
    OrFilter
    XorFilter
    其他Filter
    NodeClassFilter
    StringFilter
    LinkStringFilter
    LinkRegexFilter
    RegexFilter
    CssSelectorNodeFilter

    所有的Filter类都实现了org.htmlparser.NodeFilter接口。这个接口只有一个主要函数:
    boolean accept (Node node);
    各个子类分别实现这个函数,用于判断输入的Node是否符合这个Filter的过滤条件,如果符合,返回true,否则返回false。

    (二)判断类Filter
    2.1 TagNameFilter
    TabNameFilter是最容易理解的一个Filter,根据Tag的名字进行过滤。

    下面是用于测试的HTML文件:
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>白泽居-www.baizeju.com</title>< /head>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <body >
    <div id="top_main">
        <div id="logoindex">
            <!--这是注释-->
            白泽居-www.baizeju.com
    <a href="http://www.baizeju.com">白泽居-www.baizeju.com</a>
        </div>
        白泽居-www.baizeju.com
    </div>
    </body>
    </html>
    测试代码:(这里只列出了Main函数,全部代码请参考 HTMLParser使用入门(2)- Node内容,自己添加import部分)
    public static void main(String[] args) {
            
            try{
                Parser parser = new Parser( (HttpURLConnection) (new URL("http://127.0.0.1:8080/HTMLParserTester.html")).openConnection() );
            
                // 这里是控制测试的部分,后面的例子修改的就是这个地方。
                NodeFilter filter = new TagNameFilter ("DIV");
                NodeList nodes = parser.extractAllNodesThatMatch(filter);
     
                
                if(nodes!=null) {
                    for (int i = 0; i < nodes.size(); i++) {
                        Node textnode = (Node) nodes.elementAt(i);
                        
                        message("getText:"+textnode.getText());
                        message("=================================================");
                    }
                }            
            }
            catch( Exception e ) {     
                e.printStackTrace();
            }
        }
    输出结果:
    getText:div id="top_main"
    =================================================
    getText:div id="logoindex"
    =================================================
    可以看出文件中两个Div节点都被取出了。下面可以针对这两个DIV节点进行操作

    2.2 HasChildFilter
    下面让我们看看HasChildFilter。刚刚看到这个Filter的时候,我想当然地认为这个Filter返回的是有Child的Tag。直接初始化了一个
    NodeFilter filter = new HasChildFilter();
    结果调用NodeList nodes = parser.extractAllNodesThatMatch(filter);的时候HasChildFilter内部直接发生NullPointerException。读了一下HasChildFilter的代码,才发现,实际HasChildFilter是返回有符合条件的子节点的节点,需要另外一个Filter作为过滤子节点的参数。缺省的构造函数虽然可以初始化,但是由于子节点的Filter是null,所以使用的时候发生了Exception。从这点来看,HTMLParser的代码还有很多可以优化的的地方。呵呵。

    修改代码:
    NodeFilter innerFilter = new TagNameFilter ("DIV");
    NodeFilter filter = new HasChildFilter(innerFilter);
    NodeList nodes = parser.extractAllNodesThatMatch(filter);
    输出结果:
    getText:body 
    =================================================
    getText:div id="top_main"
    =================================================
    可以看到,输出的是两个有DIV子Tag的Tag节点。(body有子节点DIV "top_main","top_main"有子节点"logoindex"。

    注意HasChildFilter还有一个构造函数:
    public HasChildFilter (NodeFilter filter, boolean recursive)
    如果recursive是false,则只对第一级子节点进行过滤。比如前面的例子,body和top_main都是在第一级的子节点里就有DIV节点,所以匹配上了。如果我们用下面的方法调用:
    NodeFilter filter = new HasChildFilter( innerFilter, true );
    输出结果:
    getText:html xmlns="http://www.w3.org/1999/xhtml"
    =================================================
    getText:body 
    =================================================
    getText:div id="top_main"
    =================================================
    可以看到输出结果中多了一个html xmlns="http://www.w3.org/1999/xhtml",这个是整个HTML页面的节点(根节点),虽然这个节点下直接没有DIV节点,但是它的子节点body下面有DIV节点,所以它也被匹配上了。

    2.3 HasAttributeFilter
    HasAttributeFilter有3个构造函数:
    public HasAttributeFilter ();
    public HasAttributeFilter (String attribute);
    public HasAttributeFilter (String attribute, String value);
    这个Filter可以匹配出包含制定名字的属性,或者制定属性为指定值的节点。还是用例子说明比较容易。

    调用方法1:
    NodeFilter filter = new HasAttributeFilter();
    NodeList nodes = parser.extractAllNodesThatMatch(filter);
    输出结果:

    什么也没有输出。

    调用方法2:
    NodeFilter filter = new HasAttributeFilter( "id" );
    NodeList nodes = parser.extractAllNodesThatMatch(filter);
    输出结果:
    getText:div id="top_main"
    =================================================
    getText:div id="logoindex"
    =================================================

    调用方法3:
    NodeFilter filter = new HasAttributeFilter( "id", "logoindex" );
    NodeList nodes = parser.extractAllNodesThatMatch(filter);
    输出结果:
    getText:div id="logoindex"
    =================================================

    很简单吧。呵呵

    2.4 其他判断列Filter
    HasParentFilter和HasSiblingFilter的功能与HasChildFilter类似,大家自己试一下就应该了解了。

    IsEqualFilter的构造函数参数是一个Node:
    public IsEqualFilter (Node node) {
        mNode = node;
    }
    accept函数也很简单:
    public boolean accept (Node node)    {
        return (mNode == node);
    }
    不需要过多说明了。


    (三)逻辑运算Filter
    前面介绍的都是简单的Filter,只能针对某种单一类型的条件进行过滤。HTMLParser支持对于简单类型的Filter进行组合,从而实现复杂的条件。原理和一般编程语言的逻辑运算是一样的。
    3.1 AndFilter

    AndFilter可以把两种Filter进行组合,只有同时满足条件的Node才会被过滤。
    测试代码:
    NodeFilter filterID = new HasAttributeFilter( "id" );
    NodeFilter filterChild = new HasChildFilter(filterA);
    NodeFilter filter = new AndFilter(filterID, filterChild);
    输出结果:
    getText:div id="logoindex"
    =================================================

    3.2 OrFilter
    把前面的AndFilter换成OrFilter
    测试代码:
    NodeFilter filterID = new HasAttributeFilter( "id" );
    NodeFilter filterChild = new HasChildFilter(filterA);
    NodeFilter filter = new OrFilter(filterID, filterChild);
    输出结果:
    getText:div id="top_main"
    =================================================
    getText:div id="logoindex"
    =================================================

    3.3 NotFilter
    把前面的AndFilter换成NotFilter
    测试代码:
    NodeFilter filterID = new HasAttributeFilter( "id" );
    NodeFilter filterChild = new HasChildFilter(filterA);
    NodeFilter filter = new NotFilter(new OrFilter(filterID, filterChild));
    输出结果:
    getText:!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
    =================================================
    getText:

    =================================================
    getText:head
    =================================================
    getText:meta http-equiv="Content-Type" content="text/html; charset=gb2312"
    =================================================
    getText:title
    =================================================
    getText:白泽居-www.baizeju.com
    =================================================
    getText:/title
    =================================================
    getText:/head
    =================================================
    getText:

    =================================================
    getText:html xmlns="http://www.w3.org/1999/xhtml"
    =================================================
    getText:

    =================================================
    getText:body 
    =================================================
    getText:

    =================================================
    getText:
            
    =================================================
    getText:
                    
    =================================================
    getText:这是注释
    =================================================
    getText:
                    白泽居-www.baizeju.com

    =================================================
    getText:a href="http://www.baizeju.com"
    =================================================
    getText:白泽居-www.baizeju.com
    =================================================
    getText:/a
    =================================================
    getText:
            
    =================================================
    getText:/div
    =================================================
    getText:
            白泽居-www.baizeju.com

    =================================================
    getText:/div
    =================================================
    getText:

    =================================================
    getText:/body
    =================================================
    getText:

    =================================================
    getText:/html
    =================================================
    getText:

    =================================================

    除了前面3.2中输出的几个Tag,其余的Tag都在这里了。


    3.4 XorFilter
    把前面的AndFilter换成NotFilter
    测试代码:
    NodeFilter filterID = new HasAttributeFilter( "id" );
    NodeFilter filterChild = new HasChildFilter(filterA);
    NodeFilter filter = new XorFilter(filterID, filterChild);
    输出结果:
    getText:div id="top_main"
    =================================================

    (四)其他Filter
    4.1 NodeClassFilter
    这个Filter用于判断节点类型是否是某个特定的Node类型。在HTMLParser使用入门(2)- Node内容 中我们已经了解了Node的不同类型,这个Filter就可以针对类型进行过滤。
    测试代码:
               NodeFilter filter = new NodeClassFilter(RemarkNode.class);
                NodeList nodes = parser.extractAllNodesThatMatch(filter);
    输出结果:
    getText:这是注释
    =================================================
    可以看到只有RemarkNode(注释)被输出了。

    4.2 StringFilter
    这个Filter用于过滤显示字符串中包含制定内容的Tag。注意是可显示的字符串,不可显示的字符串中的内容(例如注释,链接等等)不会被显示。
    修改一下例子代码:
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>白泽居-title-www.baizeju.com</title></head>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <body >
    <div id="top_main">
        <div id="logoindex">
            <!--这是注释白泽居-www.baizeju.com -->
            白泽居-字符串1-www.baizeju.com
    <a href="http://www.baizeju.com">白泽居-链接文本-www.baizeju.com</a>
        </div>
        白泽居-字符串2-www.baizeju.com
    </div>
    </body>
    </html>

    测试代码:
               NodeFilter filter = new StringFilter("www.baizeju.com");
                NodeList nodes = parser.extractAllNodesThatMatch(filter);
    输出结果:
    getText:白泽居-title-www.baizeju.com
    =================================================
    getText:
                    白泽居-字符串1-www.baizeju.com

    =================================================
    getText:白泽居-链接文本-www.baizeju.com
    =================================================
    getText:
            白泽居-字符串2-www.baizeju.com

    =================================================
    可以看到包含title,两个内容字符串和链接的文本字符串的Tag都被输出了,但是注释和链接Tag本身没有输出。

    4.3 LinkStringFilter
    这个Filter用于判断链接中是否包含某个特定的字符串,可以用来过滤出指向某个特定网站的链接。
    测试代码:
               NodeFilter filter = new LinkStringFilter("www.baizeju.com");
                NodeList nodes = parser.extractAllNodesThatMatch(filter);
    输出结果:
    getText:a href="http://www.baizeju.com"
    =================================================

    4.4 其他几个Filter
    其他几个Filter也是根据字符串对不同的域进行判断,与前面这些的区别主要就是支持正则表达式。这个不在本文的讨论范围以内,大家可以自己实验一下。

    HTMLParser遍历了网页的内容以后,以树(森林)结构保存了结果。HTMLParser访问结果内容的方法有两种。使用Filter和使用Visitor。
    下面介绍使用Visitor访问内容的方法。

    4.1 NodeVisitor
    从简单方面的理解,Filter是根据某种条件过滤取出需要的Node再进行处理。Visitor则是遍历内容树的每一个节点,对于符合条件的节点进行处理。实际的结果异曲同工,两种不同的方法可以达到相同的结果。
    下面是一个最常见的NodeVisitro的例子。
    测试代码:
        public static void main(String[] args) {
            try{
                Parser parser = new Parser( (HttpURLConnection) (new URL("http://127.0.0.1:8080/HTMLParserTester.html")).openConnection() );

                NodeVisitor visitor = new NodeVisitor( false, false ) {
                    public void visitTag(Tag tag) {
                       message("This is Tag:"+tag.getText());
                    }
                    public void visitStringNode (Text string)    {
                         message("This is Text:"+string);
                    }
                    public void visitRemarkNode (Remark remark) {
                         message("This is Remark:"+remark.getText());
                    }
                    public void beginParsing () {
                        message("beginParsing");
                    }
                    public void visitEndTag (Tag tag){
                        message("visitEndTag:"+tag.getText());
                    }
                    public void finishedParsing () {
                        message("finishedParsing");
                    }
                };

                parser.visitAllNodesWith(visitor);
            }
            catch( Exception e ) {     
                e.printStackTrace();
            }
        }
    输出结果:
    beginParsing
    This is Tag:!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
    This is Text:Txt (121[0,121],123[1,0]):
    This is Text:Txt (244[1,121],246[2,0]):
    finishedParsing

    可以看到,开始遍历所以的节点以前,beginParsing先被调用,然后处理的是中间的Node,最后在结束遍历以前,finishParsing被调用。因为我设置的 recurseChildren和recurseSelf都是false,所以Visitor没有访问子节点也没有访问根节点的内容。中间输出的两个 就是我们在HTMLParser使用详解(1)- 初始化Parser 中讨论过的最高层的那两个换行。

    我们先把recurseSelf设置成true,看看会发生什么。
    NodeVisitor visitor = new NodeVisitor( false, true) {
    输出结果:
    beginParsing
    This is Tag:!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
    This is Text:Txt (121[0,121],123[1,0]):
    This is Tag:head
    This is Text:Txt (244[1,121],246[2,0]):
    This is Tag:html xmlns="http://www.w3.org/1999/xhtml"
    finishedParsing
    可以看到,HTML页面的第一层节点都被调用了。

    我们再用下面的方法调用看看:
    NodeVisitor visitor = new NodeVisitor( true, false) {
    输出结果:
    beginParsing
    This is Tag:!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
    This is Text:Txt (121[0,121],123[1,0]):
    This is Tag:meta http-equiv="Content-Type" content="text/html; charset=gb2312"
    This is Text:Txt (204[1,81],229[1,106]): 白泽居-title-www.baizeju.com
    visitEndTag:/title
    visitEndTag:/head
    This is Text:Txt (244[1,121],246[2,0]):
    This is Text:Txt (289[2,43],291[3,0]):
    This is Text:Txt (298[3,7],300[4,0]):
    This is Text:Txt (319[4,19],322[5,1]):
    This is Text:Txt (342[5,21],346[6,2]):
    This is Remark:这是注释白泽居-www.baizeju.com 
    This is Text:Txt (378[6,34],408[8,0]): 白泽居-字符串1-www.baizeju.com
    This is Text:Txt (441[8,33],465[8,57]): 白泽居-链接文本-www.baizeju.com
    visitEndTag:/a
    This is Text:Txt (469[8,61],472[9,1]):
    visitEndTag:/div
    This is Text:Txt (478[9,7],507[11,0]): 白泽居-字符串2-www.baizeju.com
    visitEndTag:/div
    This is Text:Txt (513[11,6],515[12,0]):
    visitEndTag:/body
    This is Text:Txt (522[12,7],524[13,0]):
    visitEndTag:/html
    finishedParsing
    可以看到,所有的子节点都出现了,除了刚刚例子里面的两个最上层节点This is Tag:head和This is Tag:html xmlns="http://www.w3.org/1999/xhtml"。

    想让它们都出来,只需要
    NodeVisitor visitor = new NodeVisitor( true, true) {
    输出结果:
    beginParsing
    This is Tag:!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
    This is Text:Txt (121[0,121],123[1,0]):
    This is Tag:head
    This is Tag:meta http-equiv="Content-Type" content="text/html; charset=gb2312"
    This is Tag:title
    This is Text:Txt (204[1,81],229[1,106]): 白泽居-title-www.baizeju.com
    visitEndTag:/title
    visitEndTag:/head
    This is Text:Txt (244[1,121],246[2,0]):
    This is Tag:html xmlns="http://www.w3.org/1999/xhtml"
    This is Text:Txt (289[2,43],291[3,0]):
    This is Tag:body 
    This is Text:Txt (298[3,7],300[4,0]):
    This is Tag:div id="top_main"
    This is Text:Txt (319[4,19],322[5,1]):
    This is Tag:div id="logoindex"
    This is Text:Txt (342[5,21],346[6,2]):
    This is Remark:这是注释白泽居-www.baizeju.com 
    This is Text:Txt (378[6,34],408[8,0]): 白泽居-字符串1-www.baizeju.com
    This is Tag:a href="http://www.baizeju.com"
    This is Text:Txt (441[8,33],465[8,57]): 白泽居-链接文本-www.baizeju.com
    visitEndTag:/a
    This is Text:Txt (469[8,61],472[9,1]):
    visitEndTag:/div
    This is Text:Txt (478[9,7],507[11,0]): 白泽居-字符串2-www.baizeju.com
    visitEndTag:/div
    This is Text:Txt (513[11,6],515[12,0]):
    visitEndTag:/body
    This is Text:Txt (522[12,7],524[13,0]):
    visitEndTag:/html
    finishedParsing
    哈哈,这下调用清楚了,大家在需要处理的地方增加自己的代码好了。


    4.2
    其他Visitor
    HTMLParser还定义了几个其他的Visitor。HtmlPage,NodeVisitor,ObjectFindingVisitor,StringFindingVisitor,TagFindingVisitor,TextExtractingVisitor,UrlModifyingVisitor,它们都是NodeVisitor的子类,实现了一些特定的功能。笔者个人的感觉是没什么用处,如果你需要什么特定的功能,还不如自己写一个,想在这些里面找到适合你需要的,化的时间可能更多。反正大家看看代码就发现,它们每个都没几行真正有效的代码。HTMLParser 是一个用来解析 HTML 文档的开放源码项目,它具有小巧、快速、使用简单的特点以及拥有强大的功能。对该项目还不了解的朋友可以参照 2004 年三月份我发表的文章--《从HTML中攫取你所需的信息》,这篇文章介绍如何通过 HTMLParser 来提取 HTML 文档中的文本数据以及提取出文档中的所有链接或者是图片等信息。

    现在该项目的最新版本是 Integration Build 1.6,与之前版本的差别在于代码结构的调整、当然也有一些功能的提升以及 BugFix,同时对字符集的处理也更加自动了。比较遗憾的该项目并没有详尽的使用文档,你只能借助于它的 API 文档、一两个简单例子以及源码来熟悉它。

    如果是 HTML 文档,那么用 HTMLParser 已经差不多可以满足你至少 90%的需求。一个 HTML 文档中可能出现的标签差不多在 HTMLParser 中都有对应的类,甚至包括一些动态的脚本标签,例如 <%...%> 这种 JSP 和 ASP 用到的标签都有相应的 JspTag 对应。HTMLParser 的强大功能还体现在你可以修改每个标签的属性或者它所包含的文本内容并生成新的 HTML 文档,比如你可以文档中的链接地址偷偷的改成你自己的地址等等。关于 HTMLParser 的强大功能,其实上一篇文章已经介绍很多,这里不再累赘,我们今天要讲的是另外一个用途--处理自定义标签。

    首先我们先解释一下什么叫自定义标签,我把所有不是 HTML 脚本语言中定义的标签称之为自定义标签,比如可以是 <scriptlet>、<book> 等等,这是我们自己创造出来的标签。你可能会很奇怪,因为这些标签一旦用在 HTML 文档中是没有任何效果的,那么我们换另外一个例子,假如你要解析的不是 HTML 文档,而是一个 WML(Wireless Markup Lauguage)文档呢?WML 文档中的 card,anchor 等标签 HTMLParser 是没有现成的标签类来处理的。还有就是你同样可以用 HTMLParser 来处理 XML 文档,而 XML 文档中所有的标签都是你自己定义的。

    为了使我们的例子更具有代表意义,接下来我们将给出一段代码用来解析出 WML 文档中的所有链接,了解 WML 文档的人都知道,WML 文档中除了与 HTML 文档相同的链接写法外,还多了一种标签叫 <anchor>,例如在一个 WML 文档我们可以用下面两种方式来表示一个链接。

     

    <a href="http://www.javayou.com?cat_id=1">Java自由人</a>

    或者:

    <anchor>

    Java自由人

        <go href="http://www.javayou.com" method="get">

            <postfield name="cat_id" value="1"/>

    </go>

    </anchor>

    (更多的时候使用 anchor 的链接用来提交一个表单。)如果我们还是使用 LinkTag 来遍历整个 WML 文档的话,那 Anchor 中的链接将会被我们所忽略掉。

    下面我们先给出一个简单的例子,然后再叙述其中的道理。这个例子包含两个文件,一个是WML 的测试脚本文件 test.wml,另外一个是 Java 程序文件 HyperLinkTrace.java,内容如下:

     


     

     

     

    1. test.wml

     

    <?xml version="1.0"?>

    <!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"

    "http://www.wapforum.org/DTD/wml_1.1.xml">

    <wml>

    <card title="Java自由人登录">

    <p> 

     用户名:<input type="text" name="username" size="15"/>

         密码:<input type="text" name="password" size="15"/>

     <br/>

     <anchor>现在登录

      <go href="/wap/user.do" method="get">

          <postfield name="name" value="$(username)"/>

          <postfield name="password" value="$(password)"/>

          <postfield name="eventSubmit_Login" value="WML"/>

      </go>

     </anchor><br/>

     <a href="/wap/index.vm">返回首页</a>

    </p>

    </card>

    </wml>

    test.wml 中的粗体部分是我们需要提取出来的链接。

     


     

     

     

    2. HyperLinkTrace.java

     

    package demo.htmlparser;

    import java.io.BufferedReader;

    import java.io.File;

    import java.io.FileReader;

    import java.net.URL;

    import org.htmlparser.Node;

    import org.htmlparser.NodeFilter;

    import org.htmlparser.Parser;

    import org.htmlparser.PrototypicalNodeFactory;

    import org.htmlparser.tags.CompositeTag;

    import org.htmlparser.tags.LinkTag;

    import org.htmlparser.util.NodeList;

    /**

     * 用来遍历WML文档中的所有超链接

     * @author Winter Lau

     */

    public class HyperLinkTrace {

     public static void main(String[] args) throws Exception {

      //初始化HTMLParser

      Parser parser = new Parser();

      parser.setEncoding("8859_1");

      parser.setInputHTML(getWmlContent());

     

      //注册新的结点解析器

      PrototypicalNodeFactory factory = new PrototypicalNodeFactory ();

      factory.registerTag(new WmlGoTag ());

      parser.setNodeFactory(factory);

      //遍历符合条件的所有节点

      NodeList nlist = parser.extractAllNodesThatMatch(lnkFilter);

      for(int i=0;i<nlist.size();i++){

       CompositeTag node = (CompositeTag)nlist.elementAt(i);

       if(node instanceof LinkTag){

        LinkTag link = (LinkTag)node;

        System.out.println("LINK: " + link.getLink());

       }

       else if(node instanceof WmlGoTag){

        WmlGoTag go = (WmlGoTag)node;

        System.out.println("GO: " + go.getLink());

       }

      }

     }

     /**

      * 获取测试的WML脚本内容

      * @return

      * @throws Exception

      */

     static String getWmlContent() throws Exception{

      URL url = ParserTester.class.getResource("/demo/htmlparser/test.wml");

      File f = new File(url.toURI());

      BufferedReader in = new BufferedReader(new FileReader(f));

      StringBuffer wml = new StringBuffer();

      do{

       String line = in.readLine();

       if(line==null)

        break;

       if(wml.length()>0)

        wml.append(" ");

       wml.append(line);  

      }while(true);

      return wml.toString(); 

     }

     /**

      * 解析出所有的链接,包括行为<a>与<go>

      */

     static NodeFilter lnkFilter = new NodeFilter() {

      public boolean accept(Node node) {

       if(node instanceof WmlGoTag)

        return true;

       if(node instanceof LinkTag)

        return true;

       return false;

      }

     };

     

     /**

      * WML文档的GO标签解析器

      * @author Winter Lau

      */

     static class WmlGoTag extends CompositeTag {

         private static final String[] mIds = new String[] {"GO"};

         private static final String[] mEndTagEnders = new String[] {"ANCHOR"};

         public String[] getIds (){

             return (mIds);

         }

         public String[] getEnders (){

             return (mIds);

         }

         public String[] getEndTagEnders (){

             return (mEndTagEnders);

         }

        

         public String getLink(){

          return super.getAttribute("href");

         }

        

         public String getMethod(){

          return super.getAttribute("method");

         }

     }

    }

    上面这段代码比较长,可以分成下面几部分来看:

    1. getWmlContent方法:该方法用来获取在同一个包中的test.wml脚本文件的内容并返回字符串。

    2. 静态属性lnkFilter:这是一个NodeFilter的匿名类所构造的实例。该实例用来传递给HTMLParser告知需要提取哪些节点。在这个例子中我们仅需要提取链接标签以及我们自定义的一个GO标签。

    3. 嵌套类WmlGoTag:这也是最为重要的一部分,这个类用来告诉HTMLParser如何去解析<go>这样一个节点。我们先看看下面这个HTMLParser的节点类层次图:


    如上图所示,HTMLParser将一个文档分成三种节点分别是:Remark(注释);Text(文本);Tag(标签)。而标签又分成两种分别是简单标签(Tag)和复合标签(CompositeTag),像<img><br/>这种标签称为简单标签,因为标签不会再包含其它内容。而像<a href="xxxx">Home</a>这种类型的标签,因为标签会嵌套文本或者其他标签的称为复合标签,也就是对应着CompositeTag这个类。简单标签的实现类很简单,只需要扩展Tag类并覆盖getIds方法以返回标签的识别文本,例如<img>标签应该返回包含"img"字符串的数组,具体的代码可以参考HTMLParser自带的ImageTag标签类的实现。

    从上图可清楚看出,复合标签事实上是对简单标签的扩展,HTMLParser在处理一个复合标签时需要知道该标签的起始标识以及结束标识,也就是我们在前面给出的源码中的两个方法getIds和getEnders,一般来讲,标签出现都是成对的,因此这两个方法一般返回相同的值。另外一个方法getEndTagEnders,这个方法用来返回父一级的标签名称,例如<tr>的父一级标签应该是<table>。这个方法的必要性在于HTML对格式的要求很不严格,在很多的HTML文档中的一些标签经常是有开始标识,但是没有结束标识,由于浏览器的超强适应能力使这种情况出现的很频繁,因此HTMLParser利用这个方法来辅助判断一个标签是否已经结束。由于WML文档的格式要求非常严格,因此上例源码中的getEndTagEnders方法事实上可有可无。

    4. 入口方法main:该方法初始化HTMLParser并注册新的节点解析器,解析文档并打印运行结果。

    最后我们编译并运行这个例子,便可以得到下面的运行结果:

     

    GO:  /wap/user.do

    LINK:  /wap/index.vm

    HTMLParser本身就是一个开放源码的项目,它对于HTML文档中出现的标签定义已经应有尽有,我们尽可以参考这些标签解析类的源码来学习如何实现一个标签的解析类,从而扩展出更丰富多彩的应用程序。

    使用 HttpClient 和 HtmlParser 实现简易爬虫

    这篇文章介绍了 HtmlParser 开源包和 HttpClient 开源包的使用,在此基础上实现了一个简易的网络爬虫 (Crawler),来说明如何使用 HtmlParser 根据需要处理 Internet 上的网页,以及如何使用 HttpClient 来简化 Get 和 Post 请求操作,构建强大的网络应用程序。

    蒋宏伟 (jianghongwei_tju@yahoo.com.cn), 在校学生, 天津大学计算机学院

    2008 年 9 月 11 日

    • +内容

    使用 HttpClient 和 HtmlParser 实现简易爬虫

    这 篇文章介绍了 HtmlParser 开源包和 HttpClient 开源包的使用,在此基础上实现了一个简易的网络爬虫 (Crawler),来说明如何使用 HtmlParser 根据需要处理 Internet 上的网页,以及如何使用 HttpClient 来简化 Get 和 Post 请求操作,构建强大的网络应用程序。

    HttpClient 与 HtmlParser 简介

    本小结简单的介绍一下 HttpClinet 和 HtmlParser 两个开源的项目,以及他们的网站和提供下载的地址。

    HttpClient 简介

    HTTP 协议是现在的因特网最重要的协议之一。除了 WEB 浏览器之外, WEB 服务,基于网络的应用程序以及日益增长的网络计算不断扩展着 HTTP 协议的角色,使得越来越多的应用程序需要 HTTP 协议的支持。虽然 JAVA 类库 .net 包提供了基本功能,来使用 HTTP 协议访问网络资源,但是其灵活性和功能远不能满足很多应用程序的需要。而 Jakarta Commons HttpClient 组件寻求提供更为灵活,更加高效的 HTTP 协议支持,简化基于 HTTP 协议的应用程序的创建。 HttpClient 提供了很多的特性,支持最新的 HTTP 标准,可以访问这里了解更多关于 HttpClinet 的详细信息。目前有很多的开源项目都用到了 HttpClient 提供的 HTTP功能,登陆网址可 以查看这些项目。本文中使用 HttpClinet 提供的类库来访问和下载 Internet上面的网页,在后续部分会详细介绍到其提供的两种请求网络资源的方法: Get 请求和 Post 请求。Apatche 提供免费的 HTTPClien t源码和 JAR 包下载,可以登陆这里 下载最新的HttpClient 组件。笔者使用的是 HttpClient3.1。

    HtmlParser 简介

    当 今的 Internet 上面有数亿记的网页,越来越多应用程序将这些网页作为分析和处理的数据对象。这些网页多为半结构化的文本,有着大量的标签和嵌套的结构。当我们自己开发一 些处理网页的应用程序时,会想到要开发一个单独的网页解析器,这一部分的工作必定需要付出相当的精力和时间。事实上,做为 JAVA 应用程序开发者, HtmlParser 为其提供了强大而灵活易用的开源类库,大大节省了写一个网页解析器的开销。 HtmlParser 是 http://sourceforge.net 上活跃的一个开源项目,它提供了线性和嵌套两种方式来解析网页,主要用于 html 网页的转换(Transformation) 以及网页内容的抽取 (Extraction)。HtmlParser 有如下一些易于使用的特性:过滤器 (Filters),访问者模式 (Visitors),处理自定义标签以及易于使用的 JavaBeans。正如 HtmlParser 首页所说:它是一个快速,健壮以及严格测试过的组件;以它设计的简洁,程序运行的速度以及处理 Internet 上真实网页的能力吸引着越来越多的开发者。 本文中就是利用HtmlParser 里提取网页里的链接,实现简易爬虫里的关键部分。HtmlParser 最新的版本是HtmlParser1.6,可以登陆这里下载其源码、 API 参考文档以及 JAR 包。

    开发环境的搭建

    笔者所使用的开发环境是 Eclipse Europa,此开发工具可以在 www.eclipse.org 免费的下载;JDK是1.6,你也可以在 www.java.sun.com 站点下载,并且在操作系统中配置好环境变量。在 Eclipse 中创建一个 JAVA 工程,在工程的 Build Path 中导入下载的Commons-httpClient3.1.Jar,htmllexer.jar 以及 htmlparser.jar 文件。

    图 1. 开发环境搭建

    开发环境搭建

    HttpClient 基本类库使用

    HttpClinet 提供了几个类来支持 HTTP 访问。下面我们通过一些示例代码来熟悉和说明这些类的功能和使用。 HttpClient 提供的 HTTP 的访问主要是通过 GetMethod 类和 PostMethod 类来实现的,他们分别对应了 HTTP Get 请求与 Http Post 请求。

    GetMethod

    使用 GetMethod 来访问一个 URL 对应的网页,需要如下一些步骤。

    1. 生成一个 HttpClinet 对象并设置相应的参数。
    2. 生成一个 GetMethod 对象并设置响应的参数。
    3. 用 HttpClinet 生成的对象来执行 GetMethod 生成的 Get 方法。
    4. 处理响应状态码。
    5. 若响应正常,处理 HTTP 响应内容。
    6. 释放连接。

    清单 1 的代码展示了这些步骤,其中的注释对代码进行了较详细的说明。

    清单 1.
    /* 1 生成 HttpClinet 对象并设置参数*/
      HttpClient httpClient=new HttpClient();
      //设置 Http 连接超时为5秒
    httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(5000);
      
      /*2 生成 GetMethod 对象并设置参数*/
      GetMethod getMethod=new GetMethod(url);	 
      //设置 get 请求超时为 5 秒
    getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT,5000);
      //设置请求重试处理,用的是默认的重试处理:请求三次
    getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
              new DefaultHttpMethodRetryHandler());
      
      /*3 执行 HTTP GET 请求*/
      try{ 
    	  int statusCode = httpClient.executeMethod(getMethod);
    	  /*4 判断访问的状态码*/
          if (statusCode != HttpStatus.SC_OK) 
          {
    System.err.println("Method failed: "+ getMethod.getStatusLine());
          }
      
          /*5 处理 HTTP 响应内容*/
          //HTTP响应头部信息,这里简单打印
      Header[] headers=getMethod.getResponseHeaders();
          for(Header  h:  headers)
      	      System.out.println(h.getName()+" "+h.getValue());*/
          //读取 HTTP 响应内容,这里简单打印网页内容
          byte[] responseBody = getMethod.getResponseBody();//读取为字节数组
    System.out.println(new String(responseBody));
          //读取为 InputStream,在网页内容数据量大时候推荐使用
          InputStream response = getMethod.getResponseBodyAsStream();//
          …
    }
    catch (HttpException e) 
    {
    	  // 发生致命的异常,可能是协议不对或者返回的内容有问题
    		  System.out.println("Please check your provided http address!");
    e.printStackTrace();
    	 } 
    catch (IOException e)
      {
    	        // 发生网络异常
    		e.printStackTrace();
    	 } finally {
    		         /*6 .释放连接*/
    			getMethod.releaseConnection();		   
    		    }

    这里值得注意的几个地方是:

    1. 设置连接超时和请求超时,这两个超时的意义不同,需要分别设置。
    2. 响应状态码的处理。
    3. 返回的结果可以为字节数组,也可以为 InputStream,而后者在网页内容数据量较大的时候推荐使用。

    在处理返回结果的时候可以根据自己的需要,进行相应的处理。如笔者是需要保存网页

    到本地,因此就可以写一个 saveToLocaleFile(byte[] data, String filePath) 的方法,将字节数组保存成本地文件。后续的简易爬虫部分会有相应的介绍。

    PostMethod

    PostMethod 方法与 GetMethod 方法的使用步骤大体相同。但是由于 PostMethod 使用的是HTTP 的 Post 请求,因而请求参数的设置与 GetMethod 有所不同。在 GetMethod 中,请求的参数直接写在 URL 里,一般以这样形式出现:http://hostname:port//file?name1=value1&name2=value …。请求参数是 name,value 对。比如我想得到百度搜索“Thinking In Java”的结果网页,就可以使 GetMethod 的构造方法中的 url 为:http://www.baidu.com/s?wd=Thinking+In+Java 。而 PostMethod 则可以模拟网页里表单提交的过程,通过设置表单里 post 请求参数的值,来动态的获得返回的网页结果。清单 2 中的代码展示了如何创建一个 Post 对象,并设置相应的请求参数。

    清单2
    PostMethod postMethod = new PostMethod("http://dict.cn/");
    postMethod.setRequestBody(new NameValuePair[]{new NameValuePair("q","java")});

    HtmlParser 基本类库使用

    HtmlParser 提供了强大的类库来处理 Internet 上的网页,可以实现对网页特定内容的提取和修改。下面通过几个例子来介绍 HtmlParser 的一些使用。这些例子其中的代码,有部分用在了后面介绍的简易爬虫中。以下所有的代码和方法都在在类 HtmlParser.Test.java 里,这是笔者编写的一个用来测试 HtmlParser 用法的类。

    • 迭代遍历网页所有节点

    网页是一个半结构化的嵌套文本文件,有类似 XML 文件的树形嵌套结构。使用HtmlParser 可以让我们轻易的迭代遍历网页的所有节点。清单 3 展示了如何来实现这个功能。

    清单 3
    // 循环访问所有节点,输出包含关键字的值节点
    	public static void extractKeyWordText(String url, String keyword) {
    		try {
                //生成一个解析器对象,用网页的 url 作为参数
    			Parser parser = new Parser(url);
    			//设置网页的编码,这里只是请求了一个 gb2312 编码网页
    			parser.setEncoding("gb2312");
    			//迭代所有节点, null 表示不使用 NodeFilter
    			NodeList list = parser.parse(null);
                //从初始的节点列表跌倒所有的节点
    			processNodeList(list, keyword);
    		} catch (ParserException e) {
    			e.printStackTrace();
    		}
    	}
    
    	private static void processNodeList(NodeList list, String keyword) {
    		//迭代开始
    		SimpleNodeIterator iterator = list.elements();
    		while (iterator.hasMoreNodes()) {
    			Node node = iterator.nextNode();
    			//得到该节点的子节点列表
    			NodeList childList = node.getChildren();
    			//孩子节点为空,说明是值节点
    			if (null == childList)
    			{
    				//得到值节点的值
    				String result = node.toPlainTextString();
    				//若包含关键字,则简单打印出来文本
    				if (result.indexOf(keyword) != -1)
    					System.out.println(result);
    			} //end if
    			//孩子节点不为空,继续迭代该孩子节点
    			else 
    			{
    				processNodeList(childList, keyword);
    			}//end else
    		}//end wile
    	}

    上面的中有两个方法:

    1. private static void processNodeList(NodeList list, String keyword)

    该方法是用类似深度优先的方法来迭代遍历整个网页节点,将那些包含了某个关键字的值节点的值打印出来。

    1. public static void extractKeyWordText(String url, String keyword)

    该方法生成针对 String 类型的 url 变量代表的某个特定网页的解析器,调用 1中的方法实现简单的遍历。

    清单 3 的代码展示了如何迭代所有的网页,更多的工作可以在此基础上展开。比如找到某个特定的网页内部节点,其实就可以在遍历所有的节点基础上来判断,看被迭代的节点是否满足特定的需要。

    • 使用 NodeFilter

    NodeFilter 是一个接口,任何一个自定义的 Filter 都需要实现这个接口中的 boolean accept() 方法。如果希望迭代网页节点的时候保留当前节点,则在节点条件满足的情况下返回 true;否则返回 false。HtmlParse 里提供了很多实现了 NodeFilter 接口的类,下面就一些笔者所用到的,以及常用的 Filter 做一些介绍:

    1. 对 Filter 做逻辑操作的 Fitler 有:AndFilterNotFilterOrFilterXorFilter

    这些 Filter 来组合不同的 Filter,形成满足两个 Filter 逻辑关系结果的 Filter。

    1. 判断节点的孩子,兄弟,以及父亲节点情况的 Filter 有:HasChildFilterHasParentFilterHasSiblingFilter
    2. 判断节点本身情况的 Filter 有 HasAttributeFilter:判读节点是否有特定属性;LinkStringFilter:判断节点是否是具有特定模式 (pattern) url 的节点;

    TagNameFilter:判断节点是否具有特定的名字;NodeClassFilter:判读节点是否是某个 HtmlParser 定义好的 Tag 类型。在 org.htmlparser.tags 包下有对应 Html标签的各种 Tag,例如 LinkTag,ImgeTag 等。

    还有其他的一些 Filter 在这里不一一列举了,可以在 org.htmlparser.filters 下找到。

    清单 4 展示了如何使用上面提到过的一些 filter 来抽取网页中的 <a> 标签里的 href属性值,<img> 标签里的 src 属性值,以及 <frame> 标签里的 src 的属性值。

    清单4
    // 获取一个网页上所有的链接和图片链接
    	public static void extracLinks(String url) {
    		try {
    			Parser parser = new Parser(url);
    			parser.setEncoding("gb2312");
    //过滤 <frame> 标签的 filter,用来提取 frame 标签里的 src 属性所、表示的链接
    			NodeFilter frameFilter = new NodeFilter() {
    				public boolean accept(Node node) {
    					if (node.getText().startsWith("frame src=")) {
    						return true;
    					} else {
    						return false;
    					}
    				}
    			};
    //OrFilter 来设置过滤 <a> 标签,<img> 标签和 <frame> 标签,三个标签是 or 的关系
    	 OrFilte rorFilter = new OrFilter(new NodeClassFilter(LinkTag.class), new 
    NodeClassFilter(ImageTag.class));
    	 OrFilter linkFilter = new OrFilter(orFilter, frameFilter);
    	//得到所有经过过滤的标签
    	NodeList list = parser.extractAllNodesThatMatch(linkFilter);
    	for (int i = 0; i < list.size(); i++) {
    		Node tag = list.elementAt(i);
    		if (tag instanceof LinkTag)//<a> 标签 
    		{
    			LinkTag link = (LinkTag) tag;
    			String linkUrl = link.getLink();//url
    			String text = link.getLinkText();//链接文字
    			System.out.println(linkUrl + "**********" + text);
    		}
    		else if (tag instanceof ImageTag)//<img> 标签
    		{
    			ImageTag image = (ImageTag) list.elementAt(i);
    			System.out.print(image.getImageURL() + "********");//图片地址
    			System.out.println(image.getText());//图片文字
    		}
    		else//<frame> 标签
    		{
    //提取 frame 里 src 属性的链接如 <frame src="test.html"/>
    			String frame = tag.getText();
    			int start = frame.indexOf("src=");
    			frame = frame.substring(start);
    			int end = frame.indexOf(" ");
    			if (end == -1)
    				end = frame.indexOf(">");
    			frame = frame.substring(5, end - 1);
    			System.out.println(frame);
    		}
    	}
    } catch (ParserException e) {
    			e.printStackTrace();
    }
    }
    • 简单强大的 StringBean

    如果你想要网页中去掉所有的标签后剩下的文本,那就是用 StringBean 吧。以下简单的代码可以帮你解决这样的问题:

    清单5

    StringBean sb = new StringBean();

    sb.setLinks(false);//设置结果中去点链接

    sb.setURL(url);//设置你所需要滤掉网页标签的页面 url

    System.out.println(sb.getStrings());//打印结果

    HtmlParser 提供了强大的类库来处理网页,由于本文旨在简单的介绍,因此只是将与笔者后续爬虫部分有关的关键类库进行了示例说明。感兴趣的读者可以专门来研究一下 HtmlParser 更为强大的类库。

    简易爬虫的实现

    HttpClient 提供了便利的 HTTP 协议访问,使得我们可以很容易的得到某个网页的源码并保存在本地;HtmlParser 提供了如此简便灵巧的类库,可以从网页中便捷的提取出指向其他网页的超链接。笔者结合这两个开源包,构建了一个简易的网络爬虫。

    爬虫 (Crawler) 原理

    学 过数据结构的读者都知道有向图这种数据结构。如下图所示,如果将网页看成是图中的某一个节点,而将网页中指向其他网页的链接看成是这个节点指向其他节点的 边,那么我们很容易将整个 Internet 上的网页建模成一个有向图。理论上,通过遍历算法遍历该图,可以访问到Internet 上的几乎所有的网页。最简单的遍历就是宽度优先以及深度优先。以下笔者实现的简易爬虫就是使用了宽度优先的爬行策略。

    图 2. 网页关系的建模图

    网页关系的建模图

    简易爬虫实现流程

    在看简易爬虫的实现代码之前,先介绍一下简易爬虫爬取网页的流程。

    图 3. 爬虫流程图

    爬虫流程图

    各个类的源码以及说明

    对应上面的流程图,简易爬虫由下面几个类组成,各个类职责如下:

    Crawler.java:爬虫的主方法入口所在的类,实现爬取的主要流程。

    LinkDb.java:用来保存已经访问的 url 和待爬取的 url 的类,提供url出对入队操作。

    Queue.java: 实现了一个简单的队列,在 LinkDb.java 中使用了此类。

    FileDownloader.java:用来下载 url 所指向的网页。

    HtmlParserTool.java: 用来抽取出网页中的链接。

    LinkFilter.java:一个接口,实现其 accept() 方法用来对抽取的链接进行过滤。

    下面是各个类的源码,代码中的注释有比较详细的说明。

    清单6 Crawler.java
    package com.ie;
    
    import java.util.Set;
    public class Crawler {
    	/* 使用种子 url 初始化 URL 队列*/
    	private void initCrawlerWithSeeds(String[] seeds)
    	{
    		for(int i=0;i<seeds.length;i++)
    			LinkDB.addUnvisitedUrl(seeds[i]);
    	}
    	
    	/* 爬取方法*/
    	public void crawling(String[] seeds)
    	{
    		LinkFilter filter = new LinkFilter(){
    			//提取以 http://www.twt.edu.cn 开头的链接
    			public boolean accept(String url) {
    				if(url.startsWith("http://www.twt.edu.cn"))
    					return true;
    				else
    					return false;
    			}
    		};
    		//初始化 URL 队列
    		initCrawlerWithSeeds(seeds);
    		//循环条件:待抓取的链接不空且抓取的网页不多于 1000
    		while(!LinkDB.unVisitedUrlsEmpty()&&LinkDB.getVisitedUrlNum()<=1000)
    		{
    			//队头 URL 出对
    			String visitUrl=LinkDB.unVisitedUrlDeQueue();
    			if(visitUrl==null)
    				continue;
    			FileDownLoader downLoader=new FileDownLoader();
    			//下载网页
    			downLoader.downloadFile(visitUrl);
    			//该 url 放入到已访问的 URL 中
    			LinkDB.addVisitedUrl(visitUrl);
    			//提取出下载网页中的 URL
    			
    			Set<String> links=HtmlParserTool.extracLinks(visitUrl,filter);
    			//新的未访问的 URL 入队
    			for(String link:links)
    			{
    					LinkDB.addUnvisitedUrl(link);
    			}
    		}
    	}
    	//main 方法入口
    	public static void main(String[]args)
    	{
    		Crawler crawler = new Crawler();
    		crawler.crawling(new String[]{"http://www.twt.edu.cn"});
    	}
    }
    清单7 LinkDb.java
    package com.ie;
    
    import java.util.HashSet;
    import java.util.Set;
    
    /**
     * 用来保存已经访问过 Url 和待访问的 Url 的类
     */
    public class LinkDB {
    
    	//已访问的 url 集合
    	private static Set<String> visitedUrl = new HashSet<String>();
    	//待访问的 url 集合
    	private static Queue<String> unVisitedUrl = new Queue<String>();
    
    	
    	public static Queue<String> getUnVisitedUrl() {
    		return unVisitedUrl;
    	}
    
    	public static void addVisitedUrl(String url) {
    		visitedUrl.add(url);
    	}
    
    	public static void removeVisitedUrl(String url) {
    		visitedUrl.remove(url);
    	}
    
    	public static String unVisitedUrlDeQueue() {
    		return unVisitedUrl.deQueue();
    	}
    
    	// 保证每个 url 只被访问一次
    	public static void addUnvisitedUrl(String url) {
    		if (url != null && !url.trim().equals("")
     && !visitedUrl.contains(url)
    				&& !unVisitedUrl.contians(url))
    			unVisitedUrl.enQueue(url);
    	}
    
    	public static int getVisitedUrlNum() {
    		return visitedUrl.size();
    	}
    
    	public static boolean unVisitedUrlsEmpty() {
    		return unVisitedUrl.empty();
    	}
    }
    清单8 Queue.java
    package com.ie;
    
    import java.util.LinkedList;
    /**
     * 数据结构队列
     */
    public class Queue<T> {
    
    	private LinkedList<T> queue=new LinkedList<T>();
    	
    	public void enQueue(T t)
    	{
    		queue.addLast(t);
    	}
    	
    	public T deQueue()
    	{
    		return queue.removeFirst();
    	}
    	
    	public boolean isQueueEmpty()
    	{
    		return queue.isEmpty();
    	}
    	
    	public boolean contians(T t)
    	{
    		return queue.contains(t);
    	}
    	
    	public boolean empty()
    	{
    		return queue.isEmpty();
    	}
    }
    清单 9 FileDownLoader.java
    package com.ie;
    
    import java.io.DataOutputStream;
    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
    import org.apache.commons.httpclient.HttpClient;
    import org.apache.commons.httpclient.HttpException;
    import org.apache.commons.httpclient.HttpStatus;
    import org.apache.commons.httpclient.methods.GetMethod;
    import org.apache.commons.httpclient.params.HttpMethodParams;
    
    public class FileDownLoader {
    	
    	/**根据 url 和网页类型生成需要保存的网页的文件名
    	 *去除掉 url 中非文件名字符 
    	 */
    	public  String getFileNameByUrl(String url,String contentType)
    	{
    		url=url.substring(7);//remove http://
    		if(contentType.indexOf("html")!=-1)//text/html
    		{
    			url= url.replaceAll("[\?/:*|<>"]", "_")+".html";
    			return url;
    		}
    		else//如application/pdf
    		{
    return url.replaceAll("[\?/:*|<>"]", "_")+"."+ 
              contentType.substring(contentType.lastIndexOf("/")+1);
    		}	
    	}
    
    	/**保存网页字节数组到本地文件
    	 * filePath 为要保存的文件的相对地址
    	 */
    	private void saveToLocal(byte[] data,String filePath)
    	{
    		try {
    			DataOutputStream out=new DataOutputStream(
    new FileOutputStream(new File(filePath)));
    			for(int i=0;i<data.length;i++)
    			out.write(data[i]);
    			out.flush();
    			out.close();
    		} catch (IOException e) {
    			e.printStackTrace();
    		}
    	}
    
    	/*下载 url 指向的网页*/
    	public String  downloadFile(String url)
    	{
    		  String filePath=null;
    		  /* 1.生成 HttpClinet 对象并设置参数*/
    		  HttpClient httpClient=new HttpClient();
    		  //设置 Http 连接超时 5s
    		  	  httpClient.getHttpConnectionManager().getParams().
    setConnectionTimeout(5000);
    		  
    		  /*2.生成 GetMethod 对象并设置参数*/
    		  GetMethod getMethod=new GetMethod(url);	 
    		  //设置 get 请求超时 5s
    		  getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT,5000);
    		  //设置请求重试处理
    		  getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
    			new DefaultHttpMethodRetryHandler());
    		  
    		  /*3.执行 HTTP GET 请求*/
    		  try{ 
    			  int statusCode = httpClient.executeMethod(getMethod);
    			  //判断访问的状态码
    			  if (statusCode != HttpStatus.SC_OK) 
    			  {
    System.err.println("Method failed: "+ getMethod.getStatusLine());
    				  filePath=null;
    			  }
    			  
    			  /*4.处理 HTTP 响应内容*/
     byte[] responseBody = getMethod.getResponseBody();//读取为字节数组
    			  //根据网页 url 生成保存时的文件名
    filePath="temp\"+getFileNameByUrl(url,
    		   getMethod.getResponseHeader("Content-Type").getValue());
    			saveToLocal(responseBody,filePath);
    		  } catch (HttpException e) {
    				   // 发生致命的异常,可能是协议不对或者返回的内容有问题
    				   System.out.println("Please check your provided http 
    address!");
    				   e.printStackTrace();
    				  } catch (IOException e) {
    				   // 发生网络异常
    				   e.printStackTrace();
    				  } finally {
    				   // 释放连接
    				   getMethod.releaseConnection();		   
    				  }
    				  return filePath;
    	}
    	//测试的 main 方法
    	public static void main(String[]args)
    	{
    		FileDownLoader downLoader = new FileDownLoader();
    		downLoader.downloadFile("http://www.twt.edu.cn");
    	}
    }
    清单 10 HtmlParserTool.java
    package com.ie;
    
    import java.util.HashSet;
    import java.util.Set;
    
    import org.htmlparser.Node;
    import org.htmlparser.NodeFilter;
    import org.htmlparser.Parser;
    import org.htmlparser.filters.NodeClassFilter;
    import org.htmlparser.filters.OrFilter;
    import org.htmlparser.tags.LinkTag;
    import org.htmlparser.util.NodeList;
    import org.htmlparser.util.ParserException;
    
    public class HtmlParserTool {
    	// 获取一个网站上的链接,filter 用来过滤链接
    	public static Set<String> extracLinks(String url,LinkFilter filter) {
    
    		Set<String> links = new HashSet<String>();
    		try {
    			Parser parser = new Parser(url);
    			parser.setEncoding("gb2312");
    			// 过滤 <frame >标签的 filter,用来提取 frame 标签里的 src 属性所表示的链接
    			NodeFilter frameFilter = new NodeFilter() {
    				public boolean accept(Node node) {
    					if (node.getText().startsWith("frame src=")) {
    						return true;
    					} else {
    						return false;
    					}
    				}
    			};
    			// OrFilter 来设置过滤 <a> 标签,和 <frame> 标签
    			OrFilter linkFilter = new OrFilter(new NodeClassFilter(
    					LinkTag.class), frameFilter);
    			// 得到所有经过过滤的标签
    			NodeList list = parser.extractAllNodesThatMatch(linkFilter);
    			for (int i = 0; i < list.size(); i++) {
    				Node tag = list.elementAt(i);
    				if (tag instanceof LinkTag)// <a> 标签
    				{
    					LinkTag link = (LinkTag) tag;
    					String linkUrl = link.getLink();// url
    					if(filter.accept(linkUrl))
    						links.add(linkUrl);
    				} else// <frame> 标签
    				{
    		        // 提取 frame 里 src 属性的链接如 <frame src="test.html"/>
    					String frame = tag.getText();
    					int start = frame.indexOf("src=");
    					frame = frame.substring(start);
    					int end = frame.indexOf(" ");
    					if (end == -1)
    						end = frame.indexOf(">");
    					String frameUrl = frame.substring(5, end - 1);
    					if(filter.accept(frameUrl))
    						links.add(frameUrl);
    				}
    			}
    		} catch (ParserException e) {
    			e.printStackTrace();
    		}
    		return links;
    	}
    	//测试的 main 方法
    	public static void main(String[]args)
    	{
    Set<String> links = HtmlParserTool.extracLinks(
    "http://www.twt.edu.cn",new LinkFilter()
    		{
    			//提取以 http://www.twt.edu.cn 开头的链接
    			public boolean accept(String url) {
    				if(url.startsWith("http://www.twt.edu.cn"))
    					return true;
    				else
    					return false;
    			}
    			
    		});
    		for(String link : links)
    			System.out.println(link);
    	}
    }
    清单11 LinkFilter.java
    package com.ie;
    
    public interface LinkFilter {
    	public boolean accept(String url);
    }

    这些代码中关键的部分都在 HttpClient 和 HtmlParser 介绍中说明过了,其他部分也比较容易,请感兴趣的读者自行理解。

    总结

    这 篇文章主要是介绍与展示了如何使用开源的 HttpClinet 包和 HtmlParser 包,以及结合这两者来给出了一个简易网络爬虫程序的实现,当然这个爬虫与实际真正的爬虫还是有所差距。由于更多的目的是关注这两个开源包的运用,加上本文 篇幅有限,因此,没有对这两个开源包做非常详尽的介绍。希望这篇文章能够引导读者对 HttpClient 包和 HtmlParser 产生兴趣,从而利用他们构建强大的 JAVA 网络应用程序。

    参考资料

    学习

    • Developworks 学习其他关于 HttpClien t和 HtmlParser 的技术文章。
    • Developworks 其它专区学习更多的最新技术。

    获得技术和产品

    www.eclipse.org 获得免费的 IDE

    讨论

    在 Developerworks 社区参与更多的讨论

    基于htmlparser实现网页内容解析

    网页解析,即程序自动分析网页内容、获取信息,从而进一步处理信息。

    网页解析是实现网络爬虫中不可缺少而且十分重要的一环,由于本人经验也很有限,我仅就我们团队开发基于关键词匹配和模板匹配的主题爬虫的经验谈谈如何实现网页解析。

    首先,必须说在最前的是我们使用的工具——htmlparser

    简要地说,htmlparser包提供方便、简洁的处理html文件的方法,它将html页面中的标签按树形结构解析成一个一个结点,一种类型的结点对应一个类,通过调用其方法可以轻松地访问标签中的内容。

    我所使用的是htmlparser2.0,也就是最新版本。强烈推荐。

    好,进入正题。

    对于主题爬虫,它的功能就是将与主题相关的网页下载到本地,将网页的相关信息存入数据库。

    网页解析模块要实现两大功能:1.从页面中提取出子链接,加入到爬取url队列中;2.解析网页内容,与主题进行相关度计算。

    由于网页内容解析需要频繁地访问网页文件,如果通过url访问网络获取文件的时间开销比较大,所以我们的做法是将爬取队列中的网页统统下载到本地,对本地的网页文件进行页面内容解析,最后删除不匹配的网页。而子链接的提取比较简单,通过网络获取页面文件即可。对于给定url通过网络访问网页,和给定文件路径访问本地网页文件,htmlparser都是支持的

    1.子链接的提取:

    做页面子链接提取的基本思路是:

    1.用被提取的网页的url实例化一个Parser

    2.实例化Filter,设置页面过滤条件——只获取<a>标签与<frame>标签的内容

    3.用Parser提取页面中所有通过Filter的结点,得到NodeList

    4.遍历NodeList,调用Node的相应方法得到其中的链接,加入子链接的集合

    5.返回子链接集合

    OK,上代码:

    复制代码
    复制代码
     1 package Crawler;
     2 
     3 
     4 import java.util.HashSet;
     5 import java.util.Set;
     6 
     7 import org.htmlparser.Node;
     8 import org.htmlparser.NodeFilter;
     9 import org.htmlparser.Parser;
    10 import org.htmlparser.filters.NodeClassFilter;
    11 import org.htmlparser.filters.OrFilter;
    12 import org.htmlparser.tags.LinkTag;
    13 import org.htmlparser.util.NodeList;
    14 import org.htmlparser.util.ParserException;
    15 
    16 public class HtmlLinkParser {
    17     //获取子链接,url为网页url,filter是链接过滤器,返回该页面子链接的HashSet
    18     public static Set<String> extracLinks(String url, LinkFilter filter) {
    19 
    20         Set<String> links = new HashSet<String>();
    21         try {
    22             Parser parser = new Parser(url);
    23             parser.setEncoding("utf-8");
    24             // 过滤 <frame >标签的 filter,用来提取 frame 标签里的 src 属性所表示的链接
    25             NodeFilter frameFilter = new NodeFilter() {
    26                 public boolean accept(Node node) {
    27                     if (node.getText().startsWith("frame src=")) {
    28                         return true;
    29                     } else {
    30                         return false;
    31                     }
    32                 }
    33             };
    34             // OrFilter 接受<a>标签或<frame>标签,注意NodeClassFilter()可用来过滤一类标签,linkTag对应<标签>
    35             OrFilter linkFilter = new OrFilter(new NodeClassFilter(
    36                     LinkTag.class), frameFilter);
    37             // 得到所有经过过滤的标签,结果为NodeList
    38             NodeList list = parser.extractAllNodesThatMatch(linkFilter);
    39             for (int i = 0; i < list.size(); i++) {
    40                 Node tag = list.elementAt(i);
    41                 if (tag instanceof LinkTag)// <a> 标签
    42                 {
    43                     LinkTag link = (LinkTag) tag;
    44                     String linkUrl = link.getLink();// 调用getLink()方法得到<a>标签中的链接
    45                     if (filter.accept(linkUrl))//将符合filter过滤条件的链接加入链接表
    46                         links.add(linkUrl);
    47                 } else{// <frame> 标签
    48                     // 提取 frame 里 src 属性的链接如 <frame src="test.html"/>
    49                     String frame = tag.getText();
    50                     int start = frame.indexOf("src=");
    51                     frame = frame.substring(start);
    52                     int end = frame.indexOf(" ");
    53                     if (end == -1)
    54                         end = frame.indexOf(">");
    55                     String frameUrl = frame.substring(5, end - 1);
    56                     if (filter.accept(frameUrl))
    57                         links.add(frameUrl);
    58                 }
    59             }
    60         } catch (ParserException e) {//捕捉parser的异常
    61             e.printStackTrace();
    62         }
    63         return links;
    64     }
    65 }
    复制代码
    复制代码

    此时可能有读者在想:呵~呵~博主忽略了相对url链接的问题了(-.-)

    其实我想到了,一开始我写了一个private方法专门把任何url转换成绝对url链接。后来调试的时候我发现我的方法根本没用,因为htmlparser很人性化地自动完成了这个转换!

    另外,Parser是需要设置编码的,在这段程序中我直接设置为utf-8。实际上网页的编码方式是多种多样的,在<meta>标签中 有关于编码方式的信息,如果编码不正确,页面的文本内容可能是乱码。不过,在子链接提取的部分,我们仅对标签内部的内容进行处理,这些内容是根据html 语法编写的,不涉及编码的问题。

    2.解析网页内容:

    基本思路:

    1.读取html文件,获得页面编码,获得String格式的文件内容

    2.用页面编码实例化html文件的Parser

    3.对需要提取的结点设置相应的Filter

    4.根据给定的Filter,用Parser解析html文件

    5.提取结点中的文本内容,进行处理(本例中是关键字匹配,计算主题相关度)

    复制代码
      1 import java.io.BufferedReader;
      2 import java.io.FileInputStream;
      3 import java.io.FileNotFoundException;
      4 import java.io.FileReader;
      5 import java.io.IOException;
      6 import java.io.InputStreamReader;
      7 import java.util.regex.Matcher;
      8 import java.util.regex.Pattern;
      9 
     10 import org.htmlparser.Parser;
     11 import org.htmlparser.filters.NodeClassFilter;
     12 import org.htmlparser.tags.HeadingTag;
     13 import org.htmlparser.tags.LinkTag;
     14 import org.htmlparser.tags.MetaTag;
     15 import org.htmlparser.tags.ParagraphTag;
     16 import org.htmlparser.tags.TitleTag;
     17 import org.htmlparser.util.NodeList;
     18 import org.htmlparser.util.ParserException;
     19 
     20 import java.util.Set;
     21 import multi.patt.match.ac.*;
     22 
     23 public class HtmlFileParser {
     24     String filepath=new String();//html文件路径
     25     private static String[] keyWords;//关键词列表
     26     /*static{
     27         keyWords=read("filePath");//从指定文件中读取关键词列表
     28     }*/
     29     public HtmlFileParser(String filepath){
     30         this.filepath=filepath;
     31     }
     32     public String getTitle(){//得到页面标题
     33         FileAndEnc fae=readHtmlFile();
     34         int i=0;
     35         try{
     36             //实例化一个本地html文件的Parser
     37             Parser titleParser = Parser.createParser(fae.getFile(),fae.getEnc());
     38             NodeClassFilter titleFilter =new NodeClassFilter(TitleTag.class);
     39             NodeList titleList = titleParser.extractAllNodesThatMatch(titleFilter);
     40             //实际上一个网页应该只有一个<title>标签,但extractAllNodesThatMatch方法返回的只能是一个NodeList
     41             for (i = 0; i < titleList.size(); i++) {
     42                 TitleTag title_tag = (TitleTag) titleList.elementAt(i);
     43                 return title_tag.getTitle();
     44             }
     45         }catch(ParserException e) {
     46             return null;
     47         }
     48         return null;
     49     }
     50     public String getEncoding(){//获得页面编码
     51         FileAndEnc fae=readHtmlFile();
     52         return fae.getEnc();
     53     }
     54     public float getRelatGrade(){//计算网页的主题相关度
     55         FileAndEnc fae=readHtmlFile();
     56         String file=fae.getFile();
     57         String enC=fae.getEnc();
     58         String curString;
     59         int curWordWei = 1;//当前关键词权重
     60         float curTagWei = 0;//当前标签权重
     61         float totalGra = 0;//总相关度分
     62         int i;
     63         AcApply obj = new AcApply();//实例化ac自动机
     64         Pattern p = null;
     65         Matcher m = null;
     66         try{//根据不同标签依次进行相关度计算
     67             //title tag    <title>
     68             curTagWei=5;
     69             Parser titleParser = Parser.createParser(file,enC);
     70             NodeClassFilter titleFilter =new NodeClassFilter(TitleTag.class);
     71             NodeList titleList = titleParser.extractAllNodesThatMatch(titleFilter);
     72             for (i = 0; i < titleList.size(); i++) {
     73                 TitleTag titleTag=(TitleTag)titleList.elementAt(i);
     74                 curString=titleTag.getTitle();
     75                 Set result = obj.findWordsInArray(keyWords, curString);//ac自动机的方法返回匹配的词的表
     76                 totalGra=totalGra+result.size()*curTagWei;//计算相关度
     77             }
     78             //meta tag of description and keyword <meta>
     79             curTagWei=4;
     80             Parser metaParser = Parser.createParser(file,enC);
     81             NodeClassFilter metaFilter =new NodeClassFilter(MetaTag.class);
     82             NodeList metaList = metaParser.extractAllNodesThatMatch(metaFilter);
     83             p = Pattern.compile("\b(description|keywords)\b",Pattern.CASE_INSENSITIVE);
     84             for (i = 0; i < metaList.size(); i++) {
     85                 MetaTag metaTag=(MetaTag)metaList.elementAt(i);
     86                 curString=metaTag.getMetaTagName();
     87                 if(curString==null){
     88                     continue;
     89                 }
     90                 m = p.matcher(curString); //正则匹配name是description或keyword的<meta>标签
     91                 if(m.find()){
     92                     curString=metaTag.getMetaContent();//提取其content
     93                     Set result = obj.findWordsInArray(keyWords, curString);
     94                     totalGra=totalGra+result.size()*curTagWei;
     95                 }
     96                 else{
     97                     curString=metaTag.getMetaContent();
     98                     Set result = obj.findWordsInArray(keyWords, curString);
     99                     totalGra=totalGra+result.size()*2;
    100                 }
    101             }
    102             //heading tag <h*>
    103             curTagWei=3;
    104             Parser headingParser = Parser.createParser(file,enC);
    105             NodeClassFilter headingFilter =new NodeClassFilter(HeadingTag.class);
    106             NodeList headingList = headingParser.extractAllNodesThatMatch(headingFilter);
    107             for (i = 0; i < headingList.size(); i++) {
    108                 HeadingTag headingTag=(HeadingTag)headingList.elementAt(i);
    109                 curString=headingTag.toPlainTextString();//得到<h*>标签中的纯文本
    110                 if(curString==null){
    111                     continue;
    112                 }
    113                 Set result = obj.findWordsInArray(keyWords, curString);
    114                 totalGra=totalGra+result.size()*curTagWei;
    115             }
    116             //paragraph tag <p>
    117             curTagWei=(float)2.5;
    118             Parser paraParser = Parser.createParser(file,enC);
    119             NodeClassFilter paraFilter =new NodeClassFilter(ParagraphTag.class);
    120             NodeList paraList = paraParser.extractAllNodesThatMatch(paraFilter);
    121             for (i = 0; i < paraList.size(); i++) {
    122                 ParagraphTag paraTag=(ParagraphTag)paraList.elementAt(i);
    123                 curString=paraTag.toPlainTextString();
    124                 if(curString==null){
    125                     continue;
    126                 }
    127                 Set result = obj.findWordsInArray(keyWords, curString);
    128                 totalGra=totalGra+result.size()*curTagWei;
    129             }
    130             //link tag <a>
    131             curTagWei=(float)0.25;
    132             Parser linkParser = Parser.createParser(file,enC);
    133             NodeClassFilter linkFilter =new NodeClassFilter(LinkTag.class);
    134             NodeList linkList = linkParser.extractAllNodesThatMatch(linkFilter);
    135             for (i = 0; i < linkList.size(); i++) {
    136                 LinkTag linkTag=(LinkTag)linkList.elementAt(i);
    137                 curString=linkTag.toPlainTextString();
    138                 if(curString==null){
    139                     continue;
    140                 }
    141                 Set result = obj.findWordsInArray(keyWords, curString);
    142                 totalGra=totalGra+result.size()*curTagWei;
    143             }        
    144         }catch(ParserException e) {
    145             return 0;
    146         }
    147         return totalGra;
    148     }
    149     private FileAndEnc readHtmlFile(){//读取html文件,返回字符串格式的文件与其编码
    150         StringBuffer abstr = new StringBuffer();
    151         FileAndEnc fae=new FileAndEnc();
    152         try{
    153             //实例化默认编码方式的BufferefReader
    154             BufferedReader enCReader= new BufferedReader(new InputStreamReader(new FileInputStream(filepath),"UTF-8"));
    155             String temp=null;
    156             while((temp=enCReader.readLine())!=null){//得到字符串格式的文件
    157                 abstr.append(temp);
    158                 abstr.append("
    ");
    159             }
    160             String result=abstr.toString();
    161             fae.setFile(result);
    162             String encoding=getEnc(result);
    163             fae.setEnc(encoding);//得到页面编码
    164             //根据得到的编码方式实例化BufferedReader
    165             BufferedReader reader= new BufferedReader(new InputStreamReader(new FileInputStream(filepath),encoding));
    166             StringBuffer abstrT = new StringBuffer();
    167             while((temp=reader.readLine())!=null){
    168                 abstrT.append(temp);
    169                 abstrT.append("
    ");
    170             }
    171             result=abstrT.toString();
    172             fae.setFile(result);//得到真正的页面内容
    173         } catch (FileNotFoundException e) {
    174             System.out.println("file not found");
    175             fae=null;
    176         } catch (IOException e) {
    177             // TODO Auto-generated catch block
    178             e.printStackTrace();
    179             fae=null;
    180         } finally {
    181             return fae;
    182         }
    183     }
    184     private String getEnc(String file){//根据正则匹配得到页面编码
    185         String enC="utf-8";
    186         Pattern p = Pattern.compile("(charset|Charset|CHARSET)\s*=\s*"?\s*([-\w]*?)[^-\w]"); 
    187         Matcher m = p.matcher(file);
    188         if(m.find()){ 
    189             enC=m.group(2);
    190         }
    191         return enC;
    192     }
    193 }
    复制代码

     读者需要注意两点:

    1.用BufferedReader读取文件是需要编码方式的,但是第一次读取我们必然不知道网页的编码。好在网页对于编码的描述在html语言框 架中,我们用默认的编码方式读取文件就可以获取编码。但这个读取的文件的文本内容可能因为编码不正确而产生乱码,所以得到编码后,我们应使用得到的编码再 实例化一个BufferedReader读取文件,这样得到的文件就是正确的了(除非网页本身给的编码就不对)。

    获得正确的编码对于解析网页内容是非常重要的,而网络上什么样的网页都有,我推荐使用比较基础、可靠的方法获得编码,我使用的是正则匹配。

    举个例子:

    这是http://kb.cnblogs.com/page/143965/的对编码的描述:

    <meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>

    这是http://www.ucsd.edu/的对编码的描述:

    <meta charset="utf-8"/>

    2.不熟悉html的读者可能有所不知<meta>的作用,来看看博客园首页的源码:

    <meta name="keywords" content="博客园,开发者,程序员,软件开发,编程,代码,极客,Developer,Programmer,Coder,Code,Coding,Greek,IT学习"/><meta name="description" content="博客园是面向程序员的高品质IT技术学习社区,是程序员学习成长的地方。博客园致力于为程序员打造一个优秀的互联网平台,帮助程序员学好IT技术,更好地用技术改变世界。" />

    这两类<meta>标签的很好的描述了网页的内容

    @编辑 博客园首页这个keyword的内容里这“Greek”……极客是“Geek”,“Greek”是希腊人

    3.由于网页的正文通常是一段最长的纯文本内容,所以当我们得到一个<p>,<li>,<ul>标签的纯文本后,我们可以通过判断字符串的长度来得到网页的正文。

    对页面大量的信息进行处理是很费时的,页面的<title>标签和<meta>标签中往往有对网页内容最精炼的描述,开发者应该考虑性能与代价

     好,我的经验就介绍完了。我还很菜,如有说的不对、讲得不好的地方望读者指正、提出建议!

  • 相关阅读:
    Hibernate的查询方式汇总
    JdbcTemplate详解
    spring读取数据库的配置信息(url、username、password)时的<bean>PropertyPlaceholderConfigurer的用法
    spring aop方式配置事务中的三个概念 pointcut advice advisor
    spring mvc静态资源请求和<mvc:annotation-driven>
    spring aop实现原理
    Spring 配置 事务的几种方式
    转!!常用的4种动态网页技术—CGI、ASP、JSP、PHP
    转! java 中“==” 与“ .equals ”比较
    mysql 批处理文件--- 创建 用户 以及 导入数据
  • 原文地址:https://www.cnblogs.com/timssd/p/4735340.html
Copyright © 2011-2022 走看看