前言

此文章只是为了学习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: devapplication:name: spider-autohome
  • 创建测试环境配置application-dev.yml
server:port: 8080tomcat:max-swallow-size: 100MB#配置数据源
spring:datasource:druid:type: com.alibaba.druid.pool.DruidDataSourcedriverClassName: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.56.120:3306/spider-autohome?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=falseusername: rootpassword: 123456# 初始化连接大小initial-size: 5# 最小空闲连接数min-idle: 5max-active: 30max-wait: 60000# 可关闭的空闲连接间隔时间time-between-eviction-runs-millis: 60000# 配置连接在池中的最小生存时间min-evictable-idle-time-millis: 300000validation-query: select '1' from dualtest-while-idle: truetest-on-borrow: falsetest-on-return: false# 打开PSCache,并且指定每个连接上PSCache的大小pool-prepared-statements: truemax-open-prepared-statements: 50max-pool-prepared-statement-per-connection-size: 20# 配置监控统计拦截的filtersfilters: statstat-view-servlet:url-pattern: /druid/*reset-enable: falselogin-username: adminlogin-password: 123456web-stat-filter:url-pattern: /*exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"servlet:multipart:max-file-size: 50MBmax-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 {@Autowiredprivate TestDao testDao;@Overridepublic String queryNowDate() {return testDao.queryNowDate();}
}

编写请求CONTROLLER

  • 创建controller文件夹,创建TestController文件
@RestController
@RequestMapping("/test")
public class TestController {@Autowiredprivate 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 {@Overridepublic 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 {@Beanpublic PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() {// 创建连接管理器PoolingHttpClientConnectionManager httpClientConnectionManager = new PoolingHttpClientConnectionManager();// 设置最大连接数httpClientConnectionManager.setMaxTotal(50);// 设置每个并发连接数httpClientConnectionManager.setDefaultMaxPerRoute(20);return httpClientConnectionManager;}
}

定时关闭失效连接

这里使用Quartz定时任务来处理定时关闭失效连接

  • 新建job文件夹,创建CloseHttpConnectionJob文件,编写定时任务
@Slf4j
@DisallowConcurrentExecution
public class CloseHttpConnectionJob extends QuartzJobBean {@Overrideprotected 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;}/*** 定义调度器*/@Beanpublic 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 {@Autowiredprivate PoolingHttpClientConnectionManager connectionManager;@Overridepublic 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;}@Overridepublic 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 {@Autowiredprivate AutoHomeApiService autoHomeApiService;@Autowiredprivate TitleFilter titleFilter;@Autowiredprivate CarTestService carTestService;/*** 测试获取HTML内容*/@Testpublic void getHtml() {String html = autoHomeApiService.getHtml("https://www.autohome.com.cn/bestauto/");System.out.println("html = " + html);}/*** 测试获取图片*/@Testpublic 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增加分页查询方法实现

    @Overridepublic 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 {@Autowiredprivate CarTestService carTestService;@Beanpublic 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 {@Autowiredprivate AutoHomeApiService autoHomeApiService;@Autowiredprivate TitleFilter titleFilter;@Autowiredprivate CarTestService carTestService;/*** 测试获取HTML内容*/@Testpublic void getHtml() {String html = autoHomeApiService.getHtml("https://www.autohome.com.cn/bestauto/");System.out.println("html = " + html);}/*** 测试获取图片*/@Testpublic 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);}/*** 获取评测数据*/@Testpublic 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);// 转换为小数并乘以1000Number 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;@Overrideprotected 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);// 转换为小数并乘以1000Number 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;}

项目结构

SpringBoot爬虫相关推荐

  1. Springboot+爬虫+推荐算法+前后端分离实现小说推荐系统

    如何针对互联网各大小说阅读网站的小说数据进行实时采集更新,建立自己的小说资源库,针对海量的小说数据开展标签处理特征分析,利用推荐算法完成针对用户的个性化阅读推荐? 基于以上问题,本次小说推荐系统,建设 ...

  2. 公众号开发之wx-tools+springboot应用实战-音乐爬虫推送[JAVA]

    天啦噜!微信公众号开发如此简单!竟然是因为-- 当然是因为wx-tools啦! springboot+wx-tools实践!音乐爬虫推送公众号DEMO GitHub地址:wx-tools 最终DEMO ...

  3. 基于微信小程序+springboot的在线商城系统毕业设计源码

    目录 一.可按需求定制:是 二.资源介绍: 三.文档目录: 四.项目截图: 五.数据库表截图: 六.代码展示: 七.更多项目: 八.资源获取:添加作者微信 一.可按需求定制:是 二.资源介绍: 项目学 ...

  4. 【java毕业设计源码】基于SSM的疫情社区物资配送系统

    目录 一.程序介绍: 三.文档目录: 四.运行截图: 五.数据库表: 六.代码展示: 七.更多学习目录: 八.互动留言 一.程序介绍: 文档:开发技术文档.参考LW.答辩PPT,部分项目另有其他文档 ...

  5. 计算机毕业设计选题、开题、答辩、模板大全(有源码)

    毕业设计选题五问法: 第一问.自己会什么(一定是自己有一定了解的,那样自己有思路): 第二问,这个是什么: 第三问,为什么要去做: 第四问,该怎么去做: 第五问.创新点是什么. 在做毕业设计的时候,遇 ...

  6. Java+SSM二手交易商城微信小程序源码【包调试运行】

    目录 一.程序介绍: 三.文档目录: 四.运行截图: 五.数据库表: 六.代码展示: 七.更多学习目录: 八.互动留言 一.程序介绍: 文档:开发技术文档.参考LW.答辩PPT,部分项目另有其他文档 ...

  7. 基于nodejs的二手物物交换平台【毕业设计源码】

    目录 一.程序介绍: 三.文档目录: 四.运行截图: 五.数据库表: 六.代码展示: 七.更多学习目录: 八.互动留言 一.程序介绍: 文档:开发技术文档.参考LW.答辩PPT,部分项目另有其他文档 ...

  8. 软件工程Alpha冲刺day2

    这个作业属于哪个课程 构建之法-2021秋-福州大学软件工程 这个作业要求在哪里 2021秋软工实践alpha冲刺 团队名称 测码奔腾 这个作业的目标 Alpha冲刺(day2) 今日进度 成员姓名 ...

  9. 开源免费的舆情系统的架构

    思通舆情 的功能: 舆情监测:通过全文搜索.来源搜索.热搜监测等多重功能实现对全网文本.图片.视频舆情实时发现; 舆情预警:根据用户设置预警条件,判别舆情信息,并第一时间通过多渠道告知用户; 舆情分析 ...

  10. StoneDT开源舆情系统大数据技术栈介绍

    我们目前开源的 舆情系统 分为3个部分,整个系统使用了多种开源技术组件和开源框架,涵盖涉及技术领域广泛,例如:分布式计算.大数据.人工智能.数据中台.数据挖掘.深度学习.java和python的大量实 ...

最新文章

  1. 真正实现与人更类似的智能!Jeff Hawkins:创造机器智能的路线图
  2. 软件素材---linux C语言:拼接字符串函数 strcat的用例(与char数组联合使用挺好)...
  3. Oracle中视图的创建和处理方法
  4. if delete annotation.xml in project folder, not local service folder
  5. 微信小程序——账号及开发工具
  6. hadoop集群swap_hadoop集群调优-OS和文件系统部分
  7. 【杂项】2021年年度报告
  8. CCPC-Wannafly Comet OJ 夏季欢乐赛(2019)E
  9. 项目管理团队建设成功经验
  10. 黑群晖的网络录像机启用并直通互联网的几个关键点
  11. H264视频解码器C++工程说明
  12. python画一个正方形和圆_正方形最新:Python 用turtle实现用正方形画圆的例子_爱安网 LoveAn.com...
  13. python 正则表达式 ,看这篇就够了
  14. 从头开始搞懂 MySQL(07)为什么同一条 SQL 时快时慢
  15. MSC Marc英文界面汉化
  16. chrome浏览器不支持video标签,不能自动播放。
  17. 个人站长可以考虑的几种赚钱模式
  18. YJAutoLayout
  19. ADG - Active
  20. 解决Linux虚拟机无法打开,报无法打开虚拟机“Kylin” (D:\javaToil\linux\Kylin\Kylin.vmx): 虚拟机似乎正在使用中

热门文章

  1. 智能管家项目总结(1)
  2. nova青春版支持鸿蒙吗,华为nova青春版评测:高颜值年轻人必备
  3. Keepalived HAProxy应用
  4. VBA-EXCEL:控制WORD文件
  5. ffmpeg给视频加水印
  6. 鲲鹏芯片的服务器,鲲鹏处理器及服务器介绍.pdf
  7. 对Parcel的使用学习
  8. ShapeShift、DAOs和工作的未来
  9. 2022年电工(技师)考试题及电工(技师)模拟试题
  10. Android按钮按下的时候改变颜色/图片