java抓取网页内容(接上简易爬虫配置池线程池(ThreadPoolTaskExecutor)抓取任务的方法)
优采云 发布时间: 2021-12-25 16:02java抓取网页内容(接上简易爬虫配置池线程池(ThreadPoolTaskExecutor)抓取任务的方法)
继续上一节(爬虫系列(0):项目建设)
网络爬虫是通过多线程和多任务逻辑实现的。线程池(ThreadPoolTaskExecutor)已经封装在springboot框架中,我们只需要使用它即可。
在本节中,我们主要实现网页连接信息的多线程捕获,并将信息存储在队列中。
介绍一个新的包
在pom中引入新的包,如下:
org.apache.commons
commons-lang3
org.jsoup
jsoup
1.8.3
org.projectlombok
lombok
provided
为了简化编码,这里引入lombok。IDE使用时需要安装lombok插件,否则会提示编译错误。
配置管理
springboot的配置文件统一管理在application.properties(.yml)中。这里我们也通过@ConfigurationProperties注解实现爬虫相关的配置。直接上传代码:
package mobi.huanyuan.spider.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 爬虫配置.
*
* @author Jonathan L.(xingbing.lai@gmail.com)
* @version 1.0.0 -- Datetime: 2020/2/18 11:10
*/
@Data
@ConfigurationProperties(prefix = "huanyuan.spider")
public class SpiderConfig {
/**
* 爬取页面最大深度
*/
public int maxDepth = 2;
/**
* 下载页面线程数
*/
public int minerHtmlThreadNum = 2;
//=================================================
// 线程池配置
//=================================================
/**
* 核心线程池大小
*/
private int corePoolSize = 4;
/**
* 最大可创建的线程数
*/
private int maxPoolSize = 100;
/**
* 队列最大长度
*/
private int queueCapacity = 1000;
/**
* 线程池维护线程所允许的空闲时间
*/
private int keepAliveSeconds = 300;
}
然后,需要修改这些配置,只需要修改里面的application.properties(.yml):
魔猿简单爬虫配置
线程池
线程池使用现有的springboot,配置也可以在上面的配置管理中。这里只需要初始化配置:
package mobi.huanyuan.spider.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 线程池配置.
*
* @author Jonathan L.(xingbing.lai@gmail.com)
* @version 1.0.0 -- Datetime: 2020/2/18 11:35
*/
@Configuration
public class ThreadPoolConfig {
@Autowired
private SpiderConfig spiderConfig;
@Bean(name = "threadPoolTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(spiderConfig.getMaxPoolSize());
executor.setCorePoolSize(spiderConfig.getCorePoolSize());
executor.setQueueCapacity(spiderConfig.getQueueCapacity());
executor.setKeepAliveSeconds(spiderConfig.getKeepAliveSeconds());
// 线程池对拒绝任务(无线程可用)的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}
队列管理
本节我们主要是抓取URL并保存到队列中,所以涉及到的队列就是要抓取的队列和要分析的队列(下节分析用到,这里只做存储),另外,为了防止重复抓取同一个URL,这里还需要添加一个Set集合来记录访问过的地址。
package mobi.huanyuan.spider;
import lombok.Getter;
import mobi.huanyuan.spider.bean.SpiderHtml;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Set;
/**
* 爬虫访问队列.
*
* @author Jonathan L.(xingbing.lai@gmail.com)
* @version 1.0.0 -- Datetime: 2020/2/18 10:54
*/
public class SpiderQueue {
private static Logger logger = LoggerFactory.getLogger(SpiderQueue.class);
/**
* Set集合 保证每一个URL只访问一次
*/
private static volatile Set urlSet = new HashSet();
/**
* 待访问队列
* 爬取页面线程从这里取数据
*/
private static volatile Queue unVisited = new LinkedList();
/**
* 等待提取URL的分析页面队列
* 解析页面线程从这里取数据
*/
private static volatile Queue waitingMine = new LinkedList();
/**
* 添加到URL队列
*
* @param url
*/
public synchronized static void addUrlSet(String url) {
urlSet.add(url);
}
/**
* 获得URL队列大小
*
* @return
*/
public static int getUrlSetSize() {
return urlSet.size();
}
/**
* 添加到待访问队列,每个URL只访问一次
*
* @param spiderHtml
*/
public synchronized static void addUnVisited(SpiderHtml spiderHtml) {
if (null != spiderHtml && !urlSet.contains(spiderHtml.getUrl())) {
logger.info("添加到待访问队列[{}] 当前第[{}]层 当前线程[{}]", spiderHtml.getUrl(), spiderHtml.getDepth(), Thread.currentThread().getName());
unVisited.add(spiderHtml);
}
}
/**
* 待访问出队列
*
* @return
*/
public synchronized static SpiderHtml unVisitedPoll() {
return unVisited.poll();
}
/**
* 添加到等待提取URL的分析页面队列
*
* @param html
*/
public synchronized static void addWaitingMine(SpiderHtml html) {
waitingMine.add(html);
}
/**
* 等待提取URL的分析页面出队列
*
* @return
*/
public synchronized static SpiderHtml waitingMinePoll() {
return waitingMine.poll();
}
/**
* 等待提取URL的分析页面队列大小
* @return
*/
public static int waitingMineSize() {
return waitingMine.size();
}
}
抓取任务
直接上传代码:
package mobi.huanyuan.spider.runable;
import mobi.huanyuan.spider.SpiderQueue;
import mobi.huanyuan.spider.bean.SpiderHtml;
import mobi.huanyuan.spider.config.SpiderConfig;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 抓取页面任务.
*
* @author Jonathan L.(xingbing.lai@gmail.com)
* @version 1.0.0 -- Datetime: 2020/2/18 11:43
*/
public class SpiderHtmlRunnable implements Runnable {
private static final Logger logger = LoggerFactory.getLogger(SpiderHtmlRunnable.class);
private static boolean done = false;
private SpiderConfig config;
public SpiderHtmlRunnable(SpiderConfig config) {
this.config = config;
}
@Override
public void run() {
while (!SpiderHtmlRunnable.done) {
done = true;
minerHtml();
done = false;
}
}
public synchronized void minerHtml() {
SpiderHtml minerUrl = SpiderQueue.unVisitedPoll(); // 待访问出队列。
try {
//判断当前页面爬取深度
if (null == minerUrl || StringUtils.isBlank(minerUrl.getUrl()) || minerUrl.getDepth() > config.getMaxDepth()) {
return;
}
//判断爬取页面URL是否包含http
if (!minerUrl.getUrl().startsWith("http")) {
logger.info("当前爬取URL[{}]没有http", minerUrl.getUrl());
return;
}
logger.info("当前爬取页面[{}]爬取深度[{}] 当前线程 [{}]", minerUrl.getUrl(), minerUrl.getDepth(), Thread.currentThread().getName());
Connection conn = Jsoup.connect(minerUrl.getUrl());
conn.header("User-Agent", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.27 Safari/525.13");//配置模拟浏览器
Document doc = conn.get();
String page = doc.html();
SpiderHtml spiderHtml = new SpiderHtml();
spiderHtml.setUrl(minerUrl.getUrl());
spiderHtml.setHtml(page);
spiderHtml.setDepth(minerUrl.getDepth());
System.out.println(spiderHtml.getUrl());
// TODO: 添加到继续爬取队列
SpiderQueue.addWaitingMine(spiderHtml);
} catch (Exception e) {
logger.info("爬取页面失败 URL [{}]", minerUrl.getUrl());
logger.info("Error info [{}]", e.getMessage());
}
}
}
这是一个 Runnable 任务。主要目的是拉取URL数据,然后将其封装成SpiderHtml对象,存放在待分析队列中。这里使用的是Jsoup——一个用于HTML分析操作的Java工具包。如果您不确定,您可以搜索它。后面的章节也会用到分析部分。
其他页面信息包SpiderHtml
package mobi.huanyuan.spider.bean;
import lombok.Data;
import java.io.Serializable;
/**
* 页面信息类.
*
* @author Jonathan L.(xingbing.lai@gmail.com)
* @version 1.0.0 -- Datetime: 2020/2/18 11:02
*/
@Data
public class SpiderHtml implements Serializable {
/**
* 页面URL
*/
private String url;
/**
* 页面信息
*/
private String html;
/**
* 爬取深度
*/
private int depth;
}
爬行动物主类
package mobi.huanyuan.spider;
import mobi.huanyuan.spider.bean.SpiderHtml;
import mobi.huanyuan.spider.config.SpiderConfig;
import mobi.huanyuan.spider.runable.SpiderHtmlRunnable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 爬虫.
*
* @author Jonathan L.(xingbing.lai@gmail.com)
* @version 1.0.0 -- Datetime: 2020/2/18 11:23
*/
@Component
public class Spider {
private static Logger logger = LoggerFactory.getLogger(Spider.class);
@Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Autowired
private SpiderConfig spiderConfig;
public void start(SpiderHtml spiderHtml) {
//程序启动,将第一个起始页面放入待访问队列。
SpiderQueue.addUnVisited(spiderHtml);
//将URL 添加到URL队列 保证每个URL只访问一次
SpiderQueue.addUrlSet(spiderHtml.getUrl());
//download
for (int i = 0; i < spiderConfig.getMinerHtmlThreadNum(); i++) {
SpiderHtmlRunnable minerHtml = new SpiderHtmlRunnable(spiderConfig);
threadPoolTaskExecutor.execute(minerHtml);
}
// TODO: 监控爬取完毕之后停线程池,关闭程序
try {
TimeUnit.SECONDS.sleep(20);
logger.info("待分析URL队列大小: {}", SpiderQueue.waitingMineSize());
// 关闭线程池
threadPoolTaskExecutor.shutdown();
} catch (Exception e) {
e.printStackTrace();
}
}
}
"// TODO:" 后面的代码逻辑在这里是暂时的。后续章节完成后,会慢慢移除。
最后
运行本节代码,需要在springboot项目的main方法中添加如下代码:
ConfigurableApplicationContext context = SpringApplication.run(SpiderApplication.class, args);
Spider spider = context.getBean(Spider.class);
SpiderHtml startPage = new SpiderHtml();
startPage.setUrl("$URL");
startPage.setDepth(2);
spider.start(startPage);
$URL 是需要抓取的网页地址。
springboot项目启动后,需要手动停止。目前,没有在处理 fetch 后自动停止运行的逻辑。运行结果如下:
Magic Ape Simple Crawler 运行结果
最后,本章完成后,整个项目的结构如下:
魔猿简单爬虫项目结构
关于我
编程界的老猿猴,自媒体界的新宠じ☆ve
编程界的老猿猴,自媒体界的新宠じ☆ve