我的多线程爬虫项目实战

2022-05-05 17:36:03 浏览数 (1)

爬虫相信很多小伙伴都做过,大部分都是用的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格式的

代码语言:javascript复制
 document = jsoupService.parseDocument(detailUrl);

爬取测试

爬取的部分数据如下

线程池

爬取数据是一条一条的爬取,如果是单线程爬,速度肯定很慢,这里使用多线程。我们使用SpringBoot的方式创建线程池。

“注意:因为是多线程成爬取,如果爬取的数据需要存入集合,需要采用并发安全的List。比如:CopyOnWriterArrayList,否则在list.add()的时候很有可能出现并发操作异常。 ”

代码语言:javascript复制
@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队列

因为在爬取的时候可能因为网络等原因,爬取的那一条数据会失败。这时我会记录失败的urlcode,并将爬取异常的urlcode存入Redis队列。

代码语言:javascript复制
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 * * ?

入库结果

入库

爬虫流程图

我的设计思路大致如下:

爬虫项目

当然我的爬虫项目还在逐渐完善中,期待完工的时候是个什么样的

0 人点赞