上篇:Java-网络爬虫(二)
上篇文章介绍了 webMagic
,通过一个简单的入门案例,对 webMagic
的核心对象和四大组件都做了简要的说明,以下内容则是继续对 webMagic
的讲解
爬虫按照系统结构和实现技术,大致可以分为以下几种类型:
通用网络爬虫(General Purpose Web Crawler)
也被称为全网爬虫,这种爬虫的爬取目标资源在全互联网中,爬取目标数据巨大。它主要用于为大型搜索引擎和门户网站采集数据。这类爬虫对于爬行速度和存储空间要求较高,对于爬行页面的顺序要求相对较低,同时由于待刷新的页面比较多,通常采用并行工作方式,但需要较长时间才能刷新一次页面,简单的说就是互联网上抓取所有数据。通用网络爬虫的基本构成包括初始 URL 集合、URL 队列、页面爬行模块、页面分析模块、页面数据库、链接过滤模块等。
聚焦网络爬虫(Focused Web Crawler)
也被称为主题网络爬虫,这种爬虫选择性地爬取那些与预先定义好的主题相关的页面。聚焦网络爬虫的目标是只抓取互联网上某一种数据。和通用网络爬虫相比,聚焦爬虫只需要爬行与主题相关的页面,极大地节省了硬件和网络资源,保存的页面也由于少量而更新快,还可以很好地满足一些特定人群对特定领域信息的需求。
量式网络爬虫(Incremental Web Crawler)
这种爬虫会不断爬取数据,但仅爬取新产生的或者已经发生变化的网页,它能够在一定程度上保证所爬行的页面是尽可能新的页面。和周期性爬行和刷新页面的网络爬虫相比,增量式爬虫只会在需要的时候爬行新产生或发生更新的页面,并不重新下载没有发生变化的页面,可有效减少数据下载量,及时更新已爬行的网页,减小时间和空间上的耗费,但是增加了爬行算法的复杂度和实现难度。
深层网络爬虫(Deep Web Crawler)
也被称为深网爬虫,这种爬虫主要抓取隐藏在搜索表单后的、不能通过静态链接获取的网页。这些页面只有当用户提交一些关键词才能获得。
在很多情况下,当我们爬取一个页面的信息时,要通过一些链接进入到其它的页面,进而继续爬取更多的信息
从上文介绍 webMagic
的原理中可以知道,只需要将待处理的 Request
放入 Scheduler
中就行了,Spider
会从 Scheduler
拉取 Requset
进行抓取,可以通过 page.addTargetRequests(Iterable
实现这一步,代码如下:
@Override
public void process(Page page) {
// 解析处理
...
// 待抓取的 URL
List<String> waitUrls = new ArrayList<>();
waitUrls.add("url_1");
waitUrls.add("url_2");
waitUrls.add("url_3");
// 将待抓取的 URL 追加到 targetRequests 中
page.addTargetRequests(waitUrls);
// 持久化处理
...
}
同样我们可以去追寻源码,进入到 addTargetRequests(Iterable
方法中:
好像也没有看到将 Request
放入到 Scheduler
中,再看回 spider.run()
方法:
进入到 processRequest(Request request)
方法:
从上述源码可知,在运行完 process()
方法后会进入到一个 extractAndAddRequests()
方法来添加一些额外的 Requests
,而这些 Requests
其实就是前面通过 page.addTargetRequests()
添加进去的,不妨再看看 extractAndAddRequests()
这个方法
到这里就可以看到通过 page.addTargetRequests()
这个方法确实会将待处理的 URL
全部推送至 Scheduler
中
倘若出现了这么一种场景,在页面 web_1
中有跳转到页面 web_2
的 URL
,而在网页 web_2
中也存在着跳转到网页 web_1
的 URL
那么就会出现这样一种现象,就是 web_1
和 web_2
这两个页面会被无休止的重复解析,这显然是不合理的,所以我们需要记录下已被解析过的页面,让其不能重复解析,这就是页面的去重
对于页面的去重,可以采取很多种方式,这里我就列举三种常用的方法:
关于布隆过滤器的原理可参见博客:Java-布隆过滤器的实现
实际上 webbMagic
提供的 Scheduler
组件已经帮我们解决了上述问题,Scheduler
不仅会将待抓取的 URL
放到队列中进行管理,还会对比已抓取的 URL
进行去重
webMagic
内置了几个常用的 Scheduler
,如果只是本地执行规模比较小的爬虫,基本无需定制 Scheduler
类 | 说明 | 备注 |
---|---|---|
DuplicateRemovedScheduler | 抽象基类,提供了一些模板方法 | 继承它可以实现自己的功能 |
QueueScheduler | 使用内存队列保存待抓取的 URL |
如果数据量比较庞大的话,可能会造成内存溢出 |
PriorityScheduler | 使用带有优先级的内存队列保存待抓取的 URL |
耗费内存较 QueueScheduler 更大,但是当设置了 request.priority 之后,只能使用 PriorityScheduler 才可使优先级生效 |
FileCacheQueueScheduler | webMagic-extension 提供,使用文件保存抓取 URL ,可以在关闭程序并下次启动时,从之前抓取到的 URL 继续抓取 |
需要指定文件存放路径,会建立 urls.txt 和 cursor.txt 两个文件 |
RedisScheduler | webMagic-extension 提供,使用 redis 保存抓取队列,可进行多台机器同时合作抓取 |
需要安装并启动 redis |
RedisPriorityScheduler | webMagic-extension 提供,使用 redis 保存抓取队列,可设置优先级 |
需要安装并启动 redis |
去重部分被单独抽象成了一个接口 — DuplicateRemover
public interface DuplicateRemover {
boolean isDuplicate(Request var1, Task var2);
void resetDuplicateCheck(Task var1);
int getTotalRequestsCount(Task var1);
}
从而可以为同一个 Scheduler
选择不同的去重方式,以适配不同的需要,目前提供了两种去重方式:
类 | 说明 |
---|---|
HashSetDuplicateRemover | 使用 HashSet 来进行去重,占用内存较大 |
BloomFilterDuplicateRemover | webMagic-extension 提供,使用 BloomFilter 来进行去重,占用内存较小,但是可能漏抓页面 |
除了 RedisScheduler
和 RedisPriorityScheduler
是使用 redis
的 set
进行去重,其它的 Scheduler
默认使用 HashSetDuplicateRemover
进行去重的
我们可以通过类图类验证,DuplicateRemovedScheduler
是其它的 Scheduler
的基类,在 DuplicateRemovedScheduler
中使用的就是 HashSetDuplicateRemover
,所以其它的 Scheduler
也继承了这一点
但是 RedisScheduler
实现了 DuplicateRemover
接口,重写了其中的去重逻辑,使用 set
来存储 url
,而 RedisPriorityScheduler
又继承了 RedisScheduler
,所以这两个是使用了 redis
的 set
进行去重的
如果需要使用到布隆过滤器进行去重,则需要进行设置
在设置之前还需要先添加 guava
依赖,因为 webMagic-extension
中使用的布隆过滤器是 guava
中的 BloomFilter
maven
导入 guava
依赖
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>33.0.0-jreversion>
dependency>
代码设置 Scheduler
使用布隆过滤器
// 创建 scheduler
QueueScheduler scheduler = new QueueScheduler();
// 设置 scheduler 使用布隆过滤器,预计存放一百万条数据
scheduler.setDuplicateRemover(new BloomFilterDuplicateRemover(1000000));
// Spider 设置 scheduler
spider.setScheduler(scheduler);
下载食品营养成分查询平台的所有页面持久化到本地磁盘中
分析:
URL
中FilePipeline
代码如下:
import com.google.common.base.Charsets;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.pipeline.FilePipeline;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.scheduler.BloomFilterDuplicateRemover;
import us.codecraft.webmagic.scheduler.QueueScheduler;
import us.codecraft.webmagic.selector.Html;
import java.util.Collections;
import java.util.List;
public class WebMagicDemo01 implements PageProcessor {
@Override
public void process(Page page) {
// 获取当前页面的 Html 对象
Html html = page.getHtml();
// 获取页面上所有的链接
List<String> links = html.links().all();
// 放入待处理 url 中
page.addTargetRequests(links);
// 将 html 内容放置 resultItems 中
page.putField("html", html.get());
}
@Override
public Site getSite() {
// 返回自定义 Site
return Site.me()
// 设置字符集
.setCharset(Charsets.UTF_8.name())
// 设置超时时间:5000(单位毫秒)
.setTimeOut(5000)
// 设置重试间隔时间:3000(单位毫秒)
.setRetrySleepTime(3000)
// 设置重试次数:5
.setRetryTimes(5);
}
public static void main(String[] args) {
// 创建 spider
Spider spider = Spider.create(new WebMagicDemo01());
// 创建 scheduler
QueueScheduler scheduler = new QueueScheduler();
// 设置 scheduler 使用布隆过滤器,预计存放一百万条数据
scheduler.setDuplicateRemover(new BloomFilterDuplicateRemover(1000000));
// Spider 设置 scheduler
spider.setScheduler(scheduler);
// 创建 filePipeline
FilePipeline filePipeline = new FilePipeline();
// 设置存放路径
filePipeline.setPath("D:\\web-magic\\download-page");
// Spider 设置 pipeline
spider.setPipelines(Collections.singletonList(filePipeline));
// 设置初始 URL
spider.addUrl("http://yycx.yybq.net/");
// 开启 2 个线程
spider.thread(2);
// 异步爬取
spider.runAsync();
}
}
可以看到指定文件夹下就开始下载这个网站的页面文件了
将下载的 html
文件内容和网页源码对比,可以看见基本上是一致的,不过多了一点内容
在游览器上打开下载的 html
文件
可以看到虽然下载的 html
文件中的内容和网页源代码几乎一样,但是却没有样式,图片也显示不了,链接跳转过去也是 404
,原因是在源码中有关资源的路径使用的都是相对路径,而本地没有这些资源当然访问不了
如果想要解决上述问题:
html
文件,资源部分未能加载解决方案:
FilePipeline
去除打印多余部分的代码改进后代码:
import com.google.common.base.Charsets;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import us.codecraft.webmagic.*;
import us.codecraft.webmagic.pipeline.FilePipeline;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.scheduler.BloomFilterDuplicateRemover;
import us.codecraft.webmagic.scheduler.QueueScheduler;
import us.codecraft.webmagic.selector.Html;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
public class WebMagicDemo01 implements PageProcessor {
@Override
public void process(Page page) {
// 获取当前页面的 Html 对象
Html html = page.getHtml();
// 获取页面上所有的链接
List<String> links = html.links().all();
// 放入待处理 url 中
page.addTargetRequests(links);
// 获取所有的 src 属性
Document document = html.getDocument();
Elements srcElements = document.select("script,img");
for (Element element : srcElements) {
if (StringUtils.isNotBlank(element.baseUri())) {
String src = element.attr("abs:src");
// 重新设置属性
element.attr("src", src);
}
}
// 获取所有的标签属性
Elements aElements = document.select("a,link");
for (Element element : aElements) {
if (StringUtils.isNotBlank(element.baseUri())) {
String href = element.attr("abs:href");
// 重新设置属性
element.attr("href", href);
}
}
// 将 html 内容放置 resultItems 中
page.putField("html", html.get());
}
@Override
public Site getSite() {
// 返回自定义 Site
return Site.me()
// 设置字符集
.setCharset(Charsets.UTF_8.name())
// 设置超时时间:5000(单位毫秒)
.setTimeOut(5000)
// 设置重试间隔时间:3000(单位毫秒)
.setRetrySleepTime(3000)
// 设置重试次数:5
.setRetryTimes(5);
}
public static void main(String[] args) {
// 创建 spider
Spider spider = Spider.create(new WebMagicDemo01());
// 创建 scheduler
QueueScheduler scheduler = new QueueScheduler();
// 设置 scheduler 使用布隆过滤器,预计存放一百万条数据
scheduler.setDuplicateRemover(new BloomFilterDuplicateRemover(1000000));
// Spider 设置 scheduler
spider.setScheduler(scheduler);
// 创建 filePipeline
FilePipeline filePipeline = new MyFilePipeline();
// 设置存放路径
filePipeline.setPath("D:\\web-magic\\download-page");
// Spider 设置 pipeline
spider.setPipelines(Collections.singletonList(filePipeline));
// 设置初始 URL
spider.addUrl("http://yycx.yybq.net/");
// 开启 2 个线程
spider.thread(2);
// 异步爬取
spider.runAsync();
}
}
/**
* 继承 FilePipeline
*/
class MyFilePipeline extends FilePipeline {
/**
* 重写 FilePipeline 中的 process 方法
* 去除打印多余部分的代码
*/
@SuppressWarnings("all")
public void process(ResultItems resultItems, Task task) {
String path = this.path + PATH_SEPERATOR + task.getUUID() + PATH_SEPERATOR;
try {
PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(new FileOutputStream(this.getFile(path + DigestUtils.md5Hex(resultItems.getRequest().getUrl()) + ".html")), "UTF-8"));
Iterator var5 = resultItems.getAll().entrySet().iterator();
while(true) {
while(var5.hasNext()) {
Map.Entry<String, Object> entry = (Map.Entry)var5.next();
if (entry.getValue() instanceof Iterable) {
Iterable value = (Iterable)entry.getValue();
Iterator var8 = value.iterator();
while(var8.hasNext()) {
Object o = var8.next();
printWriter.println(o);
}
} else {
printWriter.println((String)entry.getValue());
}
}
printWriter.close();
break;
}
} catch (IOException var10) {
var10.printStackTrace();
}
}
}
再使用游览器打开新下载好的 html
文件就会能看到,其样式也官方网站的效果一样了
PS:以上案例只做学习爬虫使用,切勿恶意攻击他人网站
上篇:Java-网络爬虫(二)