一个分布式,多线程的采用Redis缓存调度的Java爬图小系统

1.概述

本文介绍了一个简单的分布式,多线程,采用Redis缓存队列作为调度的爬虫系统。实现了图片的爬取和下载功能。该爬虫系统是基于Java实现的,网上Java实现的爬图程序很多,但是很多缺少基本的优化,程序的健壮性并不好,本文的测重点在于爬取图片的稳定性和效率。

实战爬取的网站地址为 http://www.mmxyz.net/   该网站是一个写真网站,内容不便详述。程序能够快速的爬取该网站分页的图片数据。

2.系统的简介 

  1. 分布式架构:master节点用于爬取页面Url,存放于缓存中,slave节点用于下载图片。master和slave程序打包后,发布到不同的节点运行,提高整体的效率
  2. 单节点多线程:单个节点上使用多线程的技术,提高运行效率
  3. Url缓存仓库:采用redis数据库List做Url仓库,保证数据的不可重复读,实现多节点的任务调度
  4. 使用面向接口编程的思想,多层封装基本的爬虫请求,使调用者集中在页面解析上,不必关注具体的http请求部分。

3.准备工作

本文使用的linux的版本centos6.7 64位,JDK版本jdk1.8,共有1个maste节点和3个slave节点,其中master节点使用的redis版本为3.2.8。程序的相关依赖如下

<dependencies>
		<dependency>
			<groupId>org.apache.httpcomponents</groupId>
			<artifactId>httpclient</artifactId>
			<version>4.5.3</version>
		</dependency>
		<dependency>
			<groupId>org.apache.httpcomponents</groupId>
			<artifactId>fluent-hc</artifactId>
			<version>4.5.3</version>
		</dependency>
		<dependency>
			<groupId>org.jsoup</groupId>
			<artifactId>jsoup</artifactId>
			<version>1.10.3</version>
		</dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.31</version>
		</dependency>
		<dependency>
			<groupId>com.google.code.gson</groupId>
			<artifactId>gson</artifactId>
			<version>2.8.1</version>
		</dependency>
		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
			<version>2.8.0</version>
		</dependency>
	</dependencies>

4.基础代码实现

4.1 ISpider接口

ISpider接口定义了爬虫最基本的两个方法,“发送页面请求”和“下载图片”


/**
 * 定义了爬虫最基本的两个方法
 * @author liyong
 *
 */
public interface ISpider {
	/**
	 * 请求页面
	 * @param nextUrl: 请求页面的url
	 * @param encode: 解析的编码格式
	 * @return response页面html的字符串
	 */
	public String getHtml(String nextUrl, String encode);
	/**
	 * 图片下载
	 * @param filePath 图片的存贮路径
	 * @param pic_src 图片的网络url
	 * @return
	 */
	public String loadPic(String filePath, String pic_src);
}

4.2 基础实现类BaseSpider

该类实现 ISpider接口,出现异常时,只捕获,未做其他处理

/**
 * ISpider的实现类,没有做重发和重传的机制
 */
//@SuppressWarnings("all")
public class BaseSpider implements ISpider {

	//默认stream请求超时 时间
	private int default_connect_timeout = 10000;
	//默认stream读取超时 时间
	private int default_stream_read_timeout = 10000;
	//默认stream 缓存
	private int default_buffer = 10240;
	//默认睡眠 时间 
	private int default_sleepTime = 1000;
	
	// 向页面发起请求,返回html
	@Override
	public String getHtml(String nextUrl, String encode) {
		CloseableHttpClient httpClient;
		HttpGet httpGet;
		CloseableHttpResponse response;
		String html = null;
		try {
			// 1.httpclient
			httpClient = HttpClients.createDefault();
			// 2.get请求头拼接
			httpGet = new HttpGet(nextUrl);
			httpGet.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) "
					+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36");
			httpGet.setHeader("Accept", "*/*");
			// 3.接收响应
			response = httpClient.execute(httpGet);
			if (200 == response.getStatusLine().getStatusCode()) {
				// 4.获得首页的信息,String类型
				html = EntityUtils.toString(response.getEntity(), Charset.forName(encode));
			}
			// 离线爬取,需要设置UTF-8
			// String html = EntityUtils.toString(response.getEntity(), "UTF-8");
			return html;
		} catch (Exception e) {
			// 暂时注释掉
			// System.out.println("getHtml 异常爬取地址:" + nextUrl+",异常状态码:"+code);
			// e.printStackTrace();
			try {
				Thread.sleep(default_sleepTime);
			} catch (Exception e1) {
				e1.printStackTrace();
			}
			return null;
		}
	}

	// 下载图片存于本地
	@Override
	public String loadPic(String filePath, String pic_src) {
		String pic_path=null;
		OutputStream outputStream = null;
		InputStream inputStream = null;
		BufferedInputStream bis = null;
		try {
			// 1.封装图片url
			URL imgUrl = new URL(pic_src);
			String pic_name = pic_src.substring(pic_src.lastIndexOf("/") + 1, pic_src.length());
			// 2.创建URLConnection
			HttpURLConnection conn = (HttpURLConnection) imgUrl.openConnection();
			// 3.设置请求头
			conn.setRequestMethod("GET");
			conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) "
					+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36");
			conn.setRequestProperty("Accept",
					"image/jpg, image/gif, image/jpeg, image/pjpeg, image/pjpeg, "
							+ "application/x-shockwave-flash, application/xaml+xml, "
							+ "application/vnd.ms-xpsdocument, application/x-ms-xbap, "
							+ "application/x-ms-application, application/vnd.ms-excel, "
							+ "application/vnd.ms-powerpoint, application/msword, */*");
			conn.setRequestProperty("Accept-Language", "zh-CN");
			conn.setRequestProperty("Charset", "UTF-8");
			conn.setConnectTimeout(default_connect_timeout);
			conn.setReadTimeout(default_stream_read_timeout);

			// 4.获取输入流
			inputStream = conn.getInputStream();
			// 5.将输入流信息放入缓冲流提升读写速度  
			bis = new BufferedInputStream(inputStream);
			// 6.读取字节娄  
			byte[] buf = new byte[default_buffer];
			// 7.生成文件  
			String filedir = filePath + "/" + pic_name;
			outputStream = new FileOutputStream(filedir);
			int size = 0;
			// 8.边读边写  
			while ((size = bis.read(buf)) != -1) {
				outputStream.write(buf, 0, size);
			}
			// 9.刷新文件流  
			outputStream.flush();
			pic_path=filedir;
			return pic_path;
		} catch (Exception ex) {
			System.out.println(pic_src + " 下载发生异常!!!!");
			ex.printStackTrace();
			return pic_path;

		} finally {
			releaseStream(outputStream, bis, inputStream);
		}
	}
	/**
	 * 关闭流
	 */
	public void releaseStream(OutputStream os, BufferedInputStream bis, InputStream is) {
		try {
			if (os != null) {
				os.close();
			}
			if (bis != null) {
				bis.close();
			}
			if (is != null) {
				is.close();
			}
		} catch (Exception ex) {
		}
	}	
}

基础类中重点关注图片下载部分,使用了java.net.HttpURLConnection连接对。因为网络的不稳定性,连接会经常中断。这里我截取几个常见的网络连接异常

//该异常是由于服务器端因为某种原因关闭了Connection,而客户端依然在读写数据,此时服务器会返回复位标志“RST”
java.net.SocketException: Connection reset
//该异常可能是请求的图片url不存在,或者没有补充请求头服务器拒绝请求下载
java.io.FileNotFoundException
//该异常发生于客户端发送请求,服务器端规定时间未响应
java.net.SocketTimeoutException: connect timed out
//该异常发生于客户端在获取流过程中,由于网络因素中断,等待流超时
java.net.SocketTimeoutException: Read timed out

这里重点关注“java.net.SocketTimeoutException: Read timed out”异常,需要在HttpURLConnection对象中设置超时时间才能捕获。setConnectTimeout()设置url请求超时时间,setReadTimeout()设置stream流获取超时时间。如果没有设置超时时间,HttpURLConnection就会一直处于等待接收状态,造成线程阻塞,程序卡死。

//默认请求超时 时间
private int default_connect_timeout = 10000;
//默认stream读取超时 时间
private int default_stream_read_timeout = 10000;

conn.setConnectTimeout(default_connect_timeout);
conn.setReadTimeout(default_stream_read_timeout);

4.3 SpiderBySetteing类

该是对基础类BaseSpider的封装,主要是封装了异常捕获后的处理,这里简单的采用了重发的机制,重复多次请求。

/**
 * BaseSpider的子类,增加了重发和图片重传的机制,
 * 默认页面请求重发次数,default_request_recycling_count
 * 默认的图片重下次数,default_load_recycling_count
 * 
 */
@SuppressWarnings("all")
public class SpiderBySetteing extends BaseSpider implements ISpider{
	// 默认的请求url次数
	private int default_request_recycling_count = 60;
	// 默认的图片重下次数
	private int default_load_recycling_count = 5
	// 向页面发起请求,返回html
	@Override
	public String getHtml(String nextUrl, String encode) {
		String html=null;
		int count=0;
		//请求重发
		while (html == null && count < default_request_recycling_count) {
			count++;
			html = super.getHtml(nextUrl, encode);
		}
		return html;
	}
	//解析存有url图片地址的页面
	@Override
	public String loadPic(String filePath, String pic_src) {
		int load_count = 0;
		String pic_path=null;
		//图片请求重发
		while(pic_path==null&&load_count<default_load_recycling_count) {
			load_count++;
			pic_path=super.loadPic(filePath, pic_src);
		}
		return pic_path;
	}
}

5.Master和Slave代码

本文源码,有单机版和分布式两个版本,这里贴出分布式中,Master和Slave的部分。

Master节点,系统的Master节点运行的程序,主要起者分配调度任务的作用。解析分页信息,将待爬取的url信息存储到redis中,供其他slave节点消费使用

/**
 * master:解析分页页面的信息,将带爬取的页面的url存入Redis的list集合
 * @author liyong
 *
 */
public class Spider_Master {

	private static Jedis jedis = null;
	// 分页的URL
	private	static String Url = "http://www.mmxyz.net/?action=ajax_post&pag=";

	private static ISpider baseSU = new SpiderBySetteing();
	
	static {
		jedis = new Jedis("192.168.77.130", 6379);
		jedis.auth("admin");
	}

	public static void main(String[] args) {
		// 1.解析分页页面的信息,将带爬取的页面的url存入阻塞队列
		ParseListPage();
	}

	/**
	 * 拼接分页页面
	 */
	private static void ParseListPage() {
		// 分页URL拼接
		for (int i = 1; i <= 81; i++) {
			// 解析每一个分页页面的信息
			searchList(Url + i);
			System.out.println(Url + i + "  parse over-----------");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	/**
	 * 解析每一个分页页面的信息,将带爬取的页面的url存入Redis的list集合
	 * @param nextUrl:分页Url
	 */
	private static void searchList(String nextUrl) {
		String html = null;
		try {
			html = baseSU.getHtml(nextUrl, "utf-8");
			if (html != null) {
				// 解析获取的文件
				Document document = Jsoup.parse(html);
				// 解析doucument
				Elements elements = document.select("div[class=post-thumbnail] a[class=inimg]");
				// 循环读取  
				for (Element e : elements) {// 读取网站所有图片  
					// 创建连接  
					String url1 = e.attr("href");
					// 页面文字转码
					String filename = new String(e.attr("title").getBytes("UTF-8"), "UTF-8");
					// 将(url1,filename)存入到拥塞队列中
					List<String> url_element = new ArrayList<>();
					url_element.add(url1);
					url_element.add(filename);
					Gson gjson = new Gson();
					String json_list = gjson.toJson(url_element);
					// 将将(url1,filename)以json格式存入Redis
					jedis.lpush("liyong:spider:pic", json_list);
				}
			} else {
				// 将nextUrl加入队列???
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

slave节点,具体的爬虫实现 ,获取redis的任务URL,解析url,多线程下载

public class Spider_Slave {	
	// 存储路径
	public static String basefilePath = "H://ROSI";
    // 10个容量的线程池
	private final static ExecutorService threadPool = Executors.newFixedThreadPool(10);

	// 加入重发机制
	private static ISpider baseSU = new SpiderBySetteing();
	// 未加入重发机制
	// private static ISpider baseSU = new BaseSpider();

	public static void main(String[] args) {	
		// 1.创建存储路径
		File f=new File(basefilePath);
		//部署到linux中需要設置setWritable
		f.setWritable(true,false);
		if(!f.exists()) {
			f.mkdirs();
		}
		// 2.采用线程池,创建多线程处理图片页面
		for (int i = 0; i < 10; i++) {
			threadPool.execute(new Runnable() {
				@Override
				public void run() {
					// TODO Auto-generated method stub
					Jedis jedis = new Jedis("192.168.77.130", 6379);
					jedis.auth("admin");
					while (true) {
						try {
							// take(线程安全)
							// List<String> url_element = arrayBlockQueue.take();
							// take(线程安全)
							String json_list = jedis.rpop("liyong:spider:pic");
							if (json_list != null) {
								Gson gson = new Gson();
								List<String> url_element = gson.fromJson(json_list, List.class);
								// 解析具体的产品信息
								getPicToLocal(url_element.get(0), url_element.get(1));
							}
						} catch (Exception e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}
				}
			});
		}
	}
	/**
	 * 解析存有图片Url信息的页面
	 * @param nextUrl:存有图片Url信息的页面地址
	 * @param filename:图片文件夹名称
	 */
	private static void getPicToLocal(String nextUrl, String filename) {
		String html = null;
		try {
			html = baseSU.getHtml(nextUrl, "utf-8");
			if (html != null) {
				// 解析获取的文件
				Document document = Jsoup.parse(html);
				// 解析doucument
				Elements elements = document.select("#gallery-1 a");
				// 循环读取  
				for (Element e : elements) {// 读取网站所有图片  
					// 创建连接  
					String pic_src = e.attr("href");
					new File(basefilePath + "/" + filename).mkdirs();
					baseSU.loadPic(basefilePath + "/" + filename, pic_src);
				}
				System.out.println(filename + " 下载完成!!!");
			}
		} catch (Exception e) {
			System.out.println(nextUrl);
			e.printStackTrace();
		}
	}
}

6. 总结

性能:该图片网站,分页大概有8G左右的图片资源,相同的网络条件下,在单点单线程的程序中,大概花费了6个小时爬取完毕;而在分布式的系统中,只用了一个小时全部爬取,性能提升明显。

缺点:本文的分布式小系统,最后图片下载,都存于各个分机,没有做到很好的图片管理,后续可以使用fastdsf来进行相应的文件管理;由于考虑下载速度,本文没有使用代理Ip的方式进行url请求,没有很好的防爬机制,有兴趣的可以考虑。

本系统是基于Java语言实现,核心在于整个系统的设计,后续会有改动,希望总结一下自己阶段性的学习经验,后续会针对补充,如果对源代码感兴趣,可以查看源码路径:https://github.com/xiaosijianashi/SpiderPic.git

 

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值