先贴一张架构图
整体架构分三个部分:
调度器 :分配任务
爬虫 :爬取数据并保存
监控系统 :查看爬虫状态(主要作用是某个节点down掉了可以今早发现,虽然不影响整体稳定性,但是影响爬虫效率)
爬虫部分
爬虫系统是一个独立运行的进程,我们把我们的爬虫系统打包成 jar 包,然后分发到不同的节点上执行,这样并行爬取数据可以提高爬虫的效率。(爬虫源码分为:ip池->下载页面->解析页面->存储 四部分)
IP池
加入随机 IP 代理主要是为了反反爬虫,因此如果有一个 IP 代理库,并且可以在构建 http 客户端时随机地使用不同的代理,那么对我们进行反反爬虫会有很大的帮助。在系统中使用 IP 代理库,需要先在文本文件中添加可用的代理地址信息:
# IPProxyRepository.txt
58.60.255.104:8118
219.135.164.245:3128
27.44.171.27:9999
219.135.164.245:3128
58.60.255.104:8118
58.252.6.165:9000
......
需要注意的是,上面的代理 IP 是我在西刺代理上拿到的一些代理 IP,不一定可用,建议是自己花钱购买一批代理 IP,这样可以节省很多时间和精力去寻找代理 IP。然后在构建 http 客户端的工具类中,当第一次使用工具类时,会把这些代理 IP 加载进内存中,加载到 Java 的一个 HashMap:
// IP地址代理库Map
private static Map<String, Integer> IPProxyRepository = new HashMap<>();
private static String[] keysArray = null; // keysArray是为了方便生成随机的代理对象
/**
* 初次使用时使用静态代码块将IP代理库加载进set中
*/
static {
InputStream in = HttpUtil.class.getClassLoader().getResourceAsStream("IPProxyRepository.txt"); // 加载包含代理IP的文本
// 构建缓冲流对象
InputStreamReader isr = new InputStreamReader(in);
BufferedReader bfr = new BufferedReader(isr);
String line = null;
try {
// 循环读每一行,添加进map中
while ((line = bfr.readLine()) != null) {
String[] split = line.split(":"); // 以:作为分隔符,即文本中的数据格式应为192.168.1.1:4893
String host = split[0];
int port = Integer.valueOf(split[1]);
IPProxyRepository.put(host, port);
}
Set<String> keys = IPProxyRepository.keySet();
keysArray = keys.toArray(new String[keys.size()]); // keysArray是为了方便生成随机的代理对象
} catch (IOException e) {
e.printStackTrace();
}
}
之后,在每次构建 http 客户端时,都会先到 map 中看是否有代理 IP,有则使用,没有则不使用代理:
CloseableHttpClient httpClient = null;
HttpHost proxy = null;
if (IPProxyRepository.size() > 0) { // 如果ip代理地址库不为空,则设置代理
proxy = getRandomProxy();
httpClient = HttpClients.custom().setProxy(proxy).build(); // 创建httpclient对象
} else {
httpClient = HttpClients.custom().build(); // 创建httpclient对象
}
HttpGet request = new HttpGet(url); // 构建htttp get请求
......
随机代理对象则通过下面的方法生成:
/**
* 随机返回一个代理对象
*
* @return
*/
public static HttpHost getRandomProxy() {
// 随机获取host:port,并构建代理对象
Random random = new Random();
String host = keysArray[random.nextInt(keysArray.length)];
int port = IPProxyRepository.get(host);
HttpHost proxy = new HttpHost(host, port); // 设置http代理
return proxy;
}
这样,通过上面的设计,基本就实现了随机 IP 代理器的功能,当然,其中还有很多可以完善的地方。比如,当使用这个 IP 代理而请求失败时,是否可以把这一情况记录下来;当超过一定次数时,再将其从代理库中删除,同时生成日志供开发人员或运维人员参考,这是完全可以实现的,不过我就不做这一步功能了。
下载
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import java.io.IOException;
/**
* Title: Httpclient
* Description: 采用httpclient获取htmlpage,方便使用代理
* Company: AceGear
* Author: henrywang
* Date: 2018/6/23
* JDK: 8
* Encoding: UTF-8
*/
public class Httpclient {
private static RequestConfig requestConfig = RequestConfig.custom().setProxy(new HttpHost("49.79.156.117", 8000)).setSocketTimeout(15000).setConnectTimeout(15000)
.setConnectionRequestTimeout(15000).build();
static CloseableHttpClient closeableHttpClient = null;
static CloseableHttpResponse closeableHttpResponse = null;
static HttpEntity httpEntity = null;
public Document getDocument(String httpUrl) {
closeableHttpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(httpUrl);
//HttpHost proxy = new HttpHost("39.137.69.10",80);
httpGet.setHeader("User-Agent", "Mozilla/5.0(Windows NT 6.1; rv:6.0.2) Gecko/20100101 Firefox/6.0.2");
httpGet.setConfig(requestConfig);
String responseContent = "";
try {
closeableHttpResponse = closeableHttpClient.execute(httpGet);
httpEntity = closeableHttpResponse.getEntity();
responseContent = EntityUtils.toString(httpEntity, "UTF-8");
} catch (IOException e) {
e.printStackTrace();
}
Document document = Jsoup.parse(responseContent);
return document;
}
}
解析
网页解析器就是把下载的网页中我们感兴趣的数据解析出来,并保存到某个对象中,供数据存储器进一步处理以保存到不同的持久化仓库中,其基于下面的接口进行开发:
/**
* 网页数据解析
*/
public interface IParser {
public void parser(Page page);
}
网页解析器在整个系统的开发中也算是比较重头戏的一个组件,功能不复杂,主要是代码比较多,针对不同的商城不同的商品,对应的解析器可能就不一样了。
因此需要针对特别的商城的商品进行开发,因为很显然,京东用的网页模板跟苏宁易购的肯定不一样,天猫用的跟京东用的也肯定不一样。
所以这个完全是看自己的需要来进行开发了,只是说,在解析器开发的过程当中会发现有部分重复代码,这时就可以把这些代码抽象出来开发一个工具类了。
目前在系统中爬取的是京东和苏宁易购的手机商品数据,因此就写了这两个实现类:
/**
* 解析京东商品的实现类
*/
public class JDHtmlParserImpl implements IParser {
......
}
/**
* 苏宁易购网页解析
*/
public class SNHtmlParserImpl implements IParser {
......
}
存储
数据存储器主要是将网页解析器解析出来的数据对象保存到不同的表格,而对于本次爬取的手机商品,数据对象是下面一个 Page 对象:
/**
* 网页对象,主要包含网页内容和商品数据
*/
public class Page {
private String content; // 网页内容
private String id; // 商品Id
private String source; // 商品来源
private String brand; // 商品品牌
private String title; // 商品标题
private float price; // 商品价格
private int commentCount; // 商品评论数
private String url; // 商品地址
private String imgUrl; // 商品图片地址