Jsoup 源码分析

原创
2013/09/17 17:36
阅读数 347

一、Jsoup 在类的设计上许多名字采用和 java 本身自带的 xml 解析器的类的名字一样,这样很容易让人误会,Jsoup 在设计过程中沿用了 xml 解析器,其实这种观念是错误的,jsoup 之所以这么出色,是因为,Jsoup使用了一套自己的DOM对象体系,和Java XML API互不兼容。这样做的好处是从XML的API里解脱出来,使得代码精炼了很多。这篇文章会说明Jsoup的DOM结构,DOM的遍历方式。

二、先来看一下 Jsoup 是如何设计 Dom 树 Jsoup Api 中对 Node 节点的描述

可以通过下面的类图对 Jsoup Dom 类的设计有一个直观的了解: Jsoup Dom 类图

可以看到在整个 Dom树的设计中,Node 类的地位是非常重要的,下面我分析一下 Node 类, 这是 Node 类的声明部分: public abstract class Node implements Cloneable 这是 Node 类的属性:

<!-- lang: java -->
Node parentNode; //Node 类指向父节点的引用
List<Node> childNodes; //用一个 List 保存对所有 子节点的引用
Attributes attributes; //对自身属性对象 Attributes 的引用 (Attribute 只是对 LinkedHashMap 的一个包装)
String baseUri; //Node 类指向父节点的引用 本身网页的 URL 地址,主要是用于将相对地址变为绝对地址
int siblingIndex; //在兄弟节点中的位置 由父节点的 childNodes 计算得到

Node类实现了一颗 DOM 树中一个 节点应该有的所有方法,其中有几个方法值得研究:

第一个方法:public String attr(String attributeKey) 这个方法的作用是:根据属性名,获取 属性值对应的 value 比如 class,得到 class 属性的 属性值 还有一个非常有用的作用是:他可以获取绝对地址,只要在需要获取绝对地址的属性名前面加上“abs:属性名” 下面贴上源代码:

<!-- lang: java -->
public String attr(String attributeKey) {
    Validate.notNull(attributeKey);

    if (attributes.hasKey(attributeKey))
        return attributes.get(attributeKey);
    //如果attributeKey 以 abs: 开头,不分大小写则获取 绝对地址返回
    else if (attributeKey.toLowerCase().startsWith("abs:"))
        return absUrl(attributeKey.substring("abs:".length()));
    else return "";
}

下面贴上有用的 public String absUrl(String attributeKey) 方法:

<!-- lang: java -->
public String absUrl(String attributeKey) {
    Validate.notEmpty(attributeKey);
    //先获取属性值,即有可能是相对地址
    String relUrl = attr(attributeKey);
    //没找到属性值,则返回空值
    if (!hasAttr(attributeKey)) {
        return ""; // nothing to make absolute with
    } else {
        URL base;
        try {
            try {
                //判断一下用户给的 uri 是否是正常的
                base = new URL(baseUri);
            } catch (MalformedURLException e) {
                // the base is unsuitable, but the attribute may be abs on its own, so try that
                URL abs = new URL(relUrl);
                return abs.toExternalForm();
            }
            // workaround: java resolves '//path/file + ?foo' to '//path/?foo', not '//path/file?foo' as desired
            if (relUrl.startsWith("?"))
                relUrl = base.getPath() + relUrl;
            URL abs = new URL(base, relUrl);
            return abs.toExternalForm();
        } catch (MalformedURLException e) {
            return "";
        }
    }
}

非常容易理解,以前是自己实现将相对地址变为绝对地址,以前的代码也贴上:

//给定相对地址,给定基本地址,返回绝对地址

<!-- lang: java -->
private  String absURL(String link, String url) {
	String returnLink = "";
	if(link.indexOf("http") != 0) {
		String[] cutUrl = url.split("\\/");
		int len = cutUrl.length;
		if(link.indexOf("../") == 0) {
			String[] tempLinks = link.split("\\/");
			int j = 1;
			link = "";
			for (; j < tempLinks.length-1; j++) {
				link = link + tempLinks[j] + "/";
			}
			link += tempLinks[j];
			len = len -1;
			for (int i=0; i<len-1; i++) {
				returnLink += cutUrl[i]+"/";
			}
			returnLink += link;
		}else {
			if(link.indexOf("/") == 0 || link.indexOf("./") == 0 ) {
				String[] tempLinks = link.split("\\/");
				int j = 1;
				for (; j < tempLinks.length-1; j++) {
					link = tempLinks[j] + "/";
				}
				link += tempLinks[j];
			}
			for (int i=0; i<len-1; i++) {
				returnLink += cutUrl[i]+"/";
			}
			returnLink += link;
		}
	} else 
                    returnLink = link;
	return returnLink;
 }

呵呵,自己当初代码写的很粗糙,协议只判断了 http, 而且字符串处理功底不深,和别人的比起来更是小巫见大巫,加强学习和思考很重要

第二个方法:就是树的遍历,traverse(NodeVisitor nodeVisitor) 下面是 Jsoup 用循环的方式实现的深度优先遍历

<!-- lang: java -->
public void traverse(Node root) {
    Node node = root;
    int depth = 0; 
    while (node != null) {
        visitor.head(node, depth);
        if (node.childNodeSize() > 0) {
            node = node.childNode(0);
            depth++;
        } else {
            while (node.nextSibling() == null && depth > 0) {
                visitor.tail(node, depth);
                node = node.parent();
                depth--;
            }
            visitor.tail(node, depth);
            if (node == root)
                break;
            node = node.nextSibling();
        }
    }

}

我自己实现的递归深度优先遍历,递归的好处在于容易理解,但是对内存要求大:

<!-- lang: java -->
public static void depththFirstVisitor(Node root, int depth) {
	depth++;
	List<Node> childs = root.childNodes();
	if(childs == null || childs.size() == 0) return;
	for(Node curNode : childs) {
		visit(curNode, depth);
		depththFirstVisitor(curNode, depth);
	}
 }

下面是用队列实现的广度优先遍历:

<!-- lang: java -->
private static void breadthFirstVisitor(Node node, int depth) {
	Queue<Node> nodeQueue = new LinkedList<Node>();
	nodeQueue.offer(node);
	while(nodeQueue.size() != 0) {
		
		Node curNode = nodeQueue.poll();
		visit(curNode, 0);
		List<Node> childs = curNode.childNodes();
		for(Node child : childs) {
			nodeQueue.offer(child);
		}
	}
}	

这些算法是最基本的程序员的要求,要加强训练

展开阅读全文
打赏
0
1 收藏
分享
加载中
更多评论
打赏
0 评论
1 收藏
0
分享
返回顶部
顶部