SpringBoot爬虫
前言
此文章只是为了学习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爬虫相关推荐
- Springboot+爬虫+推荐算法+前后端分离实现小说推荐系统
如何针对互联网各大小说阅读网站的小说数据进行实时采集更新,建立自己的小说资源库,针对海量的小说数据开展标签处理特征分析,利用推荐算法完成针对用户的个性化阅读推荐? 基于以上问题,本次小说推荐系统,建设 ...
- 公众号开发之wx-tools+springboot应用实战-音乐爬虫推送[JAVA]
天啦噜!微信公众号开发如此简单!竟然是因为-- 当然是因为wx-tools啦! springboot+wx-tools实践!音乐爬虫推送公众号DEMO GitHub地址:wx-tools 最终DEMO ...
- 基于微信小程序+springboot的在线商城系统毕业设计源码
目录 一.可按需求定制:是 二.资源介绍: 三.文档目录: 四.项目截图: 五.数据库表截图: 六.代码展示: 七.更多项目: 八.资源获取:添加作者微信 一.可按需求定制:是 二.资源介绍: 项目学 ...
- 【java毕业设计源码】基于SSM的疫情社区物资配送系统
目录 一.程序介绍: 三.文档目录: 四.运行截图: 五.数据库表: 六.代码展示: 七.更多学习目录: 八.互动留言 一.程序介绍: 文档:开发技术文档.参考LW.答辩PPT,部分项目另有其他文档 ...
- 计算机毕业设计选题、开题、答辩、模板大全(有源码)
毕业设计选题五问法: 第一问.自己会什么(一定是自己有一定了解的,那样自己有思路): 第二问,这个是什么: 第三问,为什么要去做: 第四问,该怎么去做: 第五问.创新点是什么. 在做毕业设计的时候,遇 ...
- Java+SSM二手交易商城微信小程序源码【包调试运行】
目录 一.程序介绍: 三.文档目录: 四.运行截图: 五.数据库表: 六.代码展示: 七.更多学习目录: 八.互动留言 一.程序介绍: 文档:开发技术文档.参考LW.答辩PPT,部分项目另有其他文档 ...
- 基于nodejs的二手物物交换平台【毕业设计源码】
目录 一.程序介绍: 三.文档目录: 四.运行截图: 五.数据库表: 六.代码展示: 七.更多学习目录: 八.互动留言 一.程序介绍: 文档:开发技术文档.参考LW.答辩PPT,部分项目另有其他文档 ...
- 软件工程Alpha冲刺day2
这个作业属于哪个课程 构建之法-2021秋-福州大学软件工程 这个作业要求在哪里 2021秋软工实践alpha冲刺 团队名称 测码奔腾 这个作业的目标 Alpha冲刺(day2) 今日进度 成员姓名 ...
- 开源免费的舆情系统的架构
思通舆情 的功能: 舆情监测:通过全文搜索.来源搜索.热搜监测等多重功能实现对全网文本.图片.视频舆情实时发现; 舆情预警:根据用户设置预警条件,判别舆情信息,并第一时间通过多渠道告知用户; 舆情分析 ...
- StoneDT开源舆情系统大数据技术栈介绍
我们目前开源的 舆情系统 分为3个部分,整个系统使用了多种开源技术组件和开源框架,涵盖涉及技术领域广泛,例如:分布式计算.大数据.人工智能.数据中台.数据挖掘.深度学习.java和python的大量实 ...
最新文章
- 真正实现与人更类似的智能!Jeff Hawkins:创造机器智能的路线图
- 软件素材---linux C语言:拼接字符串函数 strcat的用例(与char数组联合使用挺好)...
- Oracle中视图的创建和处理方法
- if delete annotation.xml in project folder, not local service folder
- 微信小程序——账号及开发工具
- hadoop集群swap_hadoop集群调优-OS和文件系统部分
- 【杂项】2021年年度报告
- CCPC-Wannafly Comet OJ 夏季欢乐赛(2019)E
- 项目管理团队建设成功经验
- 黑群晖的网络录像机启用并直通互联网的几个关键点
- H264视频解码器C++工程说明
- python画一个正方形和圆_正方形最新:Python 用turtle实现用正方形画圆的例子_爱安网 LoveAn.com...
- python 正则表达式 ,看这篇就够了
- 从头开始搞懂 MySQL(07)为什么同一条 SQL 时快时慢
- MSC Marc英文界面汉化
- chrome浏览器不支持video标签,不能自动播放。
- 个人站长可以考虑的几种赚钱模式
- YJAutoLayout
- ADG - Active
- 解决Linux虚拟机无法打开,报无法打开虚拟机“Kylin” (D:\javaToil\linux\Kylin\Kylin.vmx): 虚拟机似乎正在使用中