nginx,redis,gossip_spider

nginx

什么是nginx?

Nginx是一款高性能的http 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。由俄罗斯的程序设计师Igor Sysoev所开发,官方测试nginx能够支支撑5万并发链接,并且cpu、内存等资源消耗却非常低,运行非常稳定。

为什么要用nginx?

  • 1) http服务器。Nginx是一个http服务可以独立提供http服务。可以做网页静态服务器。
  • 2) 虚拟主机。可以实现在一台服务器虚拟出多个网站。例如个人网站使用的虚拟主机。
  • 3) 反向代理,负载均衡。当网站的访问量达到一定程度后,单台服务器不能满足用户的请求时,需要用多台服务器集群可以使用nginx做反向代理。并且多台服务器可以平均分担负载,不会因为某台服务器负载高宕机而某台服务器闲置的情况。

nginx的高可用

​ 负载均衡高可用:为了避免出现nginx出现宕机,可对nginx进行高可用,为nginx提供备用机,使用高可用监控程序(keepalived)来处理,一般的处理模式是,两台同时运行,实际提供服务的只有一台,监控中心与这台服务器之间会传输诸如’I am alive’这样得信息来监控对方的运行状况,一旦发现对方挂掉,就马上切换到另一台,如果挂掉的主服务器重新发送激活的状态 ,监控中心就会将请求交由主服务器


redis

Redis的概念

​ Redis是一款由C语言编写, 基于内存的持久化的数据库, 其中数据是以KEY-VALUE的形式存储的, Redis提供了丰富的数据类型

Redis的特点

  • 1) Redis将数据存储到内存当中, 所以Redis的读写效率非常高: 读 11w/s 写 8w/s
  • 2) redis提供了丰富的数据类型: string , hash , list ,set , sortedSet
    • 注意: redis中数据类型, 主要是描述的key-value中value的数据类型, 而key只有string
  • 3) Redis数据的移植非常快的
  • 4) redis中所有的操作都是原子性的, 保证数据的完整性的

redis数据类型的使用场景和特点

  • string: 可以使用json转换对象, 存储
    • 特点: 和 java中 string是类似的, 表示的就是字符串
    • 使用场景: 做缓存
  • hash: 存储对象是比较方便的
    • 特点: 和 java中 hashMap是类似的
    • 使用场景: 做缓存 (hash使用较少)
  • list:
    • 特点: 和 java中 linkedList是类似, 可以看做是队列(FIFO)先进先出
    • 使用场景: 任务队列
  • set :
    • 特点: 和 java中set集合是类似的 去重 无序
    • 使用场景: 去重业务
  • sortedSet
    • 特点: 和 java中 sortedSet(TreeSet)类型 有序 去重
    • 使用场景: 排序操作(排行榜)

jedis连接池

jedis看做是一个连接对象, 频繁的创建一个连接, 比较耗时耗资源的 通常情况, 采用连接池的技术, 提前的创建好一部分的连接对象, 放置到容器中, 反复的使用即可

  • jedis的连接池基本使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class JedisUtils {
private static JedisPool jedisPool;
// 什么加载: 随着类的加载而加载, 一般只会加载一次
static {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(100); //最大连接数
config.setMaxIdle(50); // 最大闲时的数量
config.setMinIdle(25); // 最小闲时的数量

jedisPool = new JedisPool(config,"192.168.72.142",6379);
}

// 获取连接的方法

public static Jedis getJedis(){


return jedisPool.getResource() ;
}
}

娱乐爬虫

163娱乐爬虫流程

1563178753672

1.确定首页url

1
2
3
4
5
6
7
8
9
10
11
12
13
public class News163Spider2 {
private static IdWorker idWorker = new IdWorker(0, 3);
private static NewsDao newsDao = new NewsDao();

public static void main(String[] args) throws Exception {
//1.确定首页url
List<String> indexUrlList = new ArrayList<String>();
indexUrlList.add("https://ent.163.com/special/000380VU/newsdata_index.js?callback=data_callback");
indexUrlList.add("https://ent.163.com/special/000380VU/newsdata_music.js?callback=data_callback");
indexUrlList.add("https://ent.163.com/special/000380VU/newsdata_show.js?callback=data_callback");
indexUrlList.add("https://ent.163.com/special/000380VU/newsdata_tv.js?callback=data_callback");
indexUrlList.add("https://ent.163.com/special/000380VU/newsdata_star.js?callback=data_callback");
indexUrlList.add("https://ent.163.com/special/000380VU/newsdata_movie.js?callback=data_callback");

2.发送请求,获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private static void page(String indexUrl) throws Exception {
String page = "02";
while (true) {
//2.发送请求,获取数据
String jsonStr = HttpClientUtils.doGet(indexUrl);
if (jsonStr == null){
break;
}
//将jsonStr转换为标准json格式
jsonStr = jsonStr.substring(jsonStr.indexOf("(") + 1, jsonStr.lastIndexOf(")"));
parseJson(jsonStr);
//获取下一页url

indexUrl = indexUrl.replaceAll("_[0-9]+.js", ".js");
String[] split = indexUrl.split(".js");
indexUrl = split[0] + "_" + page + ".js" + split[1];

int pagenum = Integer.parseInt(page);
pagenum ++;
if (pagenum < 10){
page = "0"+pagenum;
}else{
page = pagenum + "";
}
}
}

3.解析数据,封装javabean

4.保存数据(去重)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private static void parseJson(String jsonStr) throws Exception {
//3.解析数据
Gson gson = new Gson();
List<Map<String, Object>> newsList = gson.fromJson(jsonStr, List.class);
for (Map<String, Object> newsObject : newsList) {
String docUrl = (String) newsObject.get("docurl");
System.out.println(docUrl);
//过滤网页
if (!docUrl.contains("ent.163.com") || docUrl.contains("photoview")) {
continue;
}

//#########去重########
Jedis jedis = JedisUtils.getJedis();
Boolean flag = jedis.sismember("bigData:spider:163News:docUrl02", docUrl);
jedis.close();
if (flag){
continue;
}
//#########去重########

News news = parseNewsItem(docUrl);
//4.保存数据
newsDao.saveNews(news);

//#########去重########
jedis = JedisUtils.getJedis();
jedis.sadd("bigData:spider:163News:docUrl02",news.getDocurl());
jedis.close();
//#########去重########
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//解析新闻详情页  
private static News parseNewsItem(String docUrl) throws Exception {
String html = HttpClientUtils.doGet(docUrl);
Document document = Jsoup.parse(html);
News news = new News();
//获取title
String title = document.select("#epContentLeft>h1").text();
news.setTitle(title);
//获取time
String timeAndSource = document.select(".post_time_source").text();

String[] split = timeAndSource.split(" 来源: ");
String time = split[0];

news.setTime(time);
//获取source
String source = split[1].substring(0, split[1].indexOf("举"));
news.setSource(source);
//获取content
String content = document.select("#endText p").text();
news.setContent(content);
//获取editor
String editor = document.select(".ep-editor").text();
editor = editor.substring(editor.indexOf(":") + 1, editor.indexOf("_"));
news.setEditor(editor);
//获取id
long id = idWorker.nextId();
news.setId(id + "");
//设置docurl
news.setDocurl(docUrl);
// System.out.println(news);

//返回news
return news;
}

5.分页获取爬取

1
2
3
4
5
6
7
//5.分页获取数据
while (!indexUrlList.isEmpty()) {
String indexUrl = indexUrlList.remove(0);

page(indexUrl);
System.out.println("本栏目爬取完毕!");
}

整体爬虫代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
public class News163Spider2 {
private static IdWorker idWorker = new IdWorker(0, 3);
private static NewsDao newsDao = new NewsDao();

public static void main(String[] args) throws Exception {
//1.确定首页url
List<String> indexUrlList = new ArrayList<String>();
indexUrlList.add("https://ent.163.com/special/000380VU/newsdata_index.js?callback=data_callback");
indexUrlList.add("https://ent.163.com/special/000380VU/newsdata_music.js?callback=data_callback");
indexUrlList.add("https://ent.163.com/special/000380VU/newsdata_show.js?callback=data_callback");
indexUrlList.add("https://ent.163.com/special/000380VU/newsdata_tv.js?callback=data_callback");
indexUrlList.add("https://ent.163.com/special/000380VU/newsdata_star.js?callback=data_callback");
indexUrlList.add("https://ent.163.com/special/000380VU/newsdata_movie.js?callback=data_callback");
//5.分页获取数据
while (!indexUrlList.isEmpty()) {
String indexUrl = indexUrlList.remove(0);

page(indexUrl);
System.out.println("本栏目爬取完毕!");
}


}

private static void page(String indexUrl) throws Exception {
String page = "02";
while (true) {
//2.发送请求,获取数据
String jsonStr = HttpClientUtils.doGet(indexUrl);
if (jsonStr == null){
break;
}
//将jsonStr转换为标准json格式
jsonStr = jsonStr.substring(jsonStr.indexOf("(") + 1, jsonStr.lastIndexOf(")"));
parseJson(jsonStr);
//获取下一页url
indexUrl = indexUrl.replaceAll("_[0-9]+.js", ".js");
String[] split = indexUrl.split(".js");
indexUrl = split[0] + "_" + page + ".js" + split[1];
int pagenum = Integer.parseInt(page);
pagenum ++;
if (pagenum < 10){
page = "0"+pagenum;
}else{
page = pagenum + "";
}
}
}

private static void parseJson(String jsonStr) throws Exception {
//3.解析数据
Gson gson = new Gson();
List<Map<String, Object>> newsList = gson.fromJson(jsonStr, List.class);
for (Map<String, Object> newsObject : newsList) {
String docUrl = (String) newsObject.get("docurl");
System.out.println(docUrl);
//过滤网页
if (!docUrl.contains("ent.163.com") || docUrl.contains("photoview")) {
continue;
}

//#########去重########
Jedis jedis = JedisUtils.getJedis();
Boolean flag = jedis.sismember("bigData:spider:163News:docUrl02", docUrl);
jedis.close();
if (flag){
continue;
}
//#########去重########

News news = parseNewsItem(docUrl);
// System.out.println(news);
//4.保存数据
newsDao.saveNews(news);

//#########去重########
jedis = JedisUtils.getJedis();
jedis.sadd("bigData:spider:163News:docUrl02",news.getDocurl());
jedis.close();
//#########去重########
}
}

private static News parseNewsItem(String docUrl) throws Exception {
String html = HttpClientUtils.doGet(docUrl);
Document document = Jsoup.parse(html);
News news = new News();
//获取title
String title = document.select("#epContentLeft>h1").text();
news.setTitle(title);
//获取time
String timeAndSource = document.select(".post_time_source").text();

String[] split = timeAndSource.split(" 来源: ");
String time = split[0];

news.setTime(time);
//获取source
String source = split[1].substring(0, split[1].indexOf("举"));
news.setSource(source);
//获取content
String content = document.select("#endText p").text();
news.setContent(content);
//获取editor
String editor = document.select(".ep-editor").text();
editor = editor.substring(editor.indexOf(":") + 1, editor.indexOf("_"));
news.setEditor(editor);
//获取id
long id = idWorker.nextId();
news.setId(id + "");
//设置docurl
news.setDocurl(docUrl);

//返回news
return news;
}
}

分布式爬虫开发

1. 什么是分布式, 分布式和集群的区别

  • 分布式: 分布式指的就是某一个模块, 或者某个系统, 拆分成不同的业务,并进行分开部署,
  • 集群: 集群更多强调的是将相同的模块或者是系统, 重复部署多次

一般来说, 在大多数的情况下, 集群和分布式是同时存在, 共同作用于整个项目

  • 通俗描述:

    • 小饭店原来只有一个厨师,切菜洗菜备料炒菜全干。

      后来客人多了,厨房一个厨师忙不过来,又请了个厨师,两个厨师都能炒一样的菜,这两个厨师的关系是集群。

      为了让厨师专心炒菜,把菜做到极致,又请了个配菜师负责切菜,备菜,备料,厨师和配菜师的关系是分布式。

      一个配菜师也忙不过来了,又请了个配菜师,两个配菜师关系是集群。

  • 从刚才的案例中, 请分析出, 集群和分布式能解决什么样的问题?

    • 1) 主要解决单节点压力过大
    • 2) 提高代码的复用性 :
    • 3) 降低模块间或者各个子系统的耦合性

缺点: 提高开发难度

2. 进行分布式爬虫编写

2.1 为什么要进行分布式爬虫的改写

1538301333207

  • 主要是由于程序的执行效率过低, 可以进行优化提升

1) 专门用来获取163新闻详情页url程序

2) 专门用来解析163新闻详情页的程序

3) 专门用来保存数据的程序 (公共的程序)

4) 专门用来获取腾讯新闻数据的程序

2.2 分布式爬虫的架构

1538573149803

1
2
3
4
5
6
1) 用来执行去重的公共的key:  set
bigData:spider:docurl
2) 用来保存详情页docurl的key: list
bigData:spider:163itemUrl:docurl
3) 用来保存news对象key : list
bigData:spider:newsJson

2.3 分布式爬虫开发:

2.3.1 163分布式爬虫改进

说明: 163爬虫一共要拆分成三个子工程, 目前将第一个工程命名为News163Master 第二个工程命名为 News163Slave 第三个工程命名为 PublicDaoNode

  • 1) News163Master 开发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130

//需求: 获取详情页的url

/**
* 1) 确定首页url 2) 发送请求, 获取数据 3) 解析数据 4) 去重判断 5) 将docurl保存到redis中 6) 获取下一页
*/
public class News163Master {

public static void main(String[] args) throws Exception {

//1. 确定首页url:
List<String> urlList = new ArrayList<String>();
urlList.add("https://ent.163.com/special/000380VU/newsdata_index.js?callback=data_callback");
urlList.add("https://ent.163.com/special/000380VU/newsdata_star.js?callback=data_callback");
urlList.add("https://ent.163.com/special/000380VU/newsdata_movie.js?callback=data_callback");
urlList.add("https://ent.163.com/special/000380VU/newsdata_tv.js?callback=data_callback");
urlList.add("https://ent.163.com/special/000380VU/newsdata_show.js?callback=data_callback");
urlList.add("https://ent.163.com/special/000380VU/newsdata_music.js?callback=data_callback");

//5. 分页获取数据
while(!urlList.isEmpty()) {
String indexUrl = urlList.remove(0);
System.out.println("获取了下一个栏目的数据#######################################" );
page(indexUrl);
}
}
// 执行分页的方法

public static void page(String indexUrl) throws Exception{
String page = "02";
while(true) {
//1. 发送请求获取数据
// 此处获取的json的数据, 并不是一个非标准的json

String jsonStr = HttpClientUtils.doGet(indexUrl);
if(jsonStr==null){
System.out.println("数据获取完成");
break;
}
// 转换为标准json方法
jsonStr = splitJson(jsonStr);

//2. 解析数据, 3 保存数据
parseJson(jsonStr);

//4. 获取下一页的url
if(indexUrl.contains("newsdata_index")){
indexUrl = "https://ent.163.com/special/000380VU/newsdata_index_" + page + ".js?callback=data_callback";
}
if(indexUrl.contains("newsdata_star")){
indexUrl = "https://ent.163.com/special/000380VU/newsdata_star_" + page + ".js?callback=data_callback";
}
if(indexUrl.contains("newsdata_movie")){
indexUrl = "https://ent.163.com/special/000380VU/newsdata_movie_" + page + ".js?callback=data_callback";
}
if(indexUrl.contains("newsdata_tv")){
indexUrl = "https://ent.163.com/special/000380VU/newsdata_tv_" + page + ".js?callback=data_callback";
}
if(indexUrl.contains("newsdata_show")){
indexUrl = "https://ent.163.com/special/000380VU/newsdata_show_" + page + ".js?callback=data_callback";
}
if(indexUrl.contains("newsdata_music")){
indexUrl = "https://ent.163.com/special/000380VU/newsdata_music_" + page + ".js?callback=data_callback";
}


System.out.println(indexUrl);

//5. page ++
int pageNum = Integer.parseInt(page);
pageNum++;

if(pageNum <10){
page = "0"+pageNum;
}else{
page = pageNum+"";
}
}

}

// 解析json的方法
private static void parseJson(String jsonStr) throws Exception{
//3.1 将json字符串转换成 指定的对象
Gson gson = new Gson();

List<Map<String, Object>> newsList = gson.fromJson(jsonStr, List.class);
// 3.2 遍历整个新闻的结合, 获取每一个新闻的对象
for (Map<String, Object> newsObj : newsList) {
// 新闻 : 标题, 时间,来源 , 内容 , 新闻编辑 , 新闻的url
//3.2.1 获取新闻的url , 需要根据url, 获取详情页中新闻数据
String docUrl = (String) newsObj.get("docurl");
// 过滤掉一些不是新闻数据的url
if(docUrl.contains("photoview")){
continue;
}
if(docUrl.contains("v.163.com")){
continue;
}
if(docUrl.contains("c.m.163.com")){
continue;
}
if(docUrl.contains("dy.163.com")){
continue;
}
// ###################去重处理代码######################
Jedis jedis = JedisUtils.getJedis();
Boolean flag = jedis.sismember("bigData:spider:docurl", docUrl);
jedis.close();//一定一定一定不要忘记关闭, 否则用着用着没了, 导致程序卡死不动
if(flag){
// 代表存在, 表示已经爬取过了
continue;
}
// ###################去重处理代码######################
// 将docurl存储到redis的list集合中
jedis = JedisUtils.getJedis();
jedis.lpush("bigData:spider:163itemUrl:docurl",docUrl);
jedis.close();

}
}
// 将非标准的json转换为标准的json字符串
private static String splitJson(String jsonStr) {
int firstIndex = jsonStr.indexOf("(");
int lastIndex = jsonStr.lastIndexOf(")");

return jsonStr.substring(firstIndex + 1, lastIndex);

}
}
  • 2) News163Slave 开发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

// 需求 : 解析新闻详情页的数据
// 步骤: 1) 从redis中获取docurl 2) 根据url解析商品的详情页 3) 封装成news对象 4) 将news对象转换为json数据
// 5) 将newsJson存储到redis的list中 6) 循环
public class News163Slave {
private static IdWorker idWorker = new IdWorker(0,1);
public static void main(String[] args) throws Exception {
while (true) {
//1.从redis中获取docurl
Jedis jedis = JedisUtils.getJedis();

//String docurl = jedis.rpop("bigData:spider:163itemUrl:docurl"); // 取不到的时候出来
// 第一个参数: 阻塞的时间 如果list中没有数据了, 就会进行阻塞, 最多阻塞20s. 如果在23s之内有数据进来, 马上解除阻塞
// 返回值: list 在这个list中只会有两个元素, 第一个元素为key值 第二个元素为弹出的元素
List<String> list = jedis.brpop(20, "bigData:spider:163itemUrl:docurl");
jedis.close();
if(list == null || list.size()==0 ){
break;
}
String docurl = list.get(1);
//2. 根据url解析商品的详情页 封装成news对象
News news = parseNewsItem(docurl);

//3. 将news对象转换为json数据
Gson gson = new Gson();
String newsJson = gson.toJson(news);

//4. 将newsJson存储到redis中
jedis = JedisUtils.getJedis();
jedis.lpush("bigData:spider:newsJson", newsJson);
jedis.close();
}
}

// 根据url 解析新闻详情页:
private static News parseNewsItem(String docUrl) throws Exception {
System.out.println(docUrl);
// 3.3.1 发送请求, 获取新闻详情页数据
String html = HttpClientUtils.doGet(docUrl);

//3.3.2 解析新闻详情页:
Document document = Jsoup.parse(html);

//3.3.2.1 : 解析新闻的标题:
News news = new News();
Elements h1El = document.select("#epContentLeft h1");
String title = h1El.text();
news.setTitle(title);

//3.3.2.2 : 解析新闻的时间:
Elements timeAndSourceEl = document.select(".post_time_source");

String timeAndSource = timeAndSourceEl.text();

String[] split = timeAndSource.split(" 来源: ");// 请各位一定一定一定要复制, 否则会切割失败
news.setTime(split[0]);
//3.3.2.3 : 解析新闻的来源:
news.setSource(split[1]);
//3.3.2.4 : 解析新闻的正文:
Elements ps = document.select("#endText p");
String content = ps.text();
news.setContent(content);
//3.3.2.5 : 解析新闻的编辑:
Elements spanEl = document.select(".ep-editor");
// 责任编辑:陈少杰_b6952
String editor = spanEl.text();
// 一定要接收返回值, 否则白写了
editor = editor.substring(editor.indexOf(":") + 1, editor.lastIndexOf("_"));
news.setEditor(editor);
//3.3.2.6 : 解析新闻的url:
news.setDocurl(docUrl);
//3.3.2.7: id
long id = idWorker.nextId();
news.setId(id + "");

return news;
}
}
  • 3) PublicDaoNode 开发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

// 需求 : 公共的保存数据库的程序:
// 步骤: 1) 从redis中获取newsJson数据 2) 将newsJson转换成news对象 3) 去重判断 4) 保存数据
// 5) 将docurl存储到redis的去重的set集合中 6) 循环
public class PublicDaoNode {
private static NewsDao newsDao = new NewsDao();

public static void main(String[] args) {

while(true) {
//1) 从redis中获取newsJson数据
Jedis jedis = JedisUtils.getJedis();
List<String> list = jedis.brpop(20, "bigData:spider:newsJson");
jedis.close();
if (list == null || list.size() == 0) {
break;
}
String newsJson = list.get(1);
System.out.println(newsJson);
//2. 将newsJson转换成news对象
Gson gson = new Gson();
News news = gson.fromJson(newsJson, News.class);

//3) 去重判断
jedis = JedisUtils.getJedis();
Boolean flag = jedis.sismember("bigData:spider:docurl", news.getDocurl());
jedis.close();

if (flag) {
continue;
}
//4) 保存数据

newsDao.saveNews(news);

// 5) 将docurl存储到redis的去重的set集合中
jedis = JedisUtils.getJedis();
jedis.sadd("bigData:spider:docurl", news.getDocurl());
jedis.close();
}

}

}
2.2.2.2 腾讯娱乐分布式爬虫改进

说明: 腾讯娱乐爬虫需要拆分成二个子工程, 一个子工程用于获取数据,封装news对象, 一个公共的子工程用于保存数据, 其中公共的已经开发完毕, 只需要拆分另一个子工程名为NewsTencentMaster即可

  • 1) NewsTencentMaster 开发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120

// 需求 : 解析数据, 封装成news对象, 将news对象保存到redis中
// 1) 确定首页url 2) 发送请求, 获取数据 3) 解析数据 4) 去重判断 5) 封装news对象 6) 将news对象转换为newsJson
// 7) 将newsJson保存到Redis中 8) 分页获取
public class NewsTencentMaster {
private static IdWorker idWorker = new IdWorker(0,2);

public static void main(String[] args) throws Exception {
//1. 确定首页url
String topNewsUrl = "https://pacaio.match.qq.com/irs/rcd?cid=137&token=d0f13d594edfc180f5bf6b845456f3ea&ext=ent&num=60";
String noTopNewsUrl = "https://pacaio.match.qq.com/irs/rcd?cid=146&token=49cbb2154853ef1a74ff4e53723372ce&ext=ent&page=0";

//2. 执行分页:
page(topNewsUrl, noTopNewsUrl);

}

// 执行分页的方法
public static void page(String topNewsUrl, String noTopNewsUrl) throws Exception {
//1. 热点新闻数据的获取: 只有一页数据
//1.1 发送请求, 获取数据
String topNewsJsonStr = HttpClientUtils.doGet(topNewsUrl);
//1.2 解析数据
List<News> topNewsList = parseJson(topNewsJsonStr);
//1.3 保存数据
saveNews(topNewsList);

//2. 处理非热点数据
int page = 1;
while (true) {

//2.1 发送请求, 获取数据
String noTopNewsJsonStr = HttpClientUtils.doGet(noTopNewsUrl);
//2.2 解析数据
List<News> noTopNewsList = parseJson(noTopNewsJsonStr);

if (noTopNewsList == null) {
break;
}
//2.3 保存数据
saveNews(noTopNewsList);
//2.4 获取下一页url
noTopNewsUrl = "https://pacaio.match.qq.com/irs/rcd?cid=146&token=49cbb2154853ef1a74ff4e53723372ce&ext=ent&page=" + page;

//2.5 自增 +1
page++;

System.out.println(page);
}


}

// 保存数据的操作 : 腾讯返回数据的时候, 就会有重复的数据
public static void saveNews(List<News> newsList) {
Jedis jedis = JedisUtils.getJedis();
Gson gson = new Gson();
for (News news : newsList) {
// 需要将news对象转换为newsJson
String newsJson = gson.toJson(news);

// 将newsJson存储到redis的list集合中
jedis.lpush("bigData:spider:newsJson",newsJson);
}
jedis.close();

}

// 解析新闻数据
private static List<News> parseJson(String newsJsonStr) {
//3.1 将字符串json数据转换为指定的类型: map
Gson gson = new Gson();
Map<String, Object> map = gson.fromJson(newsJsonStr, Map.class);
//获取一下, 本次获取了多少条数据
Double datanum = (Double) map.get("datanum");
if (datanum.intValue() == 0) {
return null;
}
//3.2 获取data中数据 : 列表页中数据
List<Map<String, Object>> newsList = (List<Map<String, Object>>) map.get("data");
//3.3 遍历这个列表, 获取每一个新闻的数据
List<News> tencentNewList = new ArrayList<News>();
for (Map<String, Object> newsMap : newsList) {
String docurl = (String) newsMap.get("vurl");
if (docurl.contains("video")) {
continue;
}
//######################去重处理############################33
Jedis jedis = JedisUtils.getJedis();
Boolean flag = jedis.sismember("bigData:spider:docurl", docurl);
jedis.close();
if (flag) {
// 如果为true, 表示已经存在, 已经爬取过了
continue;
}

//######################去重处理############################33

//3.3.1 封装news对象
News news = new News();

news.setTitle((String) newsMap.get("title"));
news.setTime((String) newsMap.get("update_time"));
news.setSource((String) newsMap.get("source"));
news.setContent((String) newsMap.get("intro"));
news.setEditor((String) newsMap.get("source"));
news.setDocurl(docurl);

news.setId(idWorker.nextId() + "");

tencentNewList.add(news);

System.out.println(docurl);
}

return tencentNewList;


}
}

注意: 开发完成以后., 一定要进行测试:如果能够在本地全部跑通, 才可以进行部署, 否则不要进行部署

  • 测试:
    • 1) 清空数据: redis 和 mysql
    • 2) 修改一个dao连接内容(修改本地连接)
    • 3) 分别启动四个程序即可: 可以不分先后顺序

3. 进行分布式爬虫的部署

3.1 部署方案

说明: 目前一共有四个子项目, 其中二个master各占用一台虚拟机, 其中一个用来解析新闻详情页, 可以部署一个两台的集群, 其中一个用来保存数据的, 可以部署三台,构建一个集群, 一共为七台, 外加一台mysql和一台redis, 共需要九台服务器

1538574257790

  • 本次仅仅是模拟部署, 故采用三台服务器来模拟九台服务器:(实际中真实的九台服务器)
    • 1) 三台服务器都可以上网, 并都安装有jdk1.8以上
    • 2) 三台服务器的防火墙均已关闭(实际中开放端口号)
    • 3) 其中有一台需要安装 MySQL 其中一台安装 Redis,并均已正常开启,mysql必须开启远程登录

3.2 部署准备工作

  • 1) 检测linux环境:

    • 1.1) 使用 ping 命令检测三台是否可以联网,并都安装有jdk1.8
      • 注意: Windows电脑上的jdk 和 linux上的jdk需要保持一致
    • 1.2) 使用 service iptables status 查看是否关闭防火墙
    • 1.3) 检测MySQL:
      • 1.3.1) 在linux中能否正常连接mysql
      • 1.3.2) 在sqlyog中远程连接mysql是否正常
      • 1.3.3) linux和sqlyog连接MySQL的密码需要保持一致, 以免出现存储不进去的问题
    • 1.4) 使用 ps -ef | grep redis 检测redis是否正常启动
  • 将项目进行打包:一共有四个子项目, 共需要打包四个jar

    • 0) 修改NewsDao中连接数据库相关设置

    1544080592257

  • 1) 添加打包插件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<plugins>
<!--这是jdk编译的插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>utf-8</encoding>
</configuration>
</plugin>
<!--打包的插件-->
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest> <!-- 注意 此为设置程序的主入口-->
<mainClass>com.cyannote.jdSpider.JdSlave</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
  • 2) 修改打包插件中的主入口:

    1538574857793

  • 3) 将项目进行编译

    1538574959362

  • 4) 进行打包操作:

    1538575008618

  • 5) 打包成功后, 会在打包过程中提示打包的位置

    1538575060321

  • 6) 打开此位置, 查看此jar包中主入口是否是刚才是设置的主入口类

    1538575219745

  • 7) 将其进行更该成对应的jar包的名称后, 放置一个适当的位置等待上传

  • 8) 重复执行以上2~ 7 步, 将四个jar包都要打包出来

    • 特别注意: 不要忘记修改主入口类(程序的入口)和 打包前进行编译

注意: 需要单独对163slave程序再次进行打包操作. 在导包的时候, 需要修改idwork中编号,否则在部署的时候, 会出现id冲突的问题

1
因为, IDwork如果编号都一致了, 那么在同一时刻产生的id值是一样的

3.3 进行分布式部署

  • 注意, 在开启这三个服务器连接窗口的时候, 一定要三个一起开

1544081473781

使用三台虚拟机模拟9台服务器, 其本质上就是使用xshell 或者 CRT 将三台虚拟机的连接窗口开启各开启三次即可, 然后自己进行分配, 那一台是MySQL, 那一台是Redis, 那一台是master….

1
2
3
4
5
统一jar包上传至 ; /export/servers/spider 目录下

mkdir -p /export/servers/spider
rm -rf /export/servers/spider/*
cd /export/servers/spider
  • 1) 将对应的jar包上传到 服务器中, 推荐使用 rz进行上传
    • 将jar包上传至每个服务器的: /export/servers/spider
1
2
3
4
1) 安装 rz 命令:
安装命令: yum -y install lrzsz
2) 上传命令: rz
注意: 上传的位置和输入rz命令的位置是一样的
  • 2) 启动各个jar包即可: 推荐先启动获取数据的集群, 后启动 解析数据的集群, 最后启动两个master
1
启动命令:  java -jar  xxx.jar

将爬虫设置为定时执行获取数据的操作: 目前采用的shell脚本的方式来操作

需求: 每二十分钟执行一次爬虫程序, 用于爬取最新的新闻信息

实现步骤:

  • 1) 更改hosts文件(此步骤需要在每一个centos中配置)
1
2
3
4
5
6
7
8
9
vi /etc/hosts

修改如下内容:
192.168.72.141 node01
192.168.72.142 node02
192.168.72.143 node03


三台服务器必须要配置免密登录
  • 2) 编写shell脚本:

    注意: 在进行修改shell脚本的时候, 一定要明确, 那台机子上部署了什么jar包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
vi startSpider.sh
i
#脚本内容: 以下脚本为示例内容, 只提供大体逻辑, 根据实际进行局部修改

#!/bin/sh
echo "开始执行"

for host in node01 node02 node03
do
ssh -q $host "source /etc/profile; nohup java -jar /export/servers/spider/PublicDaoNode.jar >>/dev/daoLog.log 2>&1 &"
if [ $host == node01 ]
then
ssh -q $host "source /etc/profile; nohup java -jar /export/servers/spider/News163Slave.jar >>/dev/163Slave.log 2>&1 &"
fi
if [ $host == node02 ]
then
ssh -q $host "source /etc/profile; nohup java -jar /export/servers/spider/News163Slave.jar >>/dev/163Slave.log 2>&1 &"
fi
if [ $host == node03 ]
then
ssh -q $host "source /etc/profile; nohup java -jar /export/servers/spider/News163Master.jar >>/dev/163Master.log 2>&1 &"
ssh -q $host "source /etc/profile; nohup java -jar /export/servers/spider/NewsTencentMaster.jar >>/dev/tencent.log 2>&1 &"
fi
done


echo "结束了"
  • 3) 编写定时任务: 推荐定时任务和当前shell脚本在同一台虚拟机中
1
2
3
4
crontab -e          // 设置定时, 输入完成后会自动进入一个设置文档中
输入 i 进入编辑模式
*/10 * * * * sh /export/servers/spider/startSpider.sh // 表示每隔10分钟执行一次 启动爬虫的脚本
输入 esc 退出命令行模式, 输入 :wq 保存退出即可

必须修改脚本文件的权限,否则会出现无法启动脚本的情况.

4. 使用代理ip

http://hc.apache.org/httpcomponents-client-4.5.x/tutorial/html/connmgmt.html#d5e485

1
2
3
4
5
HttpHost proxy = new HttpHost("someproxy", 8080);
DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy);
CloseableHttpClient httpclient = HttpClients.custom()
.setRoutePlanner(routePlanner)
.build();

5.3.1 集成在代码中

  • 将代理的ip存放到redis中,存储类型list。
1
2
3
4
5
6
7
8
9
10
11
public void testInitIP() throws Exception {
Jedis conn = JedisUtil.getConn();
BufferedReader bufferedReader = new BufferedReader(
new FileReader(new File("C:\\Users\\maoxiangyi\\Desktop\\Proxies2018-06-06.txt")));
String line = null;
while ((line=bufferedReader.readLine())!=null) {
conn.lpush("spider:ip", line);
}
bufferedReader.close();
conn.close();
}
  • 重构后的httpclient
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private static String execute(HttpRequestBase request) {

RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(5000)// 设置创建连接的最长时间
.setConnectionRequestTimeout(5000)// 设置获取连接的最长时间
.setSocketTimeout(10 * 1000)// 设置数据传输的最长时间
.build();
request.setConfig(requestConfig);

String html = null;

// 从redis中获取代理IP
Jedis conn = JedisUtil.getConn();
// 从右边弹出一个元素之后,从新放回左边
List<String> ipkv = conn.brpop(0, "spider:ip");
// CloseableHttpClient httpClient = getHttpClient();
CloseableHttpClient httpClient = getProxyHttpClient(ipkv.get(1));
try {
CloseableHttpResponse res = httpClient.execute(request);
if (200 == res.getStatusLine().getStatusCode()) {
html = EntityUtils.toString(res.getEntity(), Charset.forName("utf-8"));
//请求成功之后,将代理IP放回去,下次继续使用
conn.lpush("spider:ip", ipkv.get(1));
conn.close();
}
} catch (Exception e) {
System.out.println("请求失败");
// TODO 需要开发自动重试功能
throw new RuntimeException(e);
}
return html;
}
private static PoolingHttpClientConnectionManager cm;
private static CloseableHttpClient getProxyHttpClient(String ipkv) {

String[] vals = ipkv.split(":");
System.out.println(vals);
HttpHost proxy = new HttpHost(vals[0], Integer.parseInt(vals[1]));
DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy);
return HttpClients.custom().setConnectionManager(connectionManager).setRoutePlanner(routePlanner).build();
}