爬虫相信很多小伙伴都做过,大部分都是用的Python。我之前也用Python爬取过12306的数据,有兴趣的可以看看我的这篇文章:
“我在github上面的一个项目———用Python爬取12306火车票 ”
但是这次我想用Java试试如何爬取网站数据。
使用框架
Jsoup:jsoup
是一款Java
的HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据。
官方文档地址如下:
“https://www.open-open.com/jsoup/ ”
下面我来介绍爬取过程。
导入Maven
代码语言:javascript复制<dependency>
<!-- jsoup HTML parser library @ https://jsoup.org/ -->
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.13.1</version>
</dependency>
返回结果是Json格式的,可以使用
代码语言:javascript复制jsonBody = jsoupService.parseBodyJson(url);
解析jsonBody
,获取指定参数值
如果是Document
格式的
document = jsoupService.parseDocument(detailUrl);
爬取测试
爬取的部分数据如下
线程池
爬取数据是一条一条的爬取,如果是单线程爬,速度肯定很慢,这里使用多线程。我们使用SpringBoot的方式创建线程池。
代码语言:javascript复制“注意:因为是多线程成爬取,如果爬取的数据需要存入集合,需要采用并发安全的List。比如:
CopyOnWriterArrayList
,否则在list.add()
的时候很有可能出现并发操作异常。 ”
@Component
@EnableAsync
public class TreadPoolConfigurer implements AsyncConfigurer {
@Override
@Bean(name = "taskExecutor")
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心线程池数量,方法: 返回可用处理器的Java虚拟机的数量。
executor.setCorePoolSize(5);
//最大线程数量
executor.setMaxPoolSize(10);
//线程池的队列容量
executor.setQueueCapacity(20);
// 设置线程活跃时间(秒)
executor.setKeepAliveSeconds(60);
//线程名称的前缀
executor.setThreadNamePrefix("这里是线程名");
// setRejectedExecutionHandler:当pool已经达到max size的时候,如何处理新任务
// CallerRunsPolicy:不在新线程中执行任务,而是由调用者所在的线程来执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}
}
如上代码,在设置最大线程数量的时候,数量最好不要超过当前系统的CPU核数。
采用异步注解创建任务,并指定线程池。
代码语言:javascript复制@Async("taskExecutor")
public void startNewThreadTaskProductInfoMachineryByUrl(List<ProductInfo> infoList, String url) {
handleProductInfoMachinery(infoList, url, null);
}
Redis队列
因为在爬取的时候可能因为网络等原因,爬取的那一条数据会失败。这时我会记录失败的url
或code
,并将爬取异常的url
或code
存入Redis队列。
log.error("异常信息:", e.getMessage());
log.info("将异常的url存入阻塞队列...");
//存入阻塞队列
redisUtils.leftPush(MACHINERY_DETAIL_URL, code);
我在后台重新启动一个线程,自旋的形式将Redis的队列中的数据阻塞式取出。然后再一次爬取。
代码语言:javascript复制String code = (String)redisUtils.rightPop(MACHINERY_DETAIL_URL);
因为爬取的数据存到List中,需要持久化到数据库。考虑到数据量比较大,采用分组的方式存入数据库。
代码语言:javascript复制 List<List<ProductInfo>> groups = Lists.partition(productInfos, 500);
groups.forEach(infos -> this.saveList(infos));
ApplicationRunner
系统启动时,开启定时任务,定时爬取。并开启后台重试线程。
代码语言:javascript复制@Component
@Slf4j
public class ProductInfoCrawlInitService implements ApplicationRunner {
@Autowired
private ProductInfoRetryCrawlMachineryTask productInfoRetryCrawlMachineryTask;
@Override
public void run(ApplicationArguments args) {
log.info("系统已加载定时任务....");
//启动定时任务
CronUtil.start();
new Thread(productInfoRetryCrawlMachineryTask).start();
}
}
定时任务
定时任务配置,采用Hutool框架,创建定时任务的文件
代码语言:javascript复制srcmainresourcesconfigcron.setting
代码语言:javascript复制# 配置定时任务
[com.xxxxxx.domain.service.impl]
#每天晚上0点执行
ProductInfoServiceImpl.saveProductInfoMachinery = 0 0 0 * * ?
入库结果
入库
爬虫流程图
我的设计思路大致如下:
爬虫项目
当然我的爬虫项目还在逐渐完善中,期待完工的时候是个什么样的