这是我参与「第四届青训营 」笔记创作活动的第二天
爬虫系统
说明:ZooKeeper监控属于监控报警系统,URL调度器属于URL调度系统
爬虫系统是一个独立运行的进程,我们把我们的爬虫系统打包成jar包,然后分发到不同的节点上执行,这样并行爬取数据可以提高爬虫的效率。\
随机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代理而请求失败时,是否可以把这一情况记录下来,当超过一定次数时,再将其从代理库中删除,同时生成日志供开发人员或运维人员参考,这是完全可以实现的,不过我就不做这一步功能了。\
网页下载器
网页下载器就是用来下载网页中的数据,主要基于下面的接口开发:\
/**
* 网页数据下载
*/
public interface IDownload {
/**
* 下载给定url的网页数据
* @param url
* @return
*/
public Page download(String url);
}
基于此,在系统中只实现了一个http get的下载器,但是也可以完成我们所需要的功能了:\
/**
* 数据下载实现类
*/
public class HttpGetDownloadImpl implements IDownload {
@Override
public Page download(String url) {
Page page = new Page();
String content = HttpUtil.getHttpContent(url); // 获取网页数据
page.setUrl(url);
page.setContent(content);
return page;
}
}
\
网页解析器
网页解析器就是把下载的网页中我们感兴趣的数据解析出来,并保存到某个对象中,供数据存储器进一步处理以保存到不同的持久化仓库中,其基于下面的接口进行开发:\
/**
* 网页数据解析
*/
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; // 商品图片地址
private String params; // 商品规格参数
private List<String> urls = new ArrayList<>(); // 解析列表页面时用来保存解析的商品URL的容器
}
对应的,在MySQL中,表数据结构如下:\
-- ----------------------------
-- Table structure for phone
-- ----------------------------
DROP TABLE IF EXISTS `phone`;
CREATE TABLE `phone` (
`id` varchar(30) CHARACTER SET armscii8 NOT NULL COMMENT '商品id',
`source` varchar(30) NOT NULL COMMENT '商品来源,如jd suning gome等',
`brand` varchar(30) DEFAULT NULL COMMENT '手机品牌',
`title` varchar(255) DEFAULT NULL COMMENT '商品页面的手机标题',
`price` float(10,2) DEFAULT NULL COMMENT '手机价格',
`comment_count` varchar(30) DEFAULT NULL COMMENT '手机评论',
`url` varchar(500) DEFAULT NULL COMMENT '手机详细信息地址',
`img_url` varchar(500) DEFAULT NULL COMMENT '图片地址',
`params` text COMMENT '手机参数,json格式存储',
PRIMARY KEY (`id`,`source`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
而在HBase中的表结构则为如下:\
## cf1 存储 id source price comment brand url
## cf2 存储 title params imgUrl
create 'phone', 'cf1', 'cf2'
## 在HBase shell中查看创建的表
hbase(main):135:0> desc 'phone'
Table phone is ENABLED
phone
COLUMN FAMILIES DESCRIPTION
{NAME => 'cf1', BLOOMFILTER => 'ROW', VERSIONS => '1', IN_MEMORY => 'false', KEEP_DELETED_CELLS => 'FALSE', DATA_BLOCK
_ENCODING => 'NONE', TTL => 'FOREVER', COMPRESSION => 'NONE', MIN_VERSIONS => '0', BLOCKCACHE => 'true', BLOCKSIZE =>
'65536', REPLICATION_SCOPE => '0'}
{NAME => 'cf2', BLOOMFILTER => 'ROW', VERSIONS => '1', IN_MEMORY => 'false', KEEP_DELETED_CELLS => 'FALSE', DATA_BLOCK
_ENCODING => 'NONE', TTL => 'FOREVER', COMPRESSION => 'NONE', MIN_VERSIONS => '0', BLOCKCACHE => 'true', BLOCKSIZE =>
'65536', REPLICATION_SCOPE => '0'}
2 row(s) in 0.0350 seconds
即在HBase中建立了两个列族,分别为cf1、cf2,其中cf1用来保存id source price comment brand url字段信息,cf2用来保存title params imgUrl字段信息。
不同的数据存储用的是不同的实现类,但是其都是基于下面同一个接口开发的:\
/**
* 商品数据的存储
*/
public interface IStore {
public void store(Page page);
}
然后基于此开发了MySQL的存储实现类、HBase的存储实现类还有控制台的输出实现类,如MySQL的存储实现类,其实就是简单的数据插入语句:\
/**
* 使用dbc数据库连接池将数据写入MySQL表中
*/
public class MySQLStoreImpl implements IStore {
private QueryRunner queryRunner = new QueryRunner(DBCPUtil.getDataSource());
@Override
public void store(Page page) {
String sql = "insert into phone(id, source, brand, title, price, comment_count, url, img_url, params) values(?, ?, ?, ?, ?, ?, ?, ?, ?)";
try {
queryRunner.update(sql, page.getId(),
page.getSource(),
page.getBrand(),
page.getTitle(),
page.getPrice(),
page.getCommentCount(),
page.getUrl(),
page.getImgUrl(),
page.getParams());
} catch (SQLException e) {
e.printStackTrace();
}
}
}
而HBase的存储实现类,则是HBase Java API的常用插入语句代码:\
......
// cf1:price
Put pricePut = new Put(rowKey);
// 必须要做是否为null判断,否则会有空指针异常
pricePut.addColumn(cf1, "price".getBytes(), page.getPrice() != null ? String.valueOf(page.getPrice()).getBytes() : "".getBytes());
puts.add(pricePut);
// cf1:comment
Put commentPut = new Put(rowKey);
commentPut.addColumn(cf1, "comment".getBytes(), page.getCommentCount() != null ? String.valueOf(page.getCommentCount()).getBytes() : "".getBytes());
puts.add(commentPut);
// cf1:brand
Put brandPut = new Put(rowKey);
brandPut.addColumn(cf1, "brand".getBytes(), page.getBrand() != null ? page.getBrand().getBytes() : "".getBytes());
puts.add(brandPut);
......
当然,至于要将数据存储在哪个地方,在初始化爬虫程序时,是可以手动选择的:\
// 3.注入存储器
iSpider.setStore(new HBaseStoreImpl());
目前还没有把代码写成可以同时存储在多个地方,按照目前代码的架构,要实现这一点也比较简单,修改一下相应代码就好了。实际上,是可以先把数据保存到MySQL中,然后通过Sqoop导入到HBase中,详细操作可以参考我写的Sqoop文章。
仍然需要注意的是,如果确定需要将数据保存到HBase中,请保证你有可用的集群环境,并且需要将如下配置文档添加到classpath下:\
core-site.xml
hbase-site.xml
hdfs-site.xml
对大数据感兴趣的同学可以折腾一下这一点,如果之前没有接触过的,直接使用MySQL存储就好了,只需要在初始化爬虫程序时注入MySQL存储器即可:\
// 3.注入存储器
iSpider.setStore(new MySQLStoreImpl());