持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情
分享一个暑假开发简易分布式爬虫系统的经历,主要是我们在开发过程中遇到的问题和介绍Flink开发经验,希望能够对看到这篇文章的朋友有帮助。
一、项目简要介绍
本项目为基于流式计算框架flink开发的分布式爬虫系统,能够实现多线程并发爬取京东网站上商品信息,将爬取到的网页信息清洗解析并存储到数据库、且能够对数据进行检索的完整的爬虫功能。
GitHub地址:github.com/jiahong1314…
视频演示地址:www.bilibili.com/video/BV1Jd…
系统运行时首先将初始化的种子url添加到redis数据库里,以待进程读取。之后创建flink流式执行环境,并从redis里遵循优先级策略读取一个url,如果网页内容不为空,则继续执行解析页面,如果该页面为列表url,则将读取到的商品url存入redis数据库中;如果是商品url,则对其进行清洗并存入到MySQL数据库中,如下图所示。系统基于流式计算框架,循环执行该流程,直至redis数据库中所有url都被执行完毕。
二、项目开发过程的思考
1.项目目标
首先我们确定了目标为开发一个基于流式计算框架、遵循robots协议的分布式爬虫,并能够对爬取到的数据进行解析并存储。基于这个目标下,经过多方学习之后,我们总结了项目面临的几大问题。
- 如何实现分布式爬取,使多线程之间可以有效地合作完成爬虫任务;
- 如何实现流式计算,实现长期稳定的数据流输入;
- 如何利用随机ip代理实现反反爬虫,保证爬虫系统的稳定性;
- 如何对爬虫进程实行监控,保证每个进程的平稳运行;
- 如何保证系统的可扩展性,使其在面对新的爬取网页和存储设备时易于修改;
2.语言选择
在一开始讨论时,我们主要考虑了java和python,Flink是基于java开发的,对java适用性肯定强,小组成员也对java比较熟悉。如果采用python,Flink 从 1.9.0 版本开始增加了对 Python 的支持(PyFlink),Pythoh 生态与大数据生态有密不可分的关系,python在数据分析、机器学习、深度学习方面有先天优势。但是综合比较,我们还是选择了java,主要有两个考量:一是对java比较熟悉,能够快速上手开发,在短时间内完成项目;二是项目主要目标是进行分布式爬虫,不需要进行数据分析,只需要将数据存储。
3.分布式爬虫的实现
首先需要确定什么是分布式,分布式系统一定是由多个节点组成的系统。其中,节点指的是计算机服务器,而且这些节点一般不是孤立的,而是互通的。一开始,我们将Flink的TaskManager管理不同的JobManager当作是一种分布式实现,将流式计算处理过程中的任务分配理解为爬虫系统中多个爬虫进行爬取数据。这种理解其实是错误的,我们在经过多次讨论后终于发现这个问题,最后选择采用基于redis实现分布式爬虫的方法。技术架构图如下。
4.爬虫系统的流式处理过程实现
在敲这一部分代码时,感觉我是痛并快乐着,需要阅读Flink开发文档,基本的API调用,其中最痛苦的还是需要自定义数据源、flatmap、sink等函数,编写这一部分代码,我查找了大量实现案例,在这些案例基础上还需要考虑基于redis怎么实现,怎么写入数据库,从redis中读入数据,再进行处理写回redis,去除重复url等问题都让我非常头大。所幸最后依次解决了这些问题,完成这一部分代码的那一刻是最快乐的。 下面给出具体的代码实现,供各位参考。
Flink主体程序
我在编写这部分代码时主要依据Flink程序处理流程,一个 Flink 程序,其实就是对 DataStream 的各种转换。具体来说,代码基本上都由以下几 部分构成,如图 所示:
-
获取执行环境(execution environment)
getExecutionEnvironment
-
读取数据源(source)
DataStream stream = env.addSource(...);
从集合、文件、socket、Kafka、自定义的Source
-
定义基于数据的转换操作(transformations)
基本转换算子:map、filter、flatmap
聚合算子:按键分区、简单聚合、归约聚合
-
定义计算结果的输出位置(sink)
-
程序执行(execute)
public class FlinkSpider {
public static void main(String[] args) throws Exception {
ISpider iSpider = ISpider.getInstance();
// 创建流式执行环境
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(4);
// 添加数据源
DataStreamSource<String> stream = env.addSource(new MyRedisSource());
// 处理数据
SingleOutputStreamOperator<UrlList> urlListSingleOutputStreamOperator = stream.flatMap(new SpiderFlatMapFunction(iSpider));
urlListSingleOutputStreamOperator.addSink(new MyRedisSinkFunction());
env.execute();
}
}
自定义数据源,从redis中读取URL
/*
自定义Flink数据源,从redis中读取URL,按照优先级读取,当一个爬虫爬取一个页面结束,
先分发高级URL队列中的URL,当高级URL队列为空时,分发低级队列中的URL
*/
public class MyRedisSource implements SourceFunction<String> {
private boolean isRunning =true;
private Jedis jedis=null;
private final long SLEEP_MILLION=5000;
@Override
public void run(SourceContext<String> sourceContext) throws Exception {
this.jedis = new Jedis("127.0.0.1", 6379);
while(isRunning){
String url = null;
while (url ==null){
String randomDomain = jedis.srandmember(SpiderConstants.SPIDER_WEBSITE_DOMAINS_KEY); // jd.com
String key = randomDomain + SpiderConstants.SPIDER_DOMAIN_HIGHER_SUFFIX; // jd.com.higher
url = jedis.lpop(key);
if(url == null) { // 如果为null,则从低优先级中获取
key = randomDomain + SpiderConstants.SPIDER_DOMAIN_LOWER_SUFFIX; // jd.com.lower
url = jedis.lpop(key);
}
SpiderUtil.sleep(1000);
}
sourceContext.collect(url);
}
}
@Override
public void cancel() {
isRunning=false;
while(jedis!=null){
jedis.close();
}
}
}
- 自定义FlatMap函数,处理URL
public class SpiderFlatMapFunction implements FlatMapFunction<String, UrlList> {
private ISpider iSpider;
public SpiderFlatMapFunction(ISpider iSpider){
this.iSpider = iSpider;
}
@Override
public void flatMap(String s, Collector<UrlList> collector) throws Exception {
UrlList urlList = iSpider.startSingle(s);
collector.collect(urlList);
}
}
- 自定义sink函数,将URL写入Redis
public class MyRedisSinkFunction extends RichSinkFunction<UrlList> {
Jedis jedis = null;
public void open(Configuration parameters) throws Exception {
jedis = JedisUtil.getJedis();
}
public void invoke(UrlList value, Context context) {
List<String> high = value.getHighList();
List<String> low = value.getLowList();
if(!high.isEmpty()){
for(String url:high){
String domain = SpiderUtil.getTopDomain(url); // 获取url对应的顶级域名,如jd.com
String key = domain + SpiderConstants.SPIDER_DOMAIN_HIGHER_SUFFIX; // 拼接url队列的key,如jd.com.higher
jedis.lpush(key, url);
}
}
if(!low.isEmpty()){
for(String url:low){
String domain = SpiderUtil.getTopDomain(url); // 获取url对应的顶级域名,如jd.com
String key = domain + SpiderConstants.SPIDER_DOMAIN_LOWER_SUFFIX; // 拼接url队列的key,如jd.com.higher
jedis.lpush(key, url);
}
}
}
public void close() throws Exception {
super.close();
if(jedis != null){
JedisUtil.returnJedis(jedis);
}
}
}
本次项目让我第一次接触大数据的开发,基于问题驱动进行开发,在开发过程中遇到问题、解决问题、思考问题,对自己编程能力有很大的提升。