网络爬虫介绍
网络爬虫(Web crawler),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本
什么是网络爬虫
在大数据时代,信息的采集是一项重要的工作,而互联网中的数据是海量的,如果单纯靠人力进行信息采集,不仅低效繁琐,搜集的成本也会提高。如何自动高效地获取互联网中我们感兴趣的信息并为我们所用是一个重要的问题,而爬虫技术就是为了解决这些问题而生的。
网络爬虫(Web crawler)也叫做网络机器人,可以代替人们自动地在互联网中进行数据信息的采集与整理。它是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本,可以自动采集所有其能够访问到的页面内容,以获取或更新这些网站的内容和检索方式。
从功能上来讲,爬虫一般分为数据采集,处理,储存三个部分。爬虫从一个或若干初始网页的URL开始,获得初始网页上的URL,在抓取网页的过程中,不断从当前页面上抽取新的URL放入队列,直到满足系统的一定停止条件。
我们感兴趣的信息分为不同的类型:如果只是做搜索引擎,那么感兴趣的信息就是互联网中尽可能多的高质量网页;如果要获取某一垂直领域的数据或者有明确的检索需求,那么感兴趣的信息就是根据我们的检索和需求所定位的这些信息,此时,需要过滤掉一些无用信息。前者我们称为通用网络爬虫,后者我们称为聚焦网络爬虫。
入门程序
网络爬虫说就是用程序帮助我们访问网络上的资源,我们一直以来都是使用HTTP协议访问互联网的网页,我们也需要编写程序,使用同样的协议访问网页。
这里我们使用Java的HTTP协议库 HttpComponents这个技术,来实现抓取网页数据。我们要抓取的是开源中国的HttpComponents页面的文章。
1 创建工程
创建工程itcast-crawler
加入以下依赖:
加入log4j.properties
log4j.rootLogger=DEBUG,A1
log4j.logger.cn.itcast = DEBUG
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%c]-[%p] %m%n
2. 抓取页面
案例:访问开源中国的HttpComponents项目的介绍页面
https://www.oschina.net/p/httpclient
编写以下代码
// 访问地址
private static String url = "https://www.oschina.net/p/httpclient";
public static void main(String[] args) throws Exception {
// 创建Httpclient对象
CloseableHttpClient httpclient = HttpClients.createDefault();
// 创建http GET请求
HttpGet httpGet = new HttpGet(url);
// 解决开源中国不允许爬虫访问的问题
httpGet.setHeader("User-Agent", "");
// 执行请求
CloseableHttpResponse response = httpclient.execute(httpGet);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
String content = EntityUtils.toString(response.getEntity(), "UTF-8");
// 输出抓取到的页面
Writer out = new FileWriter(new File("D:/httpclient.html"));
out.write(content);
out.close();
}
}
3. 解析页面
我们抓取到页面之后,需要对页面进行解析。我们使用浏览器分析发现在一个div标签里面有这个项目文章的正文。这个div标签的class属性为detail editor-viewer all。需要解析这个页面,获取正文内容
我们可以使用以下代码解析页面
// 解析页面,获取需要的内容
String str = StringUtils.substringBetween(content, "
"");
System.out.println(str);
这里就已经把内容获取到了,这就是一个最简单的爬虫的实现,无非就是抓取数据,分析数据,存储数据。
我们已经获取了需要的内容,但是还有很多html的标签没有解析,这会影响我们对数据的处理,虽然这些标签也可以手动编写逻辑过滤掉,但是会耗费我们大量时间,一般情况我们会使用专门的html解析工具进行解析。
4. Jsoup
jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据。
jsoup的主要功能如下:
先加入Jsoup依赖:
4.1. jsoup输入
4.1.1. 输入字符串
public class HttpClientTest {
// 访问地址
private static String url = "https://www.oschina.net/p/httpclient";
public static void main(String[] args) throws Exception {
// 创建Httpclient对象
CloseableHttpClient httpclient = HttpClients.createDefault();
// 创建http GET请求
HttpGet httpGet = new HttpGet(url);
// 解决开源中国不允许爬虫访问的问题
httpGet.setHeader("User-Agent", "");
// 执行请求
CloseableHttpResponse response = httpclient.execute(httpGet);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
String content = EntityUtils.toString(response.getEntity(), "UTF-8");
// 输出抓取到的页面
Writer out = new FileWriter(new File("D:/httpclient.html"));
out.write(content);
out.close();
// 解析页面,获取需要的内容
String str = StringUtils.substringBetween(content, "
"");
System.out.println(str);
// 使用jsoup解析
System.out.println("------以下jsoup----------");
// 把内容解析为Document对象
Document doc = Jsoup.parse(content);
// 使用dom的方式解析内容
Elements elements = doc.getElementsByClass("detail editor-viewer all");
for (Element element : elements) {
System.out.println(element.text());
}
}
}
}
4.1.2. 输入url
我们也可以使用jsoup直接解析一个url地址。
public class JsoupTest {
// 访问地址
private static String url = "https://www.oschina.net/p/httpclient";
public static void main(String[] args) throws Exception {
// 创建连接
Connection conn = Jsoup.connect(url);
// 设置头信息,解决反爬虫限制
conn.header("User-Agent", "");
// 发起get请求,并发结果解析为Document对象
Document doc = conn.get();
// 获取需要的数据
Elements elements = doc.getElementsByClass("detail editor-viewer all");
for (Element element : elements) {
System.out.println(element.text());
}
}
}
PS:虽然使用Jsoup可以替代HttpClient的作用直接发起请求解析数据,但是企业开发中往往不会这样用,因为实际的爬虫开发过程中,需要使用到多线程,连接池,代理等等技术,而jsoup对这些技术的支持并不是很好,所以jsoup一般仅仅作为Html解析工具使用
4.1.3. 输入文件
也可以直接解析文件
// 解析文件为Document对象
// 第一个参数为文件对象,第二个参数为编码
Document doc = Jsoup.parse(new File("D:/httpclient.html"), "UTF-8");
// 获取需要的数据
Elements elements = doc.getElementsByClass("detail editor-viewer all");
for (Element element : elements) {
System.out.println(element.text());
}
4.2. Jsoup解析
4.2.1. 使用dom方式遍历文档
查找元素
getElementById(String id)
getElementsByTag(String tag)
getElementsByClass(String className)
getElementsByAttribute(String key) (and related methods)
Element siblings: siblingElements(), firstElementSibling(), lastElementSibling(); nextElementSibling(), previousElementSibling()
Graph: parent(), children(), child(int index)
元素数据
attr(String key)获取属性attr(String key, String value)设置属性
attributes()获取所有属性
id(), className() and classNames()
text()获取文本内容text(String value) 设置文本内容
html()获取元素内HTMLhtml(String value)设置元素内的HTML内容
outerHtml()获取元素外HTML内容
data()获取数据内容(例如:script和style标签)
tag() and tagName()
4.2.2. 使用选择器语法查找元素
jsoup elements对象支持类似于CSS (或jquery)的选择器语法,来实现非常强大和灵活的查找功能。这个select 方法在Document, Element,或Elements对象中都可以使用。且是上下文相关的,因此可实现指定元素的过滤,或者链式选择访问。
Select方法将返回一个Elements集合,并提供一组方法来抽取和处理结果。
4.2.2.1. Selector选择器概述
tagname: 通过标签查找元素,比如:a
ns|tag: 通过标签在命名空间查找元素,比如:可以用 fb|name 语法来查找
#id: 通过ID查找元素,比如:#logo
.class: 通过class名称查找元素,比如:.masthead
[attribute]: 利用属性查找元素,比如:[href]
[^attr]: 利用属性名前缀来查找元素,比如:可以用[^data-] 来查找带有HTML5 Dataset属性的元素
[attr=value]: 利用属性值来查找元素,比如:[width=500]
[attr^=value], [attr$=value], [attr*=value]: 利用匹配属性值开头、结尾或包含属性值来查找元素,比如:[href*=/path/]
[attr~=regex]: 利用属性值匹配正则表达式来查找元素,比如: img[src~=(?i)\.(png|jpe?g)]
*: 这个符号将匹配所有元素
4.2.2.2. Selector选择器组合使用
el#id: 元素+ID,比如: div#logo
el.class: 元素+class,比如: div.masthead
el[attr]: 元素+class,比如: a[href]
任意组合,比如:a[href].highlight
ancestor child: 查找某个元素下子元素,比如:可以用.body p 查找在"body"元素下的所有 p元素
parent > child: 查找某个父元素下的直接子元素,比如:可以用div.content > p 查找 p 元素,也可以用body > * 查找body标签下所有直接子元素
siblingA + siblingB: 查找在A元素之前第一个同级元素B,比如:div.head + div
siblingA ~ siblingX: 查找A元素之前的同级X元素,比如:h1 ~ p
el, el, el:多个选择器组合,查找匹配任一选择器的唯一元素,例如:div.masthead, div.logo
4.2.2.3. 伪选择器selectors
:lt(n): 查找哪些元素的同级索引值(它的位置在DOM树中是相对于它的父节点)小于n,比如:td:lt(3) 表示小于三列的元素
:gt(n):查找哪些元素的同级索引值大于n,比如: div p:gt(2)表示哪些div中有包含2个以上的p元素
:eq(n): 查找哪些元素的同级索引值与n相等,比如:form input:eq(1)表示包含一个input标签的Form元素
:has(seletor): 查找匹配选择器包含元素的元素,比如:div:has(p)表示哪些div包含了p元素
:not(selector): 查找与选择器不匹配的元素,比如: div:not(.logo) 表示不包含 class=logo 元素的所有 div 列表
:contains(text): 查找包含给定文本的元素,搜索不区分大不写,比如: p:contains(jsoup)
:containsOwn(text): 查找直接包含给定文本的元素
:matches(regex): 查找哪些元素的文本匹配指定的正则表达式,比如:div:matches((?i)login)
:matchesOwn(regex): 查找自身包含文本匹配指定正则表达式的元素
5. 爬虫分类
网络爬虫按照系统结构和实现技术,大致可以分为以下几种类型:通用网络爬虫、聚焦网络爬虫、增量式网络爬虫、深层网络爬虫。 实际的网络爬虫系统通常是几种爬虫技术相结合实现的
5.1. 通用网络爬虫
简单的说就是互联网上抓取所有数据。
通用网络爬虫又称全网爬虫(Scalable Web Crawler),爬行对象从一些种子 URL 扩充到整个 Web,主要为门户站点搜索引擎和大型 Web 服务提供商采集数据。
这类网络爬虫的爬行范围和数量巨大,对于爬行速度和存储空间要求较高,对于爬行页面的顺序要求相对较低,同时由于待刷新的页面太多,通常采用并行工作方式,但需要较长时间才能刷新一次页面。
5.2. 聚焦网络爬虫
简单的说就是互联网上只抓取某一种数据。
聚焦网络爬虫(Focused Crawler),又称主题网络爬虫(Topical Crawler),是指选择性地爬行那些与预先定义好的主题相关页面的网络爬虫。
和通用网络爬虫相比,聚焦爬虫只需要爬行与主题相关的页面,极大地节省了硬件和网络资源,保存的页面也由于数量少而更新快,还可以很好地满足一些特定人群对特定领域信息的需求 。
5.3. 增量式网络爬虫
简单的说就是互联网上只抓取刚刚更新的数据。
增量式网络爬虫(Incremental Web Crawler)是 指 对 已 下 载 网 页 采 取 增量式更新和只爬行新产生的或者已经发生变化网页的爬虫,它能够在一定程度上保证所爬行的页面是尽可能新的页面。
和周期性爬行和刷新页面的网络爬虫相比,增量式爬虫只会在需要的时候爬行新产生或发生更新的页面 ,并不重新下载没有发生变化的页面,可有效减少数据下载量,及时更新已爬行的网页,减小时间和空间上的耗费,但是增加了爬行算法的复杂度和实现难度。
5.4. Deep Web 爬虫
Web 页面按存在方式可以分为表层网页(Surface Web)和深层网页(Deep Web,也称 Invisible Web Pages 或 Hidden Web)。
表层网页是指传统搜索引擎可以索引的页面,以超链接可以到达的静态网页为主构成的 Web 页面。
Deep Web 是那些大部分内容不能通过静态链接获取的、隐藏在搜索表单后的,只有用户提交一些关键词才能获得的 Web 页面。
6. 案例介绍
前面介绍了几种爬虫的分类,这里我们使用聚焦网络爬虫,抓取京东上面的商品数据。
真正的开发需要不断的爬去商品数据,更新商品数据信息,所以要用增量式网络爬虫。我们这里使用定时爬去的方式来实现,所以还需要使用到定时任务Quartz
6.1. 环境准备
使用技术:jdk8+SpringBoot+HttpClient+Jsoup+Quartz+MyBatis
6.2. 搭建工程
创建Maven工程
工程需要集成SpringBoot父
6.2.1. 加入依赖
设置jdk为1.7,并加入相关依赖
6.2.2. 加入配置
在src/main/resources路径下加入以下两个配置文件
加入application.properties
#日志
logging.level.org.mybatis=DEBUG
logging.level.cn.itcast=DEBUG
#spring集成Mybatis环境
#pojo别名扫描包
mybatis.type-aliases-package=cn.itcast.crawler.pojo
#加载Mybatis核心配置文件
mybatis.mapper-locations=classpath:mapper/*Mapper.xml
mybatis.config-location=classpath:SqlMapConfig.xml
#DBConfiguration:
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/crawler?useUnicode=true&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=root
加入SqlMapConfig.xml
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
6.2.3. 编写引导类
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
6.2.4. 准备数据库表
在MySQL中执行以下语句
创建商品类目url表
CREATE TABLE `url_item_cat` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '类目ID',
`url` varchar(1000) DEFAULT NULL COMMENT '类目URL',
`name` varchar(20) DEFAULT NULL COMMENT '类目名称',
`created` datetime DEFAULT NULL COMMENT '创建时间',
`updated` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `sort_order` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='商品类目';
创建商品详url表
CREATE TABLE `url_item` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '类目ID',
`cat_id` bigint(20) DEFAULT NULL,
`url` varchar(100) DEFAULT NULL COMMENT '类目URL',
`status` tinyint(1) DEFAULT '0' COMMENT '状态。可选值:0(未下载),1(已下载)',
`created` datetime DEFAULT NULL COMMENT '创建时间',
`updated` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `parent_id` (`status`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='商品类目';
创建商品表
CREATE TABLE `item` (
`id` bigint(10) NOT NULL AUTO_INCREMENT COMMENT '商品id,同时也是商品编号',
`title` varchar(100) NOT NULL COMMENT '商品标题',
`sell_point` varchar(150) DEFAULT NULL COMMENT '商品卖点',
`price` bigint(20) NOT NULL COMMENT '商品价格,单位为:分',
`num` int(10) NOT NULL COMMENT '库存数量',
`barcode` varchar(30) DEFAULT NULL COMMENT '商品条形码',
`image` varchar(5000) DEFAULT NULL COMMENT '商品图片',
`cid` bigint(10) NOT NULL COMMENT '所属类目,叶子类目',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '商品状态,1-正常,2-下架,3-删除',
`created` datetime NOT NULL COMMENT '创建时间',
`updated` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `cid` (`cid`),
KEY `status` (`status`),
KEY `updated` (`updated`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='商品表';
6.3. 抓取流程分析
获取基础数据:
抓取商品数据:
1). 添加新的待爬url
2). 判断待添加url是否存在
3). 判断是否还有待爬url
4). 获取待爬url
5). 修改待爬url为已爬url
7. 开发准备
7.1. 创建pojo
复制资料中的pojo到工程中
7.2. 创建Mapper
创建UrlItemCatMapper
@org.apache.ibatis.annotations.Mapper
public interface UrlItemCatMapper extends Mapper
}
创建UrlItemMapper
@org.apache.ibatis.annotations.Mapper
public interface UrlItemMapper extends Mapper
}
创建ItemMapper
@org.apache.ibatis.annotations.Mapper
public interface ItemMapper extends Mapper
}
7.3. 创建Service
略
7.4. 初始化HttpClient
编写HttpClient连接池管理器
@Configuration
public class HttpClientCfg {
@Bean
public PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() {
// 创建连接池管理器
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
// 设置最大连接数
cm.setMaxTotal(200);
// 设置每个主机地址的并发数
cm.setDefaultMaxPerRoute(10);
return cm;
}
}
编写HttpClientService
public interface HttpClientService {
/**
* 发起get请求,获取html页面
*
* @param url
* @return
*/
public String getHtml(String url);
/**
* 下载图片
*
* @param url
*/
public void getPic(String url);
}
编写
@Service
public class HttpClientServiceImpl implements HttpClientService {
@Autowired
private PoolingHttpClientConnectionManager cm;
@Override
public String getHtml(String url) {
// 使用连接池管理器获取连接
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build();
// 声明get请求
// url去空格
url = StringUtils.replace(url, " ", "");
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse response = null;
try {
// 发起请求
response = httpClient.execute(httpGet);
// 判断请求是否结果是否为200
if (response.getStatusLine().getStatusCode() == 200) {
// 如果是200,返回响应体内容
String html = EntityUtils.toString(response.getEntity(), "UTF-8");
return html;
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 释放连接
if (response != null) {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
@Override
public void getPic(String url) {
// 使用连接池管理器获取连接
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build();
// 声明get请求
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse response = null;
try {
// 发起请求
response = httpClient.execute(httpGet);
// 判断请求是否结果是否为200
if (response.getStatusLine().getStatusCode() == 200) {
// 如果是200,下载图片到本地
InputStream inputStream = response.getEntity().getContent();
String picName = StringUtils.substringAfterLast(url, "/");
FileOutputStream fos = new FileOutputStream(new File("D:/pic/" + picName));
response.getEntity().writeTo(fos);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 释放连接
if (response != null) {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
7.5. 获取商品类目url
查看京东首页,左侧有一个商品分类栏,分析发现,其实是异步加载的json数据,获取json数据并解析,即可拿到所有的商品分类url
编写测试类,获取京东所有商品分类的url
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
public class ItemCatUrl {
@Autowired
private UrlItemCatMapper urlItemCatMapper;
// 商品类目访问地址
private String jdURL = "https://dc.3.cn/category/get";
// 声明json工具类
private static final ObjectMapper MAPPER = new ObjectMapper();
@Test
public void testItemCatUrl() throws Exception {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(jdURL);
CloseableHttpResponse response = httpClient.execute(httpGet);
// 判断请求是否成功
if (response.getStatusLine().getStatusCode() == 200) {
// 获取请求返回值
String json = EntityUtils.toString(response.getEntity(), "GBK");
// System.out.println(json);
// 解析返回数据
JsonNode jsonNode = MAPPER.readTree(json);
// 获取一级类目
ArrayNode array1 = (ArrayNode) jsonNode.get("data");
for (JsonNode node1 : array1) {
node1 = node1.get("s").elements().next();
System.out.println(node1.get("n").asText());
// 获取二级类目
ArrayNode array2 = (ArrayNode) node1.get("s");
for (JsonNode node2 : array2) {
System.out.println(node2.get("n").asText());
// 获取三级类目
ArrayNode array3 = (ArrayNode) node2.get("s");
for (JsonNode node3 : array3) {
// 解析数据
String str = node3.get("n").asText();
// 获取商品分类url
String url = "";
// 如果url包含域名,则拼接协议
if (StringUtils.contains(str, "jd.com")) {
url = "https://" + StringUtils.substringBefore(node3.get("n").asText(), "|");
} else {
// 如果url不包含域名,拼接协议加域名
url = "https://list.jd.com/list.html?cat=" + StringUtils.substringBefore(node3.get("n").asText(), "|");
}
// 保存商品分类url
UrlItemCat urlItemCat = new UrlItemCat();
urlItemCat.setName(StringUtils.substringBetween(str, "|", "||"));
urlItemCat.setUrl(url);
urlItemCat.setCreated(new Date());
urlItemCat.setUpdated(urlItemCat.getCreated());
this.urlItemCatMapper.insert(urlItemCat);
}
}
}
}
}
}
7.6. 去重过滤器
在使用网络爬虫过程中,去重是一个不可避免的问题,这里需要对将要爬取得商品详情url进行去重操作
传统的去重,可以使用Map或者Set集合,或者哈希表的方式来实现。在数据量较小的情况下,使用这种方式没有问题,可是当我们需要大量爬去数据的时候,这种方式就存在很大问题。因为会极大的占用内存和系统资源,导致爬虫系统崩溃。
这里将会给大家介绍两种过滤方式:布隆过滤器和redis
7.6.1. 布隆过滤器
布隆过滤器 (Bloom Filter)是由Burton Howard Bloom于1970年提出,它是一种space efficient的概率型数据结构,用于判断一个元素是否在集合中。在垃圾邮件过滤的黑白名单方法、爬虫(Crawler)的网址判重模块中等等经常被用到。
哈希表也能用于判断元素是否在集合中,但是布隆过滤器只需要哈希表的1/8或1/4的空间复杂度就能完成同样的问题。布隆过滤器可以插入元素,但不可以删除已有元素。其中的元素越多,误报率越大,但是漏报是不可能的。
布隆过滤器原理
布隆过滤器需要的是一个位数组(和位图类似)和K个映射函数(和Hash表类似),在初始状态时,对于长度为m的位数组array,它的所有位被置0。
对于有n个元素的集合S={S1,S2...Sn},通过k个映射函数{f1,f2,......fk},将集合S中的每个元素Sj(1<=j<=n)映射为K个值{g1,g2...gk},然后再将位数组array中相对应的array[g1],array[g2]......array[gk]置为1:
如果要查找某个元素item是否在S中,则通过映射函数{f1,f2,...fk}得到k个值{g1,g2...gk},然后再判断array[g1],array[g2]...array[gk]是否都为1,若全为1,则item在S中,否则item不在S中。
布隆过滤器会造成一定的误判,因为集合中的若干个元素通过映射之后得到的数值恰巧包括g1,g2,...gk,在这种情况下可能会造成误判,但是概率很小。
布隆过滤器实现:
//ip去重过滤器,布隆过滤器
public class BloomFilter {
/* BitSet初始分配2^24个bit */
private static final int DEFAULT_SIZE = 1 << 24;
/* 不同哈希函数的种子,一般应取质数 */
private static final int[] seeds = new int[] { 5, 7, 11, 13, 31, 37 };
private BitSet bits = new BitSet(DEFAULT_SIZE);
/* 哈希函数对象 */
private SimpleHash[] func = new SimpleHash[seeds.length];
public BloomFilter() {
for (int i = 0; i < seeds.length; i++) {
func[i] = new SimpleHash(DEFAULT_SIZE, seeds[i]);
}
}
// 将url标记到bits中
public void add(String url) {
for (SimpleHash f : func) {
bits.set(f.hash(url), true);
}
}
// 判断是否已经被bits标记
public boolean contains(String url) {
if (StringUtils.isBlank(url)) {
return false;
}
boolean ret = true;
for (SimpleHash f : func) {
ret = ret && bits.get(f.hash(url));
}
return ret;
}
/* 哈希函数类 */
public static class SimpleHash {
private int cap;
private int seed;
public SimpleHash(int cap, int seed) {
this.cap = cap;
this.seed = seed;
}
// hash函数,采用简单的加权和hash
public int hash(String value) {
int result = 0;
int len = value.length();
for (int i = 0; i < len; i++) {
result = seed * result + value.charAt(i);
}
return (cap - 1) & result;
}
}
}
7.6.2. redis过滤
无论我们使用Map、Set、Hash表还是布隆过滤器的方式去重,都是需要占用网络爬虫所在的服务器资源,所以如果碰到超大型的数据处理,还是有所不足
redis也有去重功能,就是set数据类型,我们也可以把需要去重的数据放到redis的set集合中,根据返回结果来判断这条数据是否重复
7.7. URL管理器
7.7.1. 编写URL管理器
URL管理器需要有以下5个功能
1. 添加新的待爬url
2. 判断待添加url是否存在
3. 判断是否还有待爬url
4. 获取待爬url
5. 修改待爬url为已爬url
public class UrlManager {
private UrlItemMapper urlItemMapper;
// 待爬url
private List
private BloomFilter bloomFilter;
public UrlManager(UrlItemMapper urlItemMapper) {
this.urlItemMapper = urlItemMapper;
// 初始化布隆过滤器
this.initBloomFilter();
// 初始化待查url
this.queryUrl();
}
/**
* 1).添加新的待爬url
*
* @param url
*/
public void addUrlItem(UrlItem urlItem) {
// 判断商品url是否重复
if (this.bloomFilter.contains(urlItem.getUrl())) {
// 如果重复直接返回
return;
}
// 如果不重复,保存到数据库中
this.urlItemMapper.insert(urlItem);
// 加入到过滤器中
this.bloomFilter.add(urlItem.getUrl());
}
/**
* 2). 判断待添加url是否存在
*
* @param url
* @return
*/
public boolean contains(String url) {
return this.bloomFilter.contains(url);
}
/**
* 3). 判断是否还有待爬url
*
* @return
*/
public boolean isEmpty() {
// 判断待爬url集合中是否为空
if (this.urlList.size() == 0) {
// 如果为空,则再次从数据库中查询待爬url
this.queryUrl();
}
return this.urlList.size() == 0;
}
/**
* 4). 获取待爬url
*
* @return
*/
public UrlItem getUrl() {
if (this.isEmpty()) {
// 如果为空,返回null
return null;
} else {
return this.urlList.get(0);
}
}
/**
* 5). 修改待爬url为已爬url
*
* @param urlItem
*/
public void changeUrl(UrlItem urlItem) {
urlItem.setStatus(false);
urlItem.setUpdated(new Date());
this.urlItemMapper.updateByPrimaryKeySelective(urlItem);
// 删除待爬url中的元素
this.urlList.remove(urlItem);
}
/**
* 初始化布隆过滤器
*/
private void initBloomFilter() {
// 创建布隆过滤器
this.bloomFilter = new BloomFilter();
// 初始化布隆过滤器
int page = 1, pageSize = 0;
do {
// 设置分页
PageHelper.startPage(page, 5000);
// 查询
List
// 查询到的url放到过滤器中
for (UrlItem urlItem : list) {
this.bloomFilter.add(urlItem.getUrl());
}
page++;
pageSize = list.size();
} while (pageSize == 5000);
}
/**
* 分页查询待查url
*/
private void queryUrl() {
// 设置分页
PageHelper.startPage(1, 500);
// 设置查询条件
UrlItem param = new UrlItem();
param.setStatus(false);
// 查询
this.urlList = this.urlItemMapper.select(param);
}
}
7.7.2. 整合URL管理器
@Configuration
public class UrlManagerCfg {
@Bean
public UrlManager urlManager(UrlItemMapper urlItemMapper) {
// 创建url管理器
UrlManager urlManager = new UrlManager(urlItemMapper);
return urlManager;
}
}
8. 爬取商品详情url
8.1. 编写任务
使用定时任务实现商品url获取,编写定时任务
@DisallowConcurrentExecution
public class UrlItemJob extends QuartzJobBean {
private UrlItemCatService urlitemCatService;
private HttpClientService httpClientService;
private UrlManager urlManager;
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
ApplicationContext applicationContext = (ApplicationContext) context.getJobDetail().getJobDataMap()
.get("context");
// 获取服务实例
this.urlitemCatService = applicationContext.getBean(UrlItemCatService.class);
this.httpClientService = applicationContext.getBean(HttpClientService.class);
this.urlManager = applicationContext.getBean(UrlManager.class);
// 解析商品类目的页面,获取商品详情url
List
// 遍历类目页面,获取商品详情url
for (UrlItemCat urlItemCat : urlItemCats) {
// 抓取所有数据量较大,这里我们就抓取一页商品,每页是60条数据
// 根据商品类目,获取该类目的商品详情url
String next = this.getItemInfo(urlItemCat);
System.out.println(next);
}
}
/**
* 根据商品类目,获取该类目的商品详情url
*
* @param url
* @return 返回下一页url
*/
private String getItemInfo(UrlItemCat urlItemCat) {
// 获取html页面
String html = this.httpClientService.getHtml(urlItemCat.getUrl());
// 使用jsoup解析
Document doc = Jsoup.parse(html);
// 获取当前页的所有商品url
List
// 保存商品url
for (String href : hrefs) {
// 保存
this.saveUrlItem("https:" + href, urlItemCat.getId());
}
// 获取当前商品分类的下一页url
String href = "";
Elements next = doc.getElementsByClass("pn-next");
// 如果有下一页,返回下一页url
if (!next.isEmpty()) {
href = next.first().attr("href");
}
return href;
}
/**
* 保存商品url
*
* @param urlItem
*/
private void saveUrlItem(String href, Long catId) {
// 判断该url是否存在
if (this.urlManager.contains(href)) {
return;
}
// 不存在则保存
UrlItem urlItem = new UrlItem();
urlItem.setCatId(catId);
urlItem.setUrl(href);
urlItem.setCreated(new Date());
urlItem.setUpdated(urlItem.getCreated());
// 保存商品url到数据库
this.urlManager.addUrlItem(urlItem);
}
}
8.2. 整合任务
@Configuration
public class SchedledCfg {
// 定义任务
@Bean("itemJobBean")
public JobDetailFactoryBean itemJobBean() {
JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean();
jobDetailFactoryBean.setApplicationContextJobDataKey("context");
jobDetailFactoryBean.setJobClass(ItemJob.class);
jobDetailFactoryBean.setDurability(true);
return jobDetailFactoryBean;
}
// 定义触发器
@Bean("itemJobTrigger")
public CronTriggerFactoryBean ItemJobTrigger(@Qualifier(value = "itemJobBean") JobDetailFactoryBean itemJobBean) {
CronTriggerFactoryBean tigger = new CronTriggerFactoryBean();
tigger.setJobDetail(itemJobBean.getObject());
tigger.setCronExpression("0/5 * * * * ? ");
return tigger;
}
// 定义调度器
@Bean
public SchedulerFactoryBean schedulerFactory(CronTrigger[] cronTriggerImpl) {
SchedulerFactoryBean bean = new SchedulerFactoryBean();
bean.setTriggers(cronTriggerImpl);
return bean;
}
}
9. 爬取商品详情数据
9.1. 编写任务
@DisallowConcurrentExecution
public class ItemJob extends QuartzJobBean {
private ItemService itemService;
private HttpClientService httpClientService;
private UrlManager urlManager;
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
ApplicationContext applicationContext = (ApplicationContext) context.getJobDetail().getJobDataMap()
.get("context");
// 获取服务实例
this.itemService = applicationContext.getBean(ItemService.class);
this.httpClientService = applicationContext.getBean(HttpClientService.class);
this.urlManager = applicationContext.getBean(UrlManager.class);
// 获取商品url
UrlItem urlItem = this.urlManager.getUrl();
// 获取商品数据
Item item = this.parseItem(urlItem);
}
/**
* 解析页面获取商品
*
* @param html
* @return
*/
private Item parseItem(UrlItem urlItem) {
// 创建商品对象
Item item = new Item();
// 获取商品页面
String html = this.httpClientService.getHtml(urlItem.getUrl());
// 使用jsoup解析
Document doc = Jsoup.parse(html);
// 商品标题
String title = doc.select("div.sku-name").text();
item.setTitle(title);
// 商品价格
// item.setPrice(1l);
// 商品图片
// item.setImage("");
// 类目id
item.setCid(urlItem.getCatId());
// 正常状态
item.setStatus(1);
item.setCreated(new Date());
item.setUpdated(item.getCreated());
return null;
}
}
9.2. 整合任务
// @Bean("urlItemJobBean")
public JobDetailFactoryBean urlItemJobBean() {
JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean();
jobDetailFactoryBean.setApplicationContextJobDataKey("context");
jobDetailFactoryBean.setJobClass(UrlItemJob.class);
jobDetailFactoryBean.setDurability(true);
return jobDetailFactoryBean;
}
// 定义触发器
// @Bean("urlItemJobTrigger")
public CronTriggerFactoryBean urlItemJobTrigger(
@Qualifier(value = "urlItemJobBean") JobDetailFactoryBean urlItemJobBean) {
CronTriggerFactoryBean tigger = new CronTriggerFactoryBean();
tigger.setJobDetail(urlItemJobBean.getObject());
tigger.setCronExpression("0/5 * * * * ? ");
return tigger;
}
10. 动态网页
上面抓取商品详情的例子中,我们发现可以分析静态页面html,但是对js的解析部分还是很薄弱。虽然我们可以读取js的运作机制并且找到相关数据,但是这样会耗费大量时间。
除了人力解析js以外,我们还可以使用工具来模拟浏览器的运行,直接获取解析结果。这里我们使用
10.1. phantomJs+selenium
(1)一个基于webkit内核的无头浏览器,即没有UI界面,即它就是一个浏览器,只是其内的点击、翻页等人为相关操作需要程序设计实现。
(2)提供javascript API接口,即通过编写js程序可以直接与webkit内核交互,在此之上可以结合java语言等,通过java调用js等相关操作,从而解决了以前c/c++才能比较好的基于webkit开发优质采集器的限制。
(3)提供windows、linux、mac等不同os的安装使用包,也就是说可以在不同平台上二次开发采集项目或是自动项目测试等工作。
下载phantomJs
在官网上下载
http://phantomjs.org/download.html
selenium官网
http://www.seleniumhq.org/projects/webdriver/
工程加入依赖
11. 其他信息提取
11.1. 从非html提取文本
11.1.1. PDF文件
PDF 是 Adobe公司开发的电子文件格式。这种文件格式与操作系统的平台无关,可以在多数操作系统上通用。
我们经常会遇到这种情况,就是想把PDF文件中的文字复制下来,却发现不能复制,因为PDF文件的内容可能加密了,那么把PDF文件中的内容提取出来就是我们要解决的问题。
现在已经有很多工具可以帮助完成这个任务。例如PDFBox(https://pdfbox.apache.org/)就是专门用来解析PDF文件的Java项目。
11.1.2. Word、Excel文件
Word是微软公司开发的字处理文件格式,以“doc”或者“docx”作为文件后缀名。Apache的POI(http://poi.apache.org/)可以用来读取Word文档。
Excel也是微软公司开发的字处理文件格式,是由工作簿(Workbook)组成,工作簿由一个或多个工作表(Sheet)组成,每个工作表都有自己的名称,每个工作表又包含多个单元格(Cell)。除了POI项目,还有开源的jxl可以用来读写Excel。
11.2. 图像的OCR识别
11.2.1. OCR介绍
抓取过程中,如果碰到需要获取的数据是图片的格式,那么我们还需要把图片转换成文字。
从图片中识别出字符叫做光学字符识别(Optical Character Recognition)简称OCR。是指电子设备(例如扫描仪或数码相机)检查纸上打印的字符,通过检测暗、亮的模式确定其形状,然后用字符识别方法将形状翻译成计算机文字的过程。即,针对印刷体字符,采用光学的方式将纸质文档中的文字转换成为黑白点阵的图像文件,并通过识别软件将图像中的文字转换成文本格式。
文字识别包括以下几个步骤:
(1)图文输入
是指通过输入图片到计算机中,也就是获取图片。
(2)预处理
扫描一幅简单的印刷文档的图像,将每一个文字图像分检出来交给识别模块识别,这一过程称为图像预处理。预处理是指在进行文字识别之前的一些准备工作,包括图像净化处理,去掉原始图像中的显见噪声(干扰),
(3)单字识别
单字识别是体现OCR文字识别的核心技术。从扫描文本中分检出的文字图像,由计算机将其图形,图像转变成文字的标准代码.,是让计算机“认字”的关键,也就是所谓的识别技术。
识别技术就是特征比较技术,通过和识别特征库的比较,找到特征最相似的字,提取该文字的标准代码,即为识别结果。
(4)后处理
后处理是指对识别出的文字或多个识别结果采用词组方式进行上下匹配,即将单字识别的结果进行分词,与词库中的词组进行比较,以提高系统的识别率,减少误识率。 汉字字符识别是文字识别领域最为困难的问题,它涉及模式识别,图像处理,数字信号处理,自然语言理解,人工智能,模糊数学,信息论,计算机,中文信息处理等学科,是一门综合性技术。
11.2.2. Tess4j
Tess4J是一个OCR图片识别技术,我们这里使用的是Tess4J-3.4.2。在windows使用前必须安装Visual C++ 2015 Redistributable Packages,下载地址:
https://www.microsoft.com/zh-CN/download/details.aspx?id=48145)
首先需要在pom.xml中添加以下依赖
在项目的根路径下复制tessdata文件夹
Tess4j对多种语言都提供相关的语言包,可以在以下地址下载
https://github.com/tesseract-ocr/tessdata
我们这里实现的是识别图片中的数字。编写测试代码
public class Tess4jTest {
public static void main(String[] args) throws Exception {
CloseableHttpClient httpClient = HttpClients.createDefault();
BufferedImage image = ImageIO.read(new File("C:/Users/tree/Desktop/53281.png"));
image = image.getSubimage(4, 8, 42, 17);
Image scaledInstance = image.getScaledInstance(46, 25, image.SCALE_SMOOTH);// 设置缩放目标图片模板
// wr = 46 * 1.0 / bufImg.getWidth(); // 获取缩放比例
// hr = 25 * 1.0 / bufImg.getHeight();
AffineTransformOp ato = new AffineTransformOp(AffineTransform.getScaleInstance(2.5, 2.5), null);
scaledInstance = ato.filter(image, null);
File file = new File("tessdata/temp.jpg");
ImageIO.write((BufferedImage) scaledInstance, "jpg", file);
ITesseract instance = new Tesseract();
instance.setLanguage("eng");
long startTime = System.currentTimeMillis();
// Rectangle rectangle = new Rectangle
// String ocrResult = instance.doOCR(imgDir, rectangle);
String ocrResult = instance.doOCR(file);
// 输出识别结果
System.out.println("OCR Result: \n" + ocrResult + "\n 耗时:" + (System.currentTimeMillis() - startTime) + "ms");
System.out.println(NumberUtil.str2NumIp(ocrResult));
}
}
11.3. 提取地域信息
11.3.1. ip地址
网上有很多的服务商可以提供ip和地址的对应,可以通过这些服务商,根据ip获取到对应的地址。
有些服务商提供免费的ip库下载,例如纯真ip(http://www.cz88.net/),我们也可以用其作为离线ip库,并加以维护
11.3.2. 电话号码
我们也可以通过手机电话号码的前七位确定其地址,或者通过其连接互联网使用的ip来进行查询