前言

本文关于小百合论坛的爬取,共458个板块,每个板块1000个的帖子,主要借助开源包htmlparser,下面上分析过程和代码。注意本文在一篇ibm的文章的基础上进行修改。

更新

我另外简单实现了一个MapReduce版本的,不过实现的不好,权当参考一下吧.奉上github地址,上面有简单介绍.


分析过程及代码

关于爬虫

爬虫大家都知道,由一个种子页(seed)出发,分析该页,获取页面链接,加入未访问的列表中,从未访问的表中取链接,访问,重复如上步骤即可,要注意的就是要维护一个已访问的网页列表,以避免重复访问。然而这种广义的爬虫并不符合特定的情况,例如我现在要爬的bbs板块帖子,遇到具体问题,还是得具体分析。


小百合bbs爬取过程

基本的爬取步骤如下:

  • 首先获得小百合bbs的所有板块链接
  • 对于每一个板块,获取至多1000条帖子的链接
  • 对于每一条链接,获取帖子内容并存储

获取所有板块链接

那么,最好有个入口页面可以把所有板块的链接都收集好,直接爬就好,幸运的是小百合贴心的有个叫做全部讨论区的版面,lucky~省下不少功夫.
这一部分的代码如下(BoardParser.java):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.mcl.crawler;
import java.util.HashSet;
import java.util.Set;
import org.htmlparser.Node;
import org.htmlparser.Parser;
import org.htmlparser.filters.NodeClassFilter;
import org.htmlparser.tags.LinkTag;
import org.htmlparser.util.NodeList;
import org.htmlparser.util.ParserException;
public class BoardParser {
// 获取bbs网站上的所有板块链接
private Set<String> extracLinks(String url, LinkFilter filter) {
Set<String> links = new HashSet<String>();
try {
Parser parser = new Parser(url);
parser.setEncoding("UTF-8");
// linkFilter 来设置过滤 <a> 标签
NodeClassFilter linkFilter = new NodeClassFilter(LinkTag.class);
// 得到所有经过过滤的标签
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)) {
// 将一般模式切换成主题模式
String stlink = linkUrl.replace("bbsdoc", "bbstdoc");
links.add(stlink);
}
}
}
} catch (ParserException e) {
e.printStackTrace();
}
return links;
}
// 获取板块链接并切换成主题模式
public Set<String> getBoards(String seed) {
Set<String> links = extracLinks(seed, new LinkFilter() {
// 提取以 http://bbs.nju.edu.cn/bbsdoc 开头的链接
public boolean accept(String url) {
if (url.startsWith("http://bbs.nju.edu.cn/bbsdoc"))
return true;
else
return false;
}
});
// System.out.println(links.size());
// for(String link : links){
// System.out.println(link);
// }
return links;
}
// 测试的 main 方法
// public static void main(String[]args){
// BoardParser.getBoards("http://bbs.nju.edu.cn/bbsall");
// }
}


对于每一个板块,获取至多1000条帖子的链接

然后有了页面的链接,就要考虑怎么获取1000个帖子,我们并不想爬取回复,直接爬取主题,因此你可以看到上面将链接中的bbsdoc换成了bbstdoc,这就是主题模式。注意到bbs有个上一页的链接,那么获取这个链接,可以获得更多的帖子,不断反复,直到获取1000个帖子或没有更多的帖子.
以下是主要处理代码(FileParser.java):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package com.mcl.crawler;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.htmlparser.Node;
import org.htmlparser.Parser;
import org.htmlparser.filters.NodeClassFilter;
import org.htmlparser.tags.LinkTag;
import org.htmlparser.util.NodeList;
import org.htmlparser.util.ParserException;
//获取bbs网站上某板块的所有帖子链接
public class FileParser {
//每个板块爬取1000个左右的帖子
private final int LINK_UP = 1000;
Map<String,String> links = new HashMap<String,String>();
private String extracLinks(String url,LinkFilter filter) {
//下一页的链接
String nextPage = null;
try {
Parser parser = new Parser(url);
parser.setEncoding("gb2312");
// linkFilter 来设置过滤 <a> 标签
NodeClassFilter linkFilter = new NodeClassFilter(
LinkTag.class);
// 得到所有经过过滤的标签
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(link.getLinkText().equals("上一页")){
nextPage = linkUrl;
}
if(filter.accept(linkUrl) && links.size() < LINK_UP){
String title = link.getLinkText();
links.put(linkUrl, title);
}
}
}
} catch (ParserException e) {
e.printStackTrace();
}
return nextPage;
}
//获取板块链接并切换成主题模式
public Map<String, String> getFileUrls(String seed)
{
System.out.println("seed: "+ seed);
links.clear();
int linkCount = 0;
final String rule = seed.replace("bbstdoc", "bbstcon");
String list = seed;
while(linkCount < LINK_UP){
list = extracLinks(list,new LinkFilter()
{
//提取以 http://bbs.nju.edu.cn/bbstcon?board=... 开头的链接
public boolean accept(String url) {
if(url.startsWith(rule))
return true;
else
return false;
}
});
linkCount = links.size();
//如果没有下一页的链接了
if(list == null){
break;
}
try {
Thread.sleep(360);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("共 "+ links.size()+" 篇帖子");
for (Entry<String, String> entry : links.entrySet()) {
System.out.println(entry.getKey()+" "+entry.getValue());
}
return links;
}
//测试的 main 方法
public static void main(String[]args){
FileParser parser = new FileParser();
parser.getFileUrls("http://bbs.nju.edu.cn/bbstdoc?board=Blog");
}
}


对于每一条链接,获取帖子内容并存储

得到帖子的链接,下面就是获取帖子的内容,我们获取table->tbody->第二个tr->td->pre中的纯文本。
代码如下(FileDownLoader):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
package com.mcl.crawler;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import org.htmlparser.NodeFilter;
import org.htmlparser.Parser;
import org.htmlparser.filters.AndFilter;
import org.htmlparser.filters.HasAttributeFilter;
import org.htmlparser.filters.NodeClassFilter;
import org.htmlparser.filters.TagNameFilter;
import org.htmlparser.tags.TableTag;
import org.htmlparser.util.NodeList;
import org.htmlparser.util.ParserException;
public class FileDownLoader {
public static boolean createFile(String destFileName) {
File file = new File(destFileName);
if(file.exists()) {
System.out.println("创建单个文件" + destFileName + "失败,目标文件已存在!");
return false;
}
if (destFileName.endsWith(File.separator)) {
System.out.println("创建单个文件" + destFileName + "失败,目标文件不能为目录!");
return false;
}
//判断目标文件所在的目录是否存在
if(!file.getParentFile().exists()) {
//如果目标文件所在的目录不存在,则创建父目录
//System.out.println("目标文件所在目录不存在,准备创建它!");
if(!file.getParentFile().mkdirs()) {
System.out.println("创建目标文件所在目录失败!");
return false;
}
}
//创建目标文件
try {
if (file.createNewFile()) {
return true;
} else {
return false;
}
} catch (IOException e) {
e.printStackTrace();
System.out.println("创建单个文件" + destFileName + "失败!" + e.getMessage());
return false;
}
}
/**保存网页字节数组到本地文件
* filePath 为要保存的文件的相对地址
*/
private void saveToLocal(String content,String filePath)
{
try {
createFile(filePath);
OutputStreamWriter osw;
String encoding = "UTF-8";
osw = new OutputStreamWriter(
new FileOutputStream(filePath), encoding);
BufferedWriter bw = new BufferedWriter(osw);
bw.write(content);
bw.close();
osw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/*下载 url 指向的网页的帖子内容并存储*/
public String downloadFile(String url,String filePath)
{
System.out.println("filePath: "+filePath);
Parser parser;
try {
parser = new Parser(url);
parser.setEncoding("UTF-8");
NodeList tableOfPre1 = parser.extractAllNodesThatMatch(
(NodeFilter) new AndFilter(new NodeClassFilter(TableTag.class), new HasAttributeFilter("class", "main")));
if(tableOfPre1 != null && tableOfPre1.size() > 0) {
// 获取指定 table 标签的子节点中的 <tbody> 节点
NodeList tList = tableOfPre1.elementAt(0).getChildren().extractAllNodesThatMatch (new TagNameFilter("tr"), true);
//第二列第一个tag:pre
String text = tList.elementAt(1).getFirstChild().toPlainTextString();
//System.out.println(text);
saveToLocal(text,filePath);
}
} catch (ParserException e) {
e.printStackTrace();
}
return null;
}
//测试的 main 方法
public static void main(String[]args)
{
FileDownLoader downLoader = new FileDownLoader();
downLoader.downloadFile("http://bbs.nju.edu.cn/bbstcon?board=Blog&file=M.1373384270.A","temp/Blog/我私人区最后一篇文章自动消失了,能找回吗?.txt");
}
}


main文件

最后把这些步骤串起来,分为3层进行处理,因为帖子名称可能重复,因此在生成的文件名中加入帖子的id以保证唯一性,最后因为小百合连续获取帖子会出错,因此我们间隔一段时间获取数据,这个时间需要自己测试.
主要代码如下(Crawler.java):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.mcl.crawler;
import java.util.Dictionary;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
public class Crawler {
private final String dir = "temp";
/* 爬取方法*/
public void crawling(String seed)
{
FileParser fileParser = new FileParser();
BoardParser boardParser = new BoardParser();
FileDownLoader downLoader=new FileDownLoader();
//第一层获取所有板块帖子
Set<String> boardsSet = boardParser.getBoards(seed);
for (String visitUrl : boardsSet) {
String board = visitUrl.trim().split("=")[1];
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Map<String, String> fileMap = fileParser.getFileUrls(visitUrl);
//第三层,下载每个帖子的内容
for (Entry<String, String> entry : fileMap.entrySet()) {
String id = entry.getKey().split("=")[2];
String path = dir+"/"+board+"/"+entry.getValue().trim()+"_"+id+".txt";
downLoader.downloadFile(entry.getKey(),path);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
//main 方法入口
public static void main(String[]args)
{
Crawler crawler = new Crawler();
crawler.crawling("http://bbs.nju.edu.cn/bbsall");
}
}


代码上传到github

最后,因为htmlparser还会依赖一些其他的包,为了方便起见,已把代码发到github上,希望能够帮到你。


参考