目录
本系列文章记录了一个简单的网页爬虫的设计过程,设计过程主要采用面向对象设计思想。下面开始正式内容。
一个简单的爬虫一般由网页爬取(Crawler)和网页解析(Parser)两部分组成。这个系列,主要讨论网页爬取部分的设计。
什么是爬虫程序?
简单来说,爬虫程序就是从起始网址开始,按照某种规则遍历目标网站,并处理特定网页的程序。
爬虫软件设计
从爬虫程序的工作内容,大概可以梳理出以下概念。
起始网址(Start URLs)
起始网址是爬虫程序最先爬取的网址,是一次爬取任务的入口,入口可以有多个,一般会选择网站主页或者链接列表页作为入口。
链接(Link)
包括链接文字,URL,链接深度,父URL,起始网址是深度为0链接。
网页(Webpage)
对于简单爬虫,网页可以抽象为链接的集合和HTML文件,因为简单爬虫不对HTML包含的媒体内容进行信息抽取。
对于需要抽取特定网页信息的爬虫,根据抽取内容不同,需要对网页进行不同的抽象。比如抽取网页元数据的爬虫,需要解析HTML文件的所有meta标签信息;抽取关联数据(Linked Data)的爬虫,可能需要解析网页内部的json-ld;抽取视频内容的爬虫,需要关注视频标题、作者、视频链接、视频长度,还要下载视频文件。
网页是一个HTML文件,HTML的英文全称是 Hyper Text Markup Language,即超文本标记语言,超文本的含义是除了文本,还可以包含多种类型媒体内容。
遍历规则(Crawling Rule)
遍历规则由用户设置,这些规则决定了爬虫访问哪些链接。
例如,如果要采集某个网站的所有网页,那么遍历规则就是该网站域名下的所有链接。如果要便利某个网站的特定频道,遍历规则就要限定为某个字域名或者特定的父路径。如果只采集某个页面内的链接,那么遍历规则就要控制链接深度(如果规定起始URL深度为0,那么这里就要设置链接深度为1)。有时候,还需要采集并保存包含特定特征的网页,比如包含正文详情的页面。
从上面的分析,发现遍历规则这个概念可以细分为爬取范围和处理范围,爬取范围还包含了起始网址的概念。
爬取范围(Crawling Scope)
起始网址和遍历规则实际上约束了爬虫的爬取范围,可以对这个概念显示建模。爬取范围包括:
- 起始网址
- 要遍历的链接深度
- 要遍历的链接特征
- 最大遍历链接数量
- 是否只采集域名内链接
处理范围(Processing Scope)
需要后续处理的链接和网页规则,包括:
- URL特征
- 网页内部特征
- 最多处理网页数量
有了上面这些基础要素,就很容易定义一个爬取任务了。
爬取任务(Crawling Task)
爬取任务应该至少包含如下信息:
- 任务名称和ID
- 爬取范围
- 处理范围
- 任务调度约束
- 网页保存位置
- 其他所需的信息
爬虫(Crawler)
爬虫负责采集过程的控制。
- 输入,是一个爬取任务,包含了爬虫运行所需的基本信息和控制信息。
- 输出,是网页集合,这些网页可能需要保存到文件系统或者数据库中。
- 在采集过程中,需要根据爬取任务下载网页,同时满足采集间隔、采集总网页数等限制条件。因为网页之间链接关系构成了一张图,为了不重复地遍历这些网页,爬虫内部要维护待采集链接和已采集链接的集合,还要选择一种遍历方式:深度优先或者广度优先。
待采集链接集合(Target Links)
爬虫从每个网页内收集链接,把需要采集的链接放入待采集集合中。
已采集链接集合(Fetched Links)
爬虫把已经遍历过的链接放入已采集链接的集合,这样可以避免对相同链接进行重复采集。
下载器(Downloader)
爬虫遍历网页时,需要使用网页下载器,通常是一个Http客户端,有些场景需要通过代理访问目标网站。
代码示例
有了上述这些基本要素,就可以开始组装一个简单的网页爬虫了。
基本构造模块
public class Link {
String url;
int depth;
//getter setter
}
public class Webpage {
Set<Links> links;
String html;
//getter setter
}
interface CrawlingScope { //爬取范围
//起始网址
List<String> getStartUrls();
//哪些URL会继续爬取
boolean contains(Link link);
//最多爬取多少个链接
long maxToCrawl();
//爬取的最大深度
int getMaxHops();
}
interface ProcessingScope { //处理范围
//某个网页是否要被处理
boolean contains(Webpage webpage);
//最多处理多少个网页
long maxToProcess();
long maxToProcessPerSubDomain();
}
interface TargetLinks { //待采集链接集合
void add(Link link);
long size();
Link next();
void clear();
}
interface FetchedLinks { //已采集链接集合
boolean contains(Link link);
void add(Link link);
long total();
void clear();
}
class CrawlingTask { //爬取任务
String name;
private boolean enable;
CrawlingScope crawlingScope;
ProcessingScope processingScope;
}
interface Crawler { //爬虫
void crawl();
}
爬虫构建过程
主要是把CrawlingTask中的约束传递给Crawler。
//示意代码,忽略了部分实现细节
public class CrawlerBuilder {
public Crawler build(CrawlingTask task) {
CrawlerImpl crawler = new CrawlerImpl();
//其他信息略...
crawler.setCrawlingScope(task.getCrawlingScope());
crawler.setProcessingScope(task.getProcessingScope());
TargetLinksImpl targets = new TargetLinksImpl();
targets.addAll(task.startUrls());
crawler.setTargetLinks(targets);
FetchedLinksImpl fetched = new FetchedLinksImpl();
crawler.setFetchedLinks(fetched);
return crawler;
}
}
爬虫控制过程
//示意代码,忽略了部分实现细节
public class CrawlerImpl implements Crawler {
public void crawl() {
Link target = null;
while (null != (target = targetLinks.next())) {
try {
fetchAndProcess(target);
if (this.fetchedLinks.total() >= crawlingScope.maxToCrawl()) {
return;
}
TimeUnit.MILLISECONDS.sleep(this.crawlDelay);
} catch (Exception e) {
//处理错误信息,略
}
}
}
}
private void fetchAndProcess(Link target) {
//不在爬取范围内,略过
if (!this.crawlingScope.contains(target)) {
return;
}
//已经爬取过,略过
if (target.getDepth() > 0 && this.fetchedLinks.contains(target)) {
return;
}
Webpage webpage = fetch(target); //下载网页
if (processingScope.contains(webpage)) {
webpageRepository.add(webpage); //保存网页
}
Links allLinks = webpage.links();
for (Link link : allLinks) {
if (this.crawlingScope.contains(link) &&
!this.fetchedLinks.contains(link)) {
targetLinks.add(link); //保存链接
}
}
//当前链接为放入已采集集合
this.fetchedLinks.add(target);
}
更新:在后续文章中对上面的这段代码进行了重构,控制逻辑更加清晰。
简单爬虫设计(五)——重构控制流程
小结
通过这篇文章,大概描述了一个简单爬虫的建模过程。后续文章将对爬虫的各个组成部分的实现细节进行介绍。