Java 多线程爬虫及分布式爬虫架构探索
这是 Java 爬虫系列博文的第五篇,在上一篇 Java 爬虫服务器被屏蔽,不要慌,咱们换一台服务器 中,我们简单的聊反爬虫策略和反反爬虫方法,主要针对的是 IP 被封及其对应办法。前面几篇文章我们把爬虫相关的基本知识都讲的差不多啦。这一篇我们来聊一聊爬虫架构相关的内容。
前面几章内容我们的爬虫程序都是单线程,在我们调试爬虫程序的时候,单线程爬虫没什么问题,但是当我们在线上环境使用单线程爬虫程序去采集网页时,单线程就暴露出了两个致命的问题:
- 采集效率特别慢,单线程之间都是串行的,下一个执行动作需要等上一个执行完才能执行
- 对服务器的CUP等利用率不高,想想我们的服务器都是 8核16G,32G 的只跑一个线程会不会太浪费啦
线上环境不可能像我们本地测试一样,不在乎采集效率,只要能正确提取结果就行。在这个时间就是金钱的年代,不可能给你时间去慢慢的采集,所以单线程爬虫程序是行不通的,我们需要将单线程改成多线程的模式,来提升采集效率和提高计算机利用率。
多线程的爬虫程序设计比单线程就要复杂很多,但是与其他业务在高并发下要保证数据安全又不同,多线程爬虫在数据安全上到要求不是那么的高,因为每个页面都可以被看作是一个独立体。要做好多线程爬虫就必须做好两点:第一点就是统一的待采集 URL 维护,第二点就是 URL 的去重, 下面我们简单的来聊一聊这两点。
维护待采集的 URL
多线程爬虫程序就不能像单线程那样,每个线程独自维护这自己的待采集 URL,如果这样的话,那么每个线程采集的网页将是一样的,你这就不是多线程采集啦,你这是将一个页面采集的多次。基于这个原因我们就需要将待采集的 URL 统一维护,每个线程从统一 URL 维护处领取采集 URL ,完成采集任务,如果在页面上发现新的 URL 链接则添加到 统一 URL 维护的容器中。下面是几种适合用作统一 URL 维护的容器:
- JDK 的安全队列,例如 LinkedBlockingQueue
- 高性能的 NoSQL,比如 Redis、Mongodb
- MQ 消息中间件
URL 的去重
URL 的去重也是多线程采集的关键一步,因为如果不去重的话,那么我们将采集到大量重复的 URL,这样并没有提升我们的采集效率,比如一个分页的新闻列表,我们在采集第一页的时候可以得到 2、3、4、5 页的链接,在采集第二页的时候又会得到 1、3、4、5 页的链接,待采集的 URL 队列中将存在大量的列表页链接,这样就会重复采集甚至进入到一个死循环当中,所以就需要 URL 去重。URL 去重的方法就非常多啦,下面是几种常用的 URL 去重方式:
- 将 URL 保存到数据库进行去重,比如 redis、MongoDB
- 将 URL 放到哈希表中去重,例如 hashset
- 将 URL 经过 MD5 之后保存到哈希表中去重,相比于上面一种,能够节约空间
- 使用 布隆过滤器(Bloom Filter)去重,这种方式能够节约大量的空间,就是不那么准确。
关于多线程爬虫的两个核心知识点我们都知道啦,下面我画了一个简单的多线程爬虫架构图,如下图所示:
上面我们主要了解了多线程爬虫的架构设计,接下来我们不妨来试试 Java 多线程爬虫,我们以采集虎扑新闻为例来实战一下 Java 多线程爬虫,Java 多线程爬虫中设计到了 待采集 URL 的维护和 URL 去重,由于我们这里只是演示,所以我们就使用 JDK 内置的容器来完成,我们使用 LinkedBlockingQueue 作为待采集 URL 维护容器,HashSet 作为 URL 去重容器。下面是 Java 多线程爬虫核心代码,详细代码以上传 GitHub,地址在文末:
/**
* 多线程爬虫
*/
public class ThreadCrawler implements Runnable {
// 采集的文章数
private final AtomicLong pageCount = new AtomicLong(0);
// 列表页链接正则表达式
public static final String URL_LIST = "https://voice.hupu.com/nba";
protected Logger logger = LoggerFactory.getLogger(getClass());
// 待采集的队列
LinkedBlockingQueue<String> taskQueue;
// 采集过的链接列表
HashSet<String> visited;
// 线程池
CountableThreadPool threadPool;
/**
*
* @param url 起始页
* @param threadNum 线程数
* @throws InterruptedException
*/
public ThreadCrawler(String url, int threadNum) throws InterruptedException {
this.taskQueue = new LinkedBlockingQueue<>();
this.threadPool = new CountableThreadPool(threadNum);
this.visited = new HashSet<>();
// 将起始页添加到待采集队列中
this.taskQueue.put(url);
}
@Override
public void run() {
logger.info("Spider started!");
while (!Thread.currentThread().isInterrupted()) {
// 从队列中获取待采集 URL
final String request = taskQueue.poll();
// 如果获取 request 为空,并且当前的线程采已经没有线程在运行
if (request == null) {
if (threadPool.getThreadAlive() == 0) {
break;
}
} else {
// 执行采集任务
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
processRequest(request);
} catch (Exception e) {
logger.error("process request " + request + " error", e);
} finally {
// 采集页面 +1
pageCount.incrementAndGet();
}
}
});
}
}
threadPool.shutdown();
logger.info("Spider closed! {} pages downloaded.", pageCount.get());
}
/**
* 处理采集请求
* @param url
*/
protected void processRequest(String url) {
// 判断是否为列表页
if (url.matches(URL_LIST)) {
// 列表页解析出详情页链接添加到待采集URL队列中
processTaskQueue(url);
} else {
// 解析网页
processPage(url);
}
}
/**
* 处理链接采集
* 处理列表页,将 url 添加到队列中
*
* @param url
*/
protected void processTaskQueue(String url) {
try {
Document doc = Jsoup.connect(url).get();
// 详情页链接
Elements elements = doc.select(" div.news-list > ul > li > div.list-hd > h4 > a");
elements.stream().forEach((element -> {
String request = element.attr("href");
// 判断该链接是否存在队列或者已采集的 set 中,不存在则添加到队列中
if (!visited.contains(request) && !taskQueue.contains(request)) {
try {
taskQueue.put(request);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}));
// 列表页链接
Elements list_urls = doc.select("div.voice-paging > a");
list_urls.stream().forEach((element -> {
String request = element.absUrl("href");
// 判断是否符合要提取的列表链接要求
if (request.matches(URL_LIST)) {
// 判断该链接是否存在队列或者已采集的 set 中,不存在则添加到队列中
if (!visited.contains(request) && !taskQueue.contains(request)) {
try {
taskQueue.put(request);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 解析页面
*
* @param url
*/
protected void processPage(String url) {
try {
Document doc = Jsoup.connect(url).get();
String title = doc.select("body > div.hp-wrap > div.voice-main > div.artical-title > h1").first().ownText();
System.out.println(Thread.currentThread().getName() + " 在 " + new Date() + " 采集了虎扑新闻 " + title);
// 将采集完的 url 存入到已经采集的 set 中
visited.add(url);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try {
new ThreadCrawler("https://voice.hupu.com/nba", 5).run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
我们用 5 个线程去采集虎扑新闻列表页看看效果如果?运行该程序,得到如下结果:
结果中可以看出,我们启动了 5 个线程采集了 61 页页面,一共耗时 2 秒钟,可以说效果还是不错的,我们来跟单线程对比一下,看看差距有多大?我们将线程数设置为 1 ,再次启动程序,得到如下结果:
可以看出单线程采集虎扑 61 条新闻花费了 7 秒钟,耗时差不多是多线程的 4 倍,你想想这可只是 61 个页面,页面更多的话,差距会越来越大,所以多线程爬虫效率还是非常高的。
分布式爬虫架构
分布式爬虫架构是一个大型采集程序才需要使用的架构,一般情况下使用单机多线程就可以解决业务需求,反正我是没有分布式爬虫项目的经验,所以这一块我也没什么可以讲的,但是我们作为技术人员,我们需要对技术保存热度,虽然不用,但是了解了解也无妨,我查阅了不少资料得出了如下结论:
分布式爬虫架构跟我们多线程爬虫架构在思路上来说是一样的,我们只需要在多线程的基础上稍加改进就可以变成一个简单的分布式爬虫架构。因为分布式爬虫架构中爬虫程序部署在不同的机器上,所以我们待采集的 URL 和 采集过的 URL 就不能存放在爬虫程序机器的内存中啦,我们需要将它统一在某台机器上维护啦,比如存放在 Redis 或者 MongoDB 中,每台机器都从这上面获取采集链接,而不是从 LinkedBlockingQueue 这样的内存队列中取链接啦,这样一个简单的分布式爬虫架构就出现了,当然这里面还会有很多细节问题,因为我没有分布式架构的经验,我也无从说起,如果你有兴趣的话,欢迎交流。
源代码:源代码
文章不足之处,望大家多多指点,共同学习,共同进步
最后
打个小广告,欢迎扫码关注微信公众号:「平头哥的技术博文」,一起进步吧。
Java 多线程爬虫及分布式爬虫架构探索的更多相关文章
- Java 多线程爬虫及分布式爬虫架构
这是 Java 爬虫系列博文的第五篇,在上一篇 Java 爬虫服务器被屏蔽,不要慌,咱们换一台服务器 中,我们简单的聊反爬虫策略和反反爬虫方法,主要针对的是 IP 被封及其对应办法.前面几篇文章我们把 ...
- Python分布式爬虫打造搜索引擎完整版-基于Scrapy、Redis、elasticsearch和django打造一个完整的搜索引擎网站
Python分布式爬虫打造搜索引擎 基于Scrapy.Redis.elasticsearch和django打造一个完整的搜索引擎网站 https://github.com/mtianyan/Artic ...
- Python分布式爬虫必学框架Scrapy打造搜索引擎
Python分布式爬虫必学框架Scrapy打造搜索引擎 部分课程截图: 点击链接或搜索QQ号直接加群获取其它资料: 链接:https://pan.baidu.com/s/1-wHr4dTAxfd51M ...
- 【Python3爬虫】爬取美女图新姿势--Redis分布式爬虫初体验
一.写在前面 之前写的爬虫都是单机爬虫,还没有尝试过分布式爬虫,这次就是一个分布式爬虫的初体验.所谓分布式爬虫,就是要用多台电脑同时爬取数据,相比于单机爬虫,分布式爬虫的爬取速度更快,也能更好地应对I ...
- 【Python3爬虫】学习分布式爬虫第一步--Redis分布式爬虫初体验
一.写在前面 之前写的爬虫都是单机爬虫,还没有尝试过分布式爬虫,这次就是一个分布式爬虫的初体验.所谓分布式爬虫,就是要用多台电脑同时爬取数据,相比于单机爬虫,分布式爬虫的爬取速度更快,也能更好地应对I ...
- 聚焦Python分布式爬虫必学框架Scrapy 打造搜索引擎视频教程
下载链接:https://www.yinxiangit.com/595.html 目录: 第1章 课程介绍介绍课程目标.通过课程能学习到的内容.和系统开发前需要具备的知识 第2章 windows下搭建 ...
- scrapy_redis分布式爬虫
文章来源:https://github.com/rmax/scrapy-redis Scrapy-Redis Documentation: https://scrapy-redis.readthedo ...
- 基于java的分布式爬虫
分类 分布式网络爬虫包含多个爬虫,每个爬虫需要完成的任务和单个的爬行器类似,它们从互联网上下载网页,并把网页保存在本地的磁盘,从中抽取URL并沿着这些URL的指向继续爬行.由于并行爬行器需要分割下载任 ...
- Cola:一个分布式爬虫框架 - 系统架构 - Python4cn(news, jobs)
Cola:一个分布式爬虫框架 - 系统架构 - Python4cn(news, jobs) Cola:一个分布式爬虫框架 发布时间:2013-06-17 14:58:27, 关注:+2034, 赞美: ...
随机推荐
- Java网络编程 -- Netty入门
Netty简介 Netty是一个高性能,高可扩展性的异步事件驱动的网络应用程序框架,它极大的简化了TCP和UDP客户端和服务器端网络开发.它是一个NIO框架,对Java NIO进行了良好的封装.作为一 ...
- .NET Core 微信小程序支付——(统一下单)
最近公司研发了几个电商小程序,还有一个核心的电商直播,只要是电商一般都会涉及到交易信息,离不开支付系统,这里我们统一实现小程序的支付流程(与服务号实现步骤一样). 目录1.开通小程序的支付能力2.商户 ...
- c语言实现去除字符串首尾空格
字符串内存图如下: 引入头文件: 1 #include<stdlib.h> 2 #include<stdio.h> 3 #include<string.h> 函数原 ...
- 第 15 篇:优化博客功能的细节,提升使用体验—— HelloDjango 系列教程
作者:HelloGitHub-追梦人物 文中涉及的示例代码,已同步更新到 HelloGitHub-Team 仓库 在之前的系列教程中,我们已经实现了:文章的发布.展示.评论等功能,可能认真的小伙伴已经 ...
- Go操作MySQL
MySQL是常用的关系型数据库,本文介绍了Go语言如何操作MySQL数据库. Go操作MySQL 连接 Go语言中的database/sql包提供了保证SQL或类SQL数据库的泛用接口,并不提供具体的 ...
- 误删除系列一:linux的bin目录误删除后恢复操作
感言:一失足成千古恨,一不小心就把/usr/bin下所有的命令都删除了,当你以为自己很熟练时,当你以为自己操作对时,可能就是失手的时候,还好这次只是一个测试环境....God 恢复过程:(以下是在vs ...
- Windows7关闭默认共享
注意:禁用默认共享会导致安装ORACLE时执行先决条件检测不通过,要想安装ORACLE得先打开共享. 1.win+r 运行里输入compmgmt.msc进入计算机管理,在共享文件夹里,去掉所有的盘共享 ...
- 设置IntelliJ IDEA支持lambda表达式
使用IntelliJ IDEA做为开发工具,对基于maven的java工程,如果要编写lambda表达式,先确保安装并使用了jdk1.8或者更高版本,然后再要做一些设置才能正常编译和执行,具体表现在m ...
- 白话系列之IOC,三个类实现简单的Ioc
前言:博客园上已经有很多IOC的博客.而且很多写的很好,达到开源的水平,但是对于很多新人来说,只了解ioc的概念,以及怎么去使用ioc.然后想更进一步去看源码,但是大部分源码都比较困难,当不知道一个框 ...
- 04 (OC)* weak的实现原理
一:Weak 表 1: Runtime 维护了一个 Weak 表,用于存储所有 Weak 指针.Weak 表是一个哈希表,Key 是对象的地址,Value 是一个数组,数组里面放的是 Weak 指针的 ...