一、Jsoup 在类的设计上许多名字采用和 java 本身自带的 xml 解析器的类的名字一样,这样很容易让人误会,Jsoup 在设计过程中沿用了 xml 解析器,其实这种观念是错误的,jsoup 之所以这么出色,是因为,Jsoup使用了一套自己的DOM对象体系,和Java XML API互不兼容。这样做的好处是从XML的API里解脱出来,使得代码精炼了很多。这篇文章会说明Jsoup的DOM结构,DOM的遍历方式。
二、先来看一下 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);
}
}
}
这些算法是最基本的程序员的要求,要加强训练