SpringBoot爬虫
前言此文章只是为了学习http请求、jsoup、SpringBoot集成等技术,不是故意爬取数据,文章仅仅记录学习过程!什么是爬虫爬虫简介网络爬虫(又被称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。简单来说就是通过编写脚本模拟浏览器发起请求获取数据。爬虫分类通用网络爬虫(General Purpose Web Cr
前言
此文章只是为了学习http请求、jsoup、SpringBoot集成等技术,不是故意爬取数据,文章仅仅记录学习过程!
什么是爬虫
爬虫简介
网络爬虫(又被称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。
简单来说就是通过编写脚本模拟浏览器发起请求获取数据。
爬虫分类
-
通用网络爬虫(General Purpose Web Crawler):爬取一整张页面源码数据. 抓取系统(爬虫)
-
聚焦网络爬虫(Focused Web Crawler):爬取的是一张页面中局部的数据(数据解析)
-
增量式网络爬虫(Incremental Web Crawler):用于监测网站数据更新的情况,从而爬取网站中最新更新出来的数据
-
深层网络爬虫(Deep Web Crawler):Web 页面按存在方式可以分为表层网页(Surface Web)和深层网页(Deep Web,也称 Invisible Web Pages 或 Hidden Web)。 表层网页是指传统搜索引擎可以索引的页面,以超链接可以到达的静态网页为主构成的 Web 页面。Deep Web 是那些大部分内容不能通过静态链接获取的、隐藏在搜索表单后的,只有用户提交一些关键词才能获得的 Web 页面。
反爬机制与反反爬策略
爬虫:使用任何技术手段,批量获取网站信息的一种方式。
反爬虫:使用任何技术手段,阻止别人批量获取自己网站信息的一种方式。
反爬方式:
-
robots.txt协议
-
UA(User-Agent用户访问网站时候的浏览器标识)限制
-
UA反爬随机请求头
-
ip限制(限制ip访问频率和次数进行反爬)-------------构造自己的 IP 代理池,然后每次访问时随机选择代理
-
Ajax动态加载-------使用审查元素分析”请求“对应的链接:在url请求的response中进行局部搜索当前内容,如果没有就点击左边任意请求,进行ctrl+f全局搜索,找到对应的请求(抓包工具推荐:fiddler)
-
验证码反爬虫或者模拟登陆
-
cookie限制
爬虫案例学习
案例需求
前面介绍了几种爬虫的分类,这里我们使用聚焦网络爬虫,抓取汽车之家上的汽车评测数据。https://www.autohome.com.cn/bestauto/
我们需要抓取汽车之家上面所有的汽车评测数据
在页面上我们分析,需要抓取以下部分的数据:
- 车型信息
- 评测信息
排名是动态生成的,我们这里不做抓取,可以后期单独处理排名
-
编辑点评
-
评测图片
有5张图片,页面显示的是小图,我们需要打开超链接获取大图的url地址,再单独下载图片
环境准备
使用技术
- JDK1.8+
- SpringBoot2.X
- MyBatisPlus
- SpringMVC
- HttpClient
- Jsoup
- Quartz
搭建工程
设置依赖
- 新建MAVEN项目
- 设置父工程
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>
- 设置项目依赖
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<mybatisplus.version>3.3.2</mybatisplus.version>
<alibaba.boot.druid>1.1.22</alibaba.boot.druid>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatisplus.version}</version>
<exclusions>
<exclusion>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${alibaba.boot.druid}</version>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
<!-- quartz依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<!-- HttpClient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!-- jsoup -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.10.3</version>
</dependency>
<!--lombok插件 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
<optional>true</optional>
</dependency>
</dependencies>
设置配置
配置路径:src/main/resources
- 创建
application.yml
配置总体环境,方便切换环境
spring:
profiles:
active: dev
application:
name: spider-autohome
- 创建测试环境配置
application-dev.yml
server:
port: 8080
tomcat:
max-swallow-size: 100MB
#配置数据源
spring:
datasource:
druid:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.56.120:3306/spider-autohome?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false
username: root
password: 123456
# 初始化连接大小
initial-size: 5
# 最小空闲连接数
min-idle: 5
max-active: 30
max-wait: 60000
# 可关闭的空闲连接间隔时间
time-between-eviction-runs-millis: 60000
# 配置连接在池中的最小生存时间
min-evictable-idle-time-millis: 300000
validation-query: select '1' from dual
test-while-idle: true
test-on-borrow: false
test-on-return: false
# 打开PSCache,并且指定每个连接上PSCache的大小
pool-prepared-statements: true
max-open-prepared-statements: 50
max-pool-prepared-statement-per-connection-size: 20
# 配置监控统计拦截的filters
filters: stat
stat-view-servlet:
url-pattern: /druid/*
reset-enable: false
login-username: admin
login-password: 123456
web-stat-filter:
url-pattern: /*
exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
servlet:
multipart:
max-file-size: 50MB
max-request-size: 50MB
# 配置mybatis-plus日志打印
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
SpringBoot入门
这里先实现入门程序,用以熟悉SpringBoot的使用。
需求:浏览器访问,获取数据库时间
创建启动引导类
@SpringBootApplication
public class SpiderAutoHomeApplication {
public static void main(String[] args) {
SpringApplication.run(SpiderAutoHomeApplication.class,args);
}
}
编写测试DAO
- 创建dao文件夹,创建
TestDao
接口文件
@Mapper
public interface TestDao {
/**
* 查询当前时间
* @return
*/
@Select("SELECT NOW()")
public String queryNowDate();
}
编写测试SERVICE
- 创建service文件夹,创建
TestService
文件
public interface TestService {
/**
* 查询当前时间
* @return
*/
public String queryNowDate();
}
编写测试SERVICE实现
- 在service文件夹下创建impl文件夹,创建
TestServiceImpl
文件
@Service
public class TestServiceImpl implements TestService {
@Autowired
private TestDao testDao;
@Override
public String queryNowDate() {
return testDao.queryNowDate();
}
}
编写请求CONTROLLER
- 创建controller文件夹,创建
TestController
文件
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private TestService testService;
/**
* 查询当前时间
* @return
*/
@GetMapping(value = "/queryNowDate")
public String queryNowDate(){
return testService.queryNowDate();
}
}
启动测试
-
启动application启动类
-
在浏览器输入请求测试地址:
http://localhost:8080/test/queryNowDate
-
查看返回结果:
2021-05-09 09:31:42
开发分析
流程分析
分析发现,评测页的url是:
https://www.autohome.com.cn/bestauto/1
最后一个参数是页码数,我们只需要按顺序从第一页开始,把所有的页面都抓取下来就可以了
抓取页面的流程如下
抓取评测数据步骤
-
根据url抓取html页面
-
对html页面进行解析,获取该页面所有的评测数据
-
遍历所有的评测数据
-
判断遍历的评测数据是否已保存,
-
如果已保存再次遍历下一条评测数据
-
如果未保存执行下一步
-
保存评测数据到数据库中
数据库表设计
根据以上需求,设计数据库表。sql如下:
CREATE TABLE `car_test` (
`id` bigint(10) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`title` varchar(100) NOT NULL COMMENT '评测车辆的名字',
`test_speed` int(150) DEFAULT NULL COMMENT '评测项目-加速(0-100公里/小时),单位毫秒',
`test_brake` int(150) DEFAULT NULL COMMENT '评测项目-刹车(100-0公里/小时),单位毫米',
`test_oil` int(150) DEFAULT NULL COMMENT '评测项目-实测油耗(升/100公里),单位毫升',
`editor_name1` varchar(10) DEFAULT NULL COMMENT '评测编辑1',
`editor_remark1` varchar(1000) DEFAULT NULL COMMENT '点评内容1',
`editor_name2` varchar(10) DEFAULT NULL COMMENT '评测编辑2',
`editor_remark2` varchar(1000) DEFAULT NULL COMMENT '点评内容2',
`editor_name3` varchar(10) DEFAULT NULL COMMENT '评测编辑3',
`editor_remark3` varchar(1000) DEFAULT NULL COMMENT '点评内容3',
`image` varchar(1000) DEFAULT NULL COMMENT '评测图片,5张图片名,中间用,分隔',
`created` datetime DEFAULT NULL COMMENT '创建时间',
`updated` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 COMMENT='汽车之家评测表';
开发准备
编写实体ENTITY
- 创建module【这个依据个人喜好】文件夹,创建
CarTest
实体对象和数据库表进行映射
@Data
@TableName(value = "car_test")
public class CarTest {
@TableId(type = IdType.AUTO)
private Long id;
private String title;
private int testSpeed;
private int testBrake;
private int testOil;
private String editorName1;
private String editorRemark1;
private String editorName2;
private String editorRemark2;
private String editorName3;
private String editorRemark3;
private String image;
private Date created;
private Date updated;
}
编写DAO
- 在dao文件夹下面创建
CarTestDao
@Mapper
public interface CarTestDao extends BaseMapper<CarTest> {
}
编写SERVICE
- 在service文件夹下面创建
CarTestService
public interface CarTestService extends IService<CarTest> {
/**
* 分页查询标题
* @param page 当前页
* @param pageSize 分页大小
* @return
*/
public Page<CarTest> queryTitleByPage(long page, long pageSize);
}
编写SERVICE实现
- service文件夹下impl文件夹新建
CarTestServiceImpl
@Service
public class CarTestServiceImpl extends ServiceImpl<CarTestDao,CarTest> implements CarTestService {
@Override
public Page<CarTest> queryTitleByPage(long page, long pageSize) {
Page<CarTest> queryPage = new Page<>(page, pageSize);
QueryWrapper<CarTest> queryWrapper = new QueryWrapper<>();
queryWrapper.select("title");
return baseMapper.selectPage(queryPage, queryWrapper);
}
}
爬取数据
HTTP连接池管理器
因为我们爬取数据是使用的HTTP请求,我们需要一个管理HTTP连接的一个工具,所以我们定义一个HTTP连接池管理工具,交给Spring进行管理。
使用以下两个注解
@Configuration注解声明配置类。
@Bean注解声明如何创建这实例
- 新建config文件夹,创建
HttpClientManagerCfg
@Configuration
public class HttpClientManagerCfg {
@Bean
public PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() {
// 创建连接管理器
PoolingHttpClientConnectionManager httpClientConnectionManager = new PoolingHttpClientConnectionManager();
// 设置最大连接数
httpClientConnectionManager.setMaxTotal(50);
// 设置每个并发连接数
httpClientConnectionManager.setDefaultMaxPerRoute(20);
return httpClientConnectionManager;
}
}
定时关闭失效连接
这里使用Quartz定时任务来处理定时关闭失效连接
- 新建job文件夹,创建
CloseHttpConnectionJob
文件,编写定时任务
@Slf4j
@DisallowConcurrentExecution
public class CloseHttpConnectionJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
ApplicationContext applicationContext = (ApplicationContext) context.getJobDetail().getJobDataMap().get("context");
PoolingHttpClientConnectionManager httpClientPool = applicationContext.getBean(PoolingHttpClientConnectionManager.class);
httpClientPool.closeExpiredConnections();
log.info(">>>>>>>>>>>>>>>>>>>>>>>> closeExpiredConnections");
}
}
定时任务配置
- 在config目录下雪创建
QuartzConfig
文件
@Configuration
public class QuartzConfig {
/**
* 定义关闭无效连接任务
*/
@Bean("closeHttpConnectionJob")
public JobDetailFactoryBean closeHttpConnectionJob() {
JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean();
jobDetailFactoryBean.setApplicationContextJobDataKey("context");
jobDetailFactoryBean.setJobClass(CloseHttpConnectionJob.class);
jobDetailFactoryBean.setDurability(true);
return jobDetailFactoryBean;
}
/**
* 定义关闭无效连接触发器
*/
@Bean("closeHttpConnectionJobTrigger")
public CronTriggerFactoryBean closeHttpConnectionJobTrigger(
@Qualifier(value = "closeHttpConnectionJob") 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;
}
}
编写APISERVICE业务接口
需要实现两个功能的下载:
请求获取页面数据[GET]
请求下载图片[GET]
- 新建api.service目录,创建
AutoHomeApiService
文件
public interface AutoHomeApiService {
/**
* 使用get请求获取页面数据
* @param url
* @return
*/
public String getHtml(String url);
/**
* 使用get请求下载图片,返回图片名称
* @param url
* @return
*/
public String getImage(String url);
}
编写APISERVICE实现业务接口
- 在api.service下面创建impl文件夹,创建
AutoHomeApiServiceImpl
文件
@Service
@Slf4j
public class AutoHomeApiServiceImpl implements AutoHomeApiService {
@Autowired
private PoolingHttpClientConnectionManager connectionManager;
@Override
public String getHtml(String url) {
// 使用连接池管理器获取连接
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager).build();
// 创建httpGet请求
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse httpResponse = null;
String html = null;
try {
// 发起请求
httpResponse = httpClient.execute(httpGet);
// 判断请求是否成功
if (httpResponse != null && httpResponse.getStatusLine().getStatusCode() == 200) {
// 判断是否有响应体
if (httpResponse.getEntity() != null) {
// 如果有响应体,则进行解析
html = EntityUtils.toString(httpResponse.getEntity(), Charsets.UTF_8);
return html;
}
}
} catch (IOException e) {
e.printStackTrace();
log.error("获取汽车之家信息异常:{}", e);
}finally {
if (httpResponse != null) {
try {
httpResponse.close();
} catch (IOException e) {
e.printStackTrace();
log.error("获取汽车之家信息响应关闭异常:{}", e);
}
}
}
return null;
}
@Override
public String getImage(String url) {
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager).build();
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse httpResponse = null;
String fileName = null;
try {
httpResponse = httpClient.execute(httpGet);
// 判断请求是否成功
if (httpResponse != null && httpResponse.getStatusLine().getStatusCode() == 200) {
// 判断是否有响应体
if (httpResponse.getEntity() != null) {
// 如果有响应体,则进行解析
String contentTypeVal = httpResponse.getFirstHeader("Content-Type").getValue();
if(contentTypeVal.contains("image/")){
String extName = contentTypeVal.split("/")[1];
fileName = UUID.randomUUID().toString().replace("-","") + "." + extName;
OutputStream os = new FileOutputStream(new File("D:/test/autohome-image/" + fileName));
httpResponse.getEntity().writeTo(os);
return fileName;
}
}
}
} catch (IOException e) {
e.printStackTrace();
log.error("获取汽车之家评测图片异常:{}", e);
}finally {
if (httpResponse != null) {
try {
httpResponse.close();
} catch (IOException e) {
e.printStackTrace();
log.error("获取汽车之家评测图片响应关闭异常:{}", e);
}
}
}
return null;
}
}
测试APISERVICE业务实现接口
这里使用SpringBoot的测试组件,需要添加如下两个注解:
- @RunWith(value = SpringJUnit4ClassRunner.class)
让测试运行在spring的环境,这样我们测试的时候就可以和开发的时候一样编写代码,例如使用@Autowired注解直接注入
- @SpringBootTest(classes = Application.class)
执行当前的这个类是测试类,测试代码如下
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = SpiderAutoHomeApplication.class)
public class AutoHomeApiServiceTest {
@Autowired
private AutoHomeApiService autoHomeApiService;
@Autowired
private TitleFilter titleFilter;
@Autowired
private CarTestService carTestService;
/**
* 测试获取HTML内容
*/
@Test
public void getHtml() {
String html = autoHomeApiService.getHtml("https://www.autohome.com.cn/bestauto/");
System.out.println("html = " + html);
}
/**
* 测试获取图片
*/
@Test
public void getImage() {
String image = autoHomeApiService.getImage("https://car2.autoimg.cn/cardfs/product/g24/M09/AE/EB/800x0_1_q87_autohomecar__wKgHIVpxGh6AFSN1AAY8kcz3Aww921.jpg");
System.out.println("image = " + image);
}
}
去重过滤器
在使用网络爬虫过程中,去重是一个不可避免的问题,这里需要对抓取的数据内容进行过滤,就是对车辆幸好名称进行去重过滤,避免同样条数据反复保存到数据库中。
传统的去重,可以使用Map或者Set集合、哈希表的方式来实现去重,在数据量较小的情况下,使用这种方式没有问题。可是当我们需要大量爬去数据的时候,这种方式就存在很大问题。因为会极大的占用内存和系统资源,导致爬虫系统崩溃。这里将会使用布隆过滤器。
Bloom过滤器介绍
布隆过滤器主要用于判断一个元素是否在一个集合中,它可以使用一个位数组简洁的表示一个数组。它的空间效率和查询时间远远超过一般的算法,但是它存在一定的误判的概率,适用于容忍误判的场景。如果布隆过滤器判断元素存在于一个集合中,那么大概率是存在在集合中,如果它判断元素不存在一个集合中,那么一定不存在于集合中。常常被用于大数据去重。
算法思想
布隆过滤器算法主要思想就是利用k个哈希函数计算得到不同的哈希值,然后映射到相应的位数组的索引上,将相应的索引位上的值设置为1。判断该元素是否出现在集合中,就是利用k个不同的哈希函数计算哈希值,看哈希值对应相应索引位置上面的值是否是1,如果有1个不是1,说明该元素不存在在集合中。但是也有可能判断元素在集合中,但是元素不在,这个元素所有索引位置上面的1都是别的元素设置的,这就导致一定的误判几率。布隆过滤的思想如下图所示:
布隆过滤器实现
- 创建util目录,创建
TitleFilter
文件
public class TitleFilter {
private static final int DEFAULT_SIZE = 2 << 24;
private static final int[] seeds = new int[]{5, 7, 11, 13, 31, 37, 61};
private BitSet bits = new BitSet(DEFAULT_SIZE);
private SimpleHash[] func = new SimpleHash[seeds.length];
public TitleFilter() {
for (int i = 0; i < seeds.length; i++) {
func[i] = new SimpleHash(DEFAULT_SIZE, seeds[i]);
}
}
public void add(String value) {
for (SimpleHash f : func) {
bits.set(f.hash(value), true);
}
}
public boolean contains(String value) {
if (value == null) {
return false;
}
boolean ret = true;
for (SimpleHash f : func) {
ret = ret && bits.get(f.hash(value));
}
return ret;
}
/**
* 内部类,simpleHash
*/
public static class SimpleHash {
private int cap;
private int seed;
public SimpleHash(int cap, int seed) {
this.cap = cap;
this.seed = seed;
}
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;
}
}
}
初始化去重过滤器
项目一启动,就应该创建去重过滤器。
编写以下代码实现过滤器初始化
CarTestService增加分页查询方法
/**
* 分页查询标题
* @param page 当前页
* @param pageSize 分页大小
* @return
*/
public Page<CarTest> queryTitleByPage(long page, long pageSize);
CarTestService增加分页查询方法实现
@Override
public Page<CarTest> queryTitleByPage(long page, long pageSize) {
Page<CarTest> queryPage = new Page<>(page, pageSize);
QueryWrapper<CarTest> queryWrapper = new QueryWrapper<>();
queryWrapper.select("title");
return baseMapper.selectPage(queryPage, queryWrapper);
}
实现初始化去重过滤器
@Configuration
public class TitleFilterConfig {
@Autowired
private CarTestService carTestService;
@Bean
public TitleFilter titleFilter() {
// 创建车辆标题过滤器
TitleFilter titleFilter = new TitleFilter();
// 从数据库查询车辆标题,分页查询
long page = 1;
long pageSize = 5000;
boolean repatedFlag = true;
do {
Page<CarTest> carTestPage = carTestService.queryTitleByPage(page, pageSize);
if (!carTestPage.hasNext()) {
repatedFlag = false;
}else {
page += 1;
}
for (CarTest record : carTestPage.getRecords()) {
titleFilter.add(record.getTitle());
}
} while (repatedFlag);
return titleFilter;
}
}
实现爬取数据
首先实现数据爬取逻辑,先在测试方法中实现
实现爬取测试方法
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = SpiderAutoHomeApplication.class)
public class AutoHomeApiServiceTest {
@Autowired
private AutoHomeApiService autoHomeApiService;
@Autowired
private TitleFilter titleFilter;
@Autowired
private CarTestService carTestService;
/**
* 测试获取HTML内容
*/
@Test
public void getHtml() {
String html = autoHomeApiService.getHtml("https://www.autohome.com.cn/bestauto/");
System.out.println("html = " + html);
}
/**
* 测试获取图片
*/
@Test
public void getImage() {
String image = autoHomeApiService.getImage("https://car2.autoimg.cn/cardfs/product/g24/M09/AE/EB/800x0_1_q87_autohomecar__wKgHIVpxGh6AFSN1AAY8kcz3Aww921.jpg");
System.out.println("image = " + image);
}
/**
* 获取评测数据
*/
@Test
public void testGetEvaluatingResult() {
List<CarTest> saveList = new ArrayList<>();
for (int i = 0; i < 3; i++) {
String baseUrl = "https://www.autohome.com.cn/bestauto/";
String html = autoHomeApiService.getHtml(baseUrl + i);
Document document = Jsoup.parse(html);
Elements carElements = document.getElementsByClass("uibox");
for (Element carElement : carElements) {
String carTitle = carElement.getElementsByClass("uibox-title uibox-title-border").text();
/* if (titleFilter.contains(carTitle)) {
continue;
}*/
CarTest carTest = marshalCarElement(carElement);
String imageNames = marshalImageNames(carElement);
carTest.setImage(imageNames);
saveList.add(carTest);
}
if (!CollectionUtils.isEmpty(saveList)) {
carTestService.saveBatch(saveList);
}
}
}
/**
* 解析数据下载评测图片
* @param carElement
* @return
*/
private String marshalImageNames(Element carElement) {
String carImageName = null;
List<String> imageNameList = new ArrayList<>();
Elements imageElements = carElement.select(".piclist-box.fn-clear ul.piclist02 a");
for (Element imageElement : imageElements) {
String imageUrl = "https:" + imageElement.getElementsByTag("img").attr("src");
String imageName = autoHomeApiService.getImage(imageUrl);
imageNameList.add(imageName);
}
if (!CollectionUtils.isEmpty(imageNameList)) {
carImageName = StringUtils.join(imageNameList, ",");
}
return carImageName;
}
/**
* 解析数据封装成汽车评测对象
* @param carElement
* @return
*/
private CarTest marshalCarElement(Element carElement) {
CarTest carTest = new CarTest();
String carTitle = carElement.getElementsByClass("uibox-title uibox-title-border").text();
carTest.setTitle(carTitle);
String testSpeed = carElement.select(".tabbox1 dd:nth-child(2) > div.dd-div2").first().text();
carTest.setTestSpeed(covertStrToNum(testSpeed));
String testBrake = carElement.select(".tabbox1 dd:nth-child(3) > div.dd-div2").first().text();
carTest.setTestBrake(covertStrToNum(testBrake));
String testOil = carElement.select(".tabbox1 dd:nth-child(4) > div.dd-div2").first().text();
carTest.setTestOil(covertStrToNum(testOil));
String editorName1 = carElement.select(".tabbox2.tabbox-score dd:nth-child(2) > div.dd-div1").first().text();
carTest.setEditorName1(editorName1);
String editorRemark1 = carElement.select(".tabbox2.tabbox-score dd:nth-child(2) > div.dd-div3").first().text();
carTest.setEditorRemark1(editorRemark1);
String editorName2 = carElement.select(".tabbox2.tabbox-score dd:nth-child(3) > div.dd-div1").first().text();
carTest.setEditorName2(editorName2);
String editorRemark2 = carElement.select(".tabbox2.tabbox-score dd:nth-child(3) > div.dd-div3").first().text();
carTest.setEditorRemark2(editorRemark2);
String editorName3 = carElement.select(".tabbox2.tabbox-score dd:nth-child(4) > div.dd-div1").first().text();
carTest.setEditorName3(editorName3);
String editorRemark3 = carElement.select(".tabbox2.tabbox-score dd:nth-child(4) > div.dd-div3").first().text();
carTest.setEditorRemark3(editorRemark3);
Date currentDate = new Date();
carTest.setCreated(currentDate);
carTest.setUpdated(currentDate);
return carTest;
}
/**
* 把字符串去掉最后一个数,转为乘以1000的数字
* @param str
* @return
*/
private int covertStrToNum(String str) {
try {
if ("--".equals(str)) {
return 0;
}
// 字符串去掉随后一个数
str = StringUtils.substring(str, 0, str.length() - 1);
// 转换为小数并乘以1000
Number num = Float.valueOf(str) * 1000;
return num.intValue();
} catch (Exception e) {
e.printStackTrace();
System.out.println(str);
}
return 0;
}
}
整合任务
把测试方法中的爬取数据代码改造为任务,再使用Quartz定时任务定时处理,就可以实现定时抓取汽车评测数据,能够获取最新的数据了
改造任务
@Slf4j
@DisallowConcurrentExecution
public class CrawlerAutoHomeJob extends QuartzJobBean {
private AutoHomeApiService autoHomeApiService;
private TitleFilter titleFilter;
private CarTestService carTestService;
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>> start crawlerAutoHomeJob");
// 获取spring容器
ApplicationContext applicationContext = (ApplicationContext) context.getJobDetail().getJobDataMap()
.get("context");
// 获取抓取数据服务
this.autoHomeApiService = applicationContext.getBean(AutoHomeApiService.class);
// 获取汽车评测服务
this.carTestService = applicationContext.getBean(CarTestService.class);
// 获取过滤器
this.titleFilter = applicationContext.getBean(TitleFilter.class);
List<CarTest> saveList = new ArrayList<>();
for (int i = 1; i < 188; i++) {
String baseUrl = "https://www.autohome.com.cn/bestauto/" + i;
String html = autoHomeApiService.getHtml(baseUrl);
Document document = Jsoup.parse(html);
Elements carElements = document.getElementsByClass("uibox");
for (Element carElement : carElements) {
String carTitle = carElement.getElementsByClass("uibox-title uibox-title-border").text();
/* if (titleFilter.contains(carTitle)) {
continue;
}*/
CarTest carTest = marshalCarElement(carElement);
String imageNames = marshalImageNames(carElement);
carTest.setImage(imageNames);
saveList.add(carTest);
}
if (!CollectionUtils.isEmpty(saveList)) {
carTestService.saveBatch(saveList);
}
}
log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>> end crawlerAutoHomeJob");
}
/**
* 解析数据下载评测图片
* @param carElement
* @return
*/
private String marshalImageNames(Element carElement) {
String carImageName = null;
List<String> imageNameList = new ArrayList<>();
Elements imageElements = carElement.select(".piclist-box.fn-clear ul.piclist02 a");
for (Element imageElement : imageElements) {
String imageUrl = "https:" + imageElement.getElementsByTag("img").attr("src");
String imageName = autoHomeApiService.getImage(imageUrl);
imageNameList.add(imageName);
}
if (!CollectionUtils.isEmpty(imageNameList)) {
carImageName = StringUtils.join(imageNameList, ",");
}
return carImageName;
}
/**
* 解析数据封装成汽车评测对象
* @param carElement
* @return
*/
private CarTest marshalCarElement(Element carElement) {
CarTest carTest = new CarTest();
String carTitle = carElement.getElementsByClass("uibox-title uibox-title-border").text();
carTest.setTitle(carTitle);
String testSpeed = carElement.select(".tabbox1 dd:nth-child(2) > div.dd-div2").first().text();
carTest.setTestSpeed(covertStrToNum(testSpeed));
String testBrake = carElement.select(".tabbox1 dd:nth-child(3) > div.dd-div2").first().text();
carTest.setTestBrake(covertStrToNum(testBrake));
String testOil = carElement.select(".tabbox1 dd:nth-child(4) > div.dd-div2").first().text();
carTest.setTestOil(covertStrToNum(testOil));
String editorName1 = carElement.select(".tabbox2.tabbox-score dd:nth-child(2) > div.dd-div1").first().text();
carTest.setEditorName1(editorName1);
String editorRemark1 = carElement.select(".tabbox2.tabbox-score dd:nth-child(2) > div.dd-div3").first().text();
carTest.setEditorRemark1(editorRemark1);
String editorName2 = carElement.select(".tabbox2.tabbox-score dd:nth-child(3) > div.dd-div1").first().text();
carTest.setEditorName2(editorName2);
String editorRemark2 = carElement.select(".tabbox2.tabbox-score dd:nth-child(3) > div.dd-div3").first().text();
carTest.setEditorRemark2(editorRemark2);
String editorName3 = carElement.select(".tabbox2.tabbox-score dd:nth-child(4) > div.dd-div1").first().text();
carTest.setEditorName3(editorName3);
String editorRemark3 = carElement.select(".tabbox2.tabbox-score dd:nth-child(4) > div.dd-div3").first().text();
carTest.setEditorRemark3(editorRemark3);
Date currentDate = new Date();
carTest.setCreated(currentDate);
carTest.setUpdated(currentDate);
return carTest;
}
/**
* 把字符串去掉最后一个数,转为乘以1000的数字
* @param str
* @return
*/
private int covertStrToNum(String str) {
try {
if ("--".equals(str)) {
return 0;
}
// 字符串去掉随后一个数
str = StringUtils.substring(str, 0, str.length() - 1);
// 转换为小数并乘以1000
Number num = Float.valueOf(str) * 1000;
return num.intValue();
} catch (Exception e) {
e.printStackTrace();
System.out.println(str);
}
return 0;
}
}
增加定时任务
- 在定时任务配置
QuartzConfig
中添加爬取汽车之家的定时任务
/**
* 定义定时爬取评测任务任务
*/
@Bean("crawlerAutoHomeJob")
public JobDetailFactoryBean crawlerAutoHomeJob() {
JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean();
jobDetailFactoryBean.setApplicationContextJobDataKey("context");
jobDetailFactoryBean.setJobClass(CrawlerAutoHomeJob.class);
jobDetailFactoryBean.setDurability(true);
return jobDetailFactoryBean;
}
/**
* 定义关闭无效连接触发器
*/
@Bean("crawlerAutoHomeJobTrigger")
public CronTriggerFactoryBean crawlerAutoHomeJobTrigger(
@Qualifier(value = "crawlerAutoHomeJob") JobDetailFactoryBean itemJobBean) {
CronTriggerFactoryBean tigger = new CronTriggerFactoryBean();
tigger.setJobDetail(itemJobBean.getObject());
tigger.setCronExpression("0/5 * * * * ? ");
return tigger;
}
num = Float.valueOf(str) * 1000;
return num.intValue();
} catch (Exception e) {
e.printStackTrace();
System.out.println(str);
}
return 0;
}
}
增加定时任务
- 在定时任务配置
QuartzConfig
中添加爬取汽车之家的定时任务
/**
* 定义定时爬取评测任务任务
*/
@Bean("crawlerAutoHomeJob")
public JobDetailFactoryBean crawlerAutoHomeJob() {
JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean();
jobDetailFactoryBean.setApplicationContextJobDataKey("context");
jobDetailFactoryBean.setJobClass(CrawlerAutoHomeJob.class);
jobDetailFactoryBean.setDurability(true);
return jobDetailFactoryBean;
}
/**
* 定义关闭无效连接触发器
*/
@Bean("crawlerAutoHomeJobTrigger")
public CronTriggerFactoryBean crawlerAutoHomeJobTrigger(
@Qualifier(value = "crawlerAutoHomeJob") JobDetailFactoryBean itemJobBean) {
CronTriggerFactoryBean tigger = new CronTriggerFactoryBean();
tigger.setJobDetail(itemJobBean.getObject());
tigger.setCronExpression("0/5 * * * * ? ");
return tigger;
}
项目结构
更多推荐
所有评论(0)