前面,我们分别介绍了爬虫程序的 3 个部分:源码下载、内容解析、页面链接提取。现在,让我们把这些部分串起来,构建一个完整的简易爬虫程序。
一、爬虫流程
构建程序前,我们首先需要了解爬虫的具体流程。
一个简易的爬虫程序,具备以下流程:
若以文字表述,就是:
-
从任务库(可以是 MySQL 等关系型数据库)选取种子 URL;
-
在程序中初始化一个 URL 队列,将种子 URL 加入到队列中;
-
若 URL 队列不为空,则位于队头的 URL 出队;若 URL 队列为空,则退出程序;
-
程序根据出列的 URL,反射出对应的解析类,同时新建线程,开始解析任务;
-
程序将下载 URL 指向的网页,并判断该页面是详情页还是列表页(如博客中的博客详情与博文列表),若为详情页,则解析出页面内容并入库,若为列表页,则提取出页面链接,加入到 URL 队列中;
-
解析任务完成后,重复第 3 步。
二、程序结构
我们已经清楚爬虫的具体流程,现在,我们需要一个合理的程序结构来实现它。
首先,介绍一下该简易爬虫程序的主要结构组成:
类名 | 作用 |
---|---|
SpiderApplication.java | 程序入口,负责任务调度,初始化 URL 队列,线程调度 |
DownloadService.java | 下载服务类,负责下载 URL 指向的页面 |
PluginFactory.java | 插件工厂,根据 URL 反射对应的插件类(解析类) |
Plugin.java | 插件注解类,用于插件(解析类)的注解 |
AbstractPlugin.java | 抽象插件类,所有插件(解析类)的父类,包含公用方法 |
XmuPlugin.java | 具体插件类,负责指定 URL 的解析任务 |
然后,再了解一下程序中的工具类与实体类。
类名 | 作用 |
---|---|
LinkFilter.java | 链接过滤接口,规范过滤方法 |
LinkExtractor.java | 基于 htmlparser 的链接提取类 |
HttpUtil.java | 封装 HttpClient 的工具类 |
CommonUtil.java | 程序通用工具类 |
Task.java | 任务对象 |
HttpParams.java | http 请求对象 |
Proxy.java | 代理配置类 |
StructData.java | 结构化数据 |
最后,我们根据类的作用,将其放置到上述流程图中对应的位置中。具体的示意图如下所示:
现在,我们已经完成了实际流程到程序逻辑的转换。接下来,我们将通过源码的介绍,深入到程序中的各个细节。
三、任务调度、初始化队列
在简易爬虫程序中,任务调度、初始化队列都在 SpiderApplication 类中完成。
package main;
import entity.Task;
import factory.PluginFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import plugins.AbstractPlugin;
import java.util.*;
import java.util.zip.CRC32;
/**
* 应用入口
*
* @author panda
* @date 2017/10/28
*/
public class SpiderApplication {
private static final Logger logger = LoggerFactory.getLogger(SpiderApplication.class);
// URL 队列
private static Queue<String> urlQueue = new LinkedList<String>();
// URL 排重
private static Map<Long, Integer> urlPool = new HashMap<Long, Integer>();
public static void main(String[] args) {
// 实例化任务对象
Task task = Task.getBuilder()
.setUrl("http://sm.xmu.edu.cn/")
.build();
// 初始化 URL 队列
urlQueue.add(task.getUrl());
addUrlPool(task.getUrl(), 1);
// 循环调度
String taskUrl;
while ((taskUrl = urlQueue.poll()) != null) {
logger.info("当前任务URL:" + taskUrl + ",当前层深:" + getDepth(taskUrl));
try {
task.setUrl(taskUrl);
AbstractPlugin plugin = PluginFactory.getInstance().getPlugin(task);
plugin.run();
if (plugin.getUrlList() != null) {
int depth = getDepth(taskUrl) + 1;
for (String url : plugin.getUrlList()) {
if (!isUrlExist(url)) {
urlQueue.add(url);
addUrlPool(url, depth);
}
}
}
Thread.sleep(300);
} catch (Exception e) {
continue;
}
}
}
/**
* 添加链接到 url 池
*
* @param url
* @param depth
*/
private static void addUrlPool(String url, int depth) {
CRC32 c = new CRC32();
c.update(url.getBytes());
urlPool.put(c