一个简单的爬虫一般由网页爬取(Crawler)和网页解析(Parser)两部分组成。这个系列,主要讨论网页爬取部分的设计。
简单来说,爬虫程序就是从起始网址开始,按照某种规则遍历目标网站,并处理特定网页的程序。
从爬虫程序的工作内容,大概可以梳理出以下概念。
起始网址是爬虫程序最先爬取的网址,是一次爬取任务的入口,入口可以有多个,一般会选择网站主页或者链接列表页作为入口。
包括链接文字,URL,链接深度,父URL,起始网址是深度为0链接。
对于简单爬虫,网页可以抽象为链接的集合和HTML文件,因为简单爬虫不对HTML包含的媒体内容进行信息抽取。
对于需要抽取特定网页信息的爬虫,根据抽取内容不同,需要对网页进行不同的抽象。比如抽取网页元数据的爬虫,需要解析HTML文件的所有meta标签信息;抽取关联数据(Linked Data)的爬虫,可能需要解析网页内部的json-ld;抽取视频内容的爬虫,需要关注视频标题、作者、视频链接、视频长度,还要下载视频文件。
网页是一个HTML文件,HTML的英文全称是 Hyper Text Markup Language,即超文本标记语言,超文本的含义是除了文本,还可以包含多种类型媒体内容。
遍历规则由用户设置,这些规则决定了爬虫访问哪些链接。
例如,如果要采集某个网站的所有网页,那么遍历规则就是该网站域名下的所有链接。如果要便利某个网站的特定频道,遍历规则就要限定为某个字域名或者特定的父路径。如果只采集某个页面内的链接,那么遍历规则就要控制链接深度(如果规定起始URL深度为0,那么这里就要设置链接深度为1)。有时候,还需要采集并保存包含特定特征的网页,比如包含正文详情的页面。
从上面的分析,发现遍历规则这个概念可以细分为爬取范围和处理范围,爬取范围还包含了起始网址的概念。
起始网址和遍历规则实际上约束了爬虫的爬取范围,可以对这个概念显示建模。爬取范围包括:
需要后续处理的链接和网页规则,包括:
有了上面这些基础要素,就很容易定义一个爬取任务了。
爬取任务应该至少包含如下信息:
爬虫负责采集过程的控制。
爬虫从每个网页内收集链接,把需要采集的链接放入待采集集合中。
爬虫把已经遍历过的链接放入已采集链接的集合,这样可以避免对相同链接进行重复采集。
爬虫遍历网页时,需要使用网页下载器,通常是一个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);
}
更新:在后续文章中对上面的这段代码进行了重构,控制逻辑更加清晰。
简单爬虫设计(五)——重构控制流程
通过这篇文章,大概描述了一个简单爬虫的建模过程。后续文章将对爬虫的各个组成部分的实现细节进行介绍。
简单爬虫设计(二)——爬取范围
简单爬虫设计(三)——需要处理的网页范围
简单爬虫设计(四)——管理爬虫内部状态
简单爬虫设计(五)——重构控制流程