最近有解析HTML的需求,在Java中,好用的HTML解析框架也比较多,如
JSoup
,HTMLParser
,JTidy
等等。在对比几款框架之后,最终选取了HTMLParser
做为第一版实现的框架。所以对HTMLParser
的源码进行了一次整理。由于这种解析类的框架内部细节特别多,所以这里并不会特别的关注所有细节,而是侧重梳理HTMLParser
整个解析的流程。
类图
对我而言,画类图是学习一个框架源码比较直接的方式,一是有利于自己梳理逻辑,二是以后自己看类图还是会很容易联想起其中的一些细节。所以,这里放出类图,下面会对主要的类源码进行分析。
整体介绍
HTMLParser
主要靠Node来表示一个节点,细分为Text、Remark和Tag,通过以上三种形式的组合来表示Html,其中Text接口表示纯文本,Remark表示注释,Tag表示标签。
Parser是直接对外提供服务的类,其parse方法可以返回整个HTML文档被转换后的NodeList。
Lexer直译过来为词法分析程序,它主要负责将Html转换为Node节点的,其nextNode方法使我们后文分析的重点。Lexer与Parser的关系就像老板与员工,Parser是对外谈生意的,Lexer才是实打实干活的伙计。
Page表示整个HTML文档,但它里面的主要逻辑都交由Source负责,Source是对HTML源的一个抽象,HTMLParser中有InputStreamSource与StringSource两种实现,前一种可以处理网络或者文件类的流信息,后者可以处理纯文本的HTML。
解析流程
HTMLParser的使用非常简单,如下就是最基本的形式:
Parser parser = new Parser(TEXT); NodeList list = parser.parse(null);
其中的TEXT可以使纯文本HTML,也可以是一个url,HTMLParser内部会自动判断,但是其判断的逻辑非常简单:
length = resource.length (); html = false; for (int i = 0; i < length; i++) { ch = resource.charAt (i); if (!Character.isWhitespace (ch)) { if ('<' == ch) html = true; break; } }
只是判断了首个不为空白的字符是否为<
。
接下来我们主要看Parser的parse方法:
public NodeList parse (NodeFilter filter) throws ParserException { NodeIterator e; Node node; NodeList ret; ret = new NodeList (); for (e = elements (); e.hasMoreNodes (); ) { node = e.nextNode (); if (null != filter) node.collectInto (ret, filter); else ret.add (node); } return (ret); }
整体来看,这个函数并没有做什么东西,唯一可能复杂的就是在变量e(NodeIterator)的获取上,我们追进elements方法:
public NodeIterator elements () throws ParserException { return (new IteratorImpl (getLexer (), getFeedback ())); }
这个方法最终返回了IteratorImpl实例,getLexer方法获取了前面说过负责将Html转换为Node节点的Lexer,Lexer的实例是在Parser的构造函数中创建的,getFeedback方法返回的是ParserFeedback的一个实例,它的主要作用就是输出一些信息。所以,我们主要来看下IteratorImpl的构造函数的实现:
public IteratorImpl (Lexer lexer, ParserFeedback fb) { mLexer = lexer; mFeedback = fb; mCursor = new Cursor (mLexer.getPage (), 0); }
首先,缓存变量lexer与fb,紧接着,生成Cursor变量,这个Cursor用来表示当前处理的位置信息。
紧接着,我们来看IteratorImpl的hasMoreNodes方法:
public boolean hasMoreNodes() throws ParserException { boolean ret; mCursor.setPosition (mLexer.getPosition ()); ret = Page.EOF != mLexer.getPage ().getCharacter (mCursor); // more characters? return (ret); }
这里需要明确的是,mLexer是用来处理HTML的,所以它知道当前处理的位置,而这个位置,就用cursor表示。Page表示整个HTML文档,所以,它可以根据cursor的信息来查询当前cursor所对应的字符。因此,上述函数翻译过来就是查看当前处理的节点是否为结束符,如果是,则表示没有更多节点了,返回false。
接下来,来看nextNode函数,这里省略一些异常处理:
ret = mLexer.nextNode (); if (null != ret) { // kick off recursion for the top level node if (ret instanceof Tag) { tag = (Tag)ret; if (!tag.isEndTag ()) { // now recurse if there is a scanner for this type of tag scanner = tag.getThisScanner (); if (null != scanner) { stack = new NodeList (); ret = scanner.scan (tag, mLexer, stack); } } } } return ret;
首先,这个函数的前面逻辑交由了lexer的nextNode函数,所以,lexer的nextNode函数我们肯定要跟进,但这里我们先存个档,记为A,因为一会会回到这里。我们追进nextNode,
public Node nextNode (boolean quotesmart) throws ParserException { int start; char ch; Node ret; // debugging suppport if (-1 != mDebugLineTrigger) { Page page = getPage (); int lineno = page.row (mCursor); if (mDebugLineTrigger < lineno) mDebugLineTrigger = lineno + 1; // trigger