1.概述
本文介绍了一个简单的分布式,多线程,采用Redis缓存队列作为调度的爬虫系统。实现了图片的爬取和下载功能。该爬虫系统是基于Java实现的,网上Java实现的爬图程序很多,但是很多缺少基本的优化,程序的健壮性并不好,本文的测重点在于爬取图片的稳定性和效率。
该网站是一个写真网站,内容不便详述。程序能够快速的爬取该网站分页的图片数据。
2.系统的简介
- 分布式架构:master节点用于爬取页面Url,存放于缓存中,slave节点用于下载图片。master和slave程序打包后,发布到不同的节点运行,提高整体的效率
- 单节点多线程:单个节点上使用多线程的技术,提高运行效率
- Url缓存仓库:采用redis数据库List做Url仓库,保证数据的不可重复读,实现多节点的任务调度
- 使用面向接口编程的思想,多层封装基本的爬虫请求,使调用者集中在页面解析上,不必关注具体的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请求,没有很好的防爬机制,有兴趣的可以考虑。