您的位置:首页 > 理论基础 > 计算机网络

网络采集器Demo:Jsoup+Java多线程实现[爬虫](下)

2015-11-14 20:53 429 查看
ailab-mltk:http://blog.csdn.net/qdhy199148/article/details/49403585

下半部分主要是介绍Java的多线程编程。

我们得到了所有的有效链接和获取各个链接页面有效内容的方法,帮助大家回忆一下:

1.

public Set<ExactLinks> filterUrl(String seedUrl)


2.

public String getParagraphContent(String pageUrl)


这里要介绍的多线程编程模式是Java1.5之后推出的callable+future,Java1.5的这个多线程API相比之前老旧的runable还是提升了不少,主要有两点:

1.支持每个子线程能够传回returne值

2.加入了Java自己非常强大的线程池模式

所以啊,各位师弟今后再从什么《Java编程那些事儿》、《Java开发实战经典》这种老书中看到关于runable复杂的描述,就不必纠结下去了。

很多废话,接下来说一下callable+future多线程编程的两个重要部分

首先是线程部分,也就是以前rubable中的继承类和run方法,关于上面的爬虫问题,我们将每个子线程设计成爬取单个有效链接内部的有效文本内容,并存入指定路径下的文件里。线程类的代码如下:

public class FetchPageContentThread implements Callable<Boolean> {
private ExactLinks links;
private String folderPath;

public FetchPageContentThread(ExactLinks links, String folderPath) {
super();
this.links = links;
this.folderPath = folderPath;
}

@Override
public Boolean call() throws Exception {

System.out.println(this.links.getUrl());
String content = new FetchPaContentFromPage()
.getParagraphContent(this.links.getUrl());

Boolean flagSucc = false;
if (!content.equals("") && content != null) {
String filePath = this.folderPath + links.getUrlMD5() + ".txt";
flagSucc = new WriteContentIntoFile().writeContent(filePath,
content);
}

return flagSucc;
}

}


我们来分析一下,由于call方法不能传入参数,所以如果需要传入必要的参数,需要以构造方法初始化线程类成员变量的形式完成(~_~)。另外注意:call方法是可以有返回值的,返回值类型需要在两个地方声明,一个是传统的方法名前面,另一个在声明实现<span style="font-family: Arial, Helvetica, sans-serif;">callable<></span>接口中的方括号里,完成单线程全部的任务之后,返回要返回的值即可,如果没什么返回的,随便返回一个什么,接受的时候不要管他就行了。

线程类call方法中调用的WriteContentIntoFile方法如下:

public boolean writeContent(String filePath, String content) {

try {
File file = new File(filePath);
if (!file.exists()) {
file.createNewFile();
}

FileWriter fw = new FileWriter(file);
fw.write(content);

fw.close();

return true;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();

return false;
}
}


Java I/O,看看就可以了,没什么可说的。

基本的线程类编写好了,接下来我们需要调用他,调用刚才说明的线程类的代码:

public class ExecFetchSinaNewsContent {

private Set<ExactLinks> links;
private String folderPath;

public ExecFetchSinaNewsContent() {
super();
// TODO Auto-generated constructor stub
}

public ExecFetchSinaNewsContent(Set<ExactLinks> links, String folderPath) {
super();
this.links = links;
this.folderPath = folderPath;
}

public void initSinaLinks() {
FetchLinksFromPage fetchObj = new FetchLinksFromPage();
setLinks(fetchObj.filterUrl(CrawlSeedParam.SINA_NEWS));
}

public void initFileFolderPath() {
setFolderPath("./file/crawler_test/");
}

public void execFetchSinaContentThread() {

// TODO 改成手动传入参数
this.initSinaLinks();
this.initFileFolderPath();

// 创建线程池
ExecutorService exes = Executors.newFixedThreadPool(5);
Set<Future<Boolean>> setThreads = new java.util.HashSet<Future<Boolean>>();
for (ExactLinks links : getLinks()) {
// 创建线程任务
FetchPageContentThread fetchThread = new FetchPageContentThread(
links, getFolderPath());

// 提交线程任务
setThreads.add(exes.submit(fetchThread));
}

// 执行多线程任务
for (Future<Boolean> future : setThreads) {

try {
Boolean flagSucc = future.get();

// TODO delete print
System.out.println(flagSucc);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

public Set<ExactLinks> getLinks() {
return links;
}

public void setLinks(Set<ExactLinks> links) {
this.links = links;
}

public String getFolderPath() {
return folderPath;
}

public void setFolderPath(String folderPath) {
this.folderPath = folderPath;
}

}


载入全部有效链接、爬取内容的磁盘存放路径作为传入子线程的准备,接下来是调用多线程的关键,这里需要初始化一个ExecutorService对象,我们暂且称为exes,没有exes就不能正常地调用callable线程。exec的初始化有多种形式,介绍如下:

1.Executors.newCachedThreadPool()(Java线程池动态分配线程)

2.Executors.newFixedThreadPool(int);(Java线程池分配固定数量的同时运行线程数,其余排队)

3.Executors.newScheduledThreadPool(int);(Java线程池分配固定大小的线程占用资源数,超过排队,用的很少)

4.Executors.newSingleThreadExecutor();(Java线程池每次只分配一个激活的线程,其余排队,相当于单线程)

接下来,我们设计了一个线程集合,用于存储新建的线程任务。集合的类型是Future<Boolean>,Future就是之前提到的callable+future中的future,代表一个运行的线程,也可以理解为用来接收子线程call方法返回值工具,<>方括号中的变量类型就是在子线程中声明的返回值类型。

循环中,新建线程任务后,通过exes的submit方法提交线程(注意,此刻线程就开始运行了,一定要注意线程的生命周期)

后一个循环以线程集合中的每个线程作为基本单位,并用future变量接受各个子线程的返回值。这里也要注意,不要以为没什么可返回的就忽略了这一步,接受返回值的目的不但是接收所谓的子线程返回值,也有线程等待的功能(关注线程安全的童鞋应该清楚线程等待的重要性),全部线程结束后,你可以再次自有地进行线程操作。

好了,师弟们,结合上一节试试编写一个简单的多线程爬虫。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: