WebMagic之爬虫监控
前言
年前闲着无聊,研究了一阵子爬虫技术,接触到爬虫框架 WebMagic,感觉很好用。
在之后的工作中,接手了新站与第三方接口对接的工作,主要的工作是去抓取对方接口的内容;初始的时候,之前负责该工作的同事,是手动使用多线程去抓取,在应用的过程当中暴露了不少问题。比如对于接口内容超级多的时候,虽然使用了多线程,但是抓取的效率很低,而且也没有实现增量抓取,每次都需要去全量抓取,跑一次基本需要好几天-.-;小说是连载的情况下,这种问题是亟需解决的。
趁着熟悉了新兵器 WebMagic, 果断在项目中进行引入,解决以上问题。功能上线后,替换了原有的多线程抓取,目前已经十分稳定, 基本上配置好任务之后,就无需再人工干预了。
以下,正文是基于我学习 WebMagic 时练手项目,功能和在公司开发的差不多,只不过我本地开发的是去抓取盗版网站的内容。
项目预览
-
菜单管理
-
爬虫任务管理
-
实现了爬虫的状态监控,以及可视化启停
初入手兵器-基本使用
-
爬虫套路分析
先看官方文档的总体架构图
大部分模块WebMagic已经提供了默认实现。
一般来说,对于编写一个爬虫,PageProcessor是需要编写的部分,而Spider则是创建和控制爬虫的入口。得益于 WebMagic 框架的良好封装,对于框架的使用者来说,所需要编写的代码几乎只有爬虫的逻辑代码,而对于怎么爬,维护任务队列的事情,WebMagic 都可以替我们做好。开始我们的爬虫之旅吧!
-
引入依赖
本文中所使用到的项目是基于 Maven 的 SSM 项目,在 pom.xml 中引入 WebMagic 的依赖。<dependency> <groupId>us.codecraft</groupId> <artifactId>webmagic-core</artifactId> <version>0.7.3</version> </dependency> <dependency> <groupId>us.codecraft</groupId> <artifactId>webmagic-extension</artifactId> <version>0.7.3</version> </dependency>
-
基本类图
先将对应的处理类进行抽象出来,方便统一处理。
-
每个爬虫都有其对应的配置信息
Site 是 抓取网站的相关配置,包括编码、抓取间隔、重试次数 -
对应的实现类重写 process 方法,在方法中实现对应的爬虫逻辑处理
-
启动爬虫
-
爬虫的使用就简单带过,具体可以将本文与官方文档结合使用,官方文档的示例只是基于 main 方法。
爬虫监控
-
扩展源码
为了实现项目预览的效果,实现爬虫的状态监控,需要对爬虫进行扩展。因为官网提供的方式功能不足以达到在页面展示的效果。添加监控非常简单,获取一个 SpiderMonitor 的单例 SpiderMonitor.instance(),并将你想要监控的 Spider 注册进去即可。你可以注册多个 Spider 到 SpiderMonitor 中。
查看 SpiderMonitor 源代码后,如果调用的是 获取一个 SpiderMonitor 的单例 SpiderMonitor 的 注册方法,发现 WebMagic 将每只爬虫的状态对象 SpiderStatusMXBean 全部添加到一个 List 集合当中去,这样就难以区分具体是哪一只爬虫的状态,所以我们需要对 SpiderMonitor 进行扩展。
将 SpiderMonitor 中的
private List<SpiderStatusMXBean> spiderStatuses = new ArrayList<SpiderStatusMXBean>();
修改为 Map 集合,key 选择 Spider 的 UUID 作为唯一区分爬虫的标记。@Experimental public class MySpiderMonitor { private static MySpiderMonitor INSTANCE = new MySpiderMonitor(); private AtomicBoolean started = new AtomicBoolean(false); private Logger logger = LoggerFactory.getLogger(getClass()); private MBeanServer mbeanServer; private String jmxServerName; private Map<String,MySpiderStatus> spiderStatuses = new HashMap<String,MySpiderStatus>(); protected MySpiderMonitor() { jmxServerName = "WebMagic"; mbeanServer = ManagementFactory.getPlatformMBeanServer(); } public Map<String,MySpiderStatus> getSpiderStatuses() { return spiderStatuses; } /** * Register spider for monitor. * * @param spiders spiders * @return this */ public synchronized MySpiderMonitor register(Spider... spiders) throws JMException { for (Spider spider : spiders) { MyMonitorSpiderListener monitorSpiderListener = new MyMonitorSpiderListener(); if (spider.getSpiderListeners() == null) { List<SpiderListener> spiderListeners = new ArrayList<SpiderListener>(); spiderListeners.add(monitorSpiderListener); spider.setSpiderListeners(spiderListeners); } else { spider.getSpiderListeners().add(monitorSpiderListener); } MySpiderStatus spiderStatusMBean = getSpiderStatusMBean(spider, monitorSpiderListener); registerMBean(spiderStatusMBean); spiderStatuses.put(spider.getUUID(),spiderStatusMBean); } return this; } protected MySpiderStatus getSpiderStatusMBean(Spider spider, MyMonitorSpiderListener monitorSpiderListener) { return new MySpiderStatus(spider, monitorSpiderListener); } public static MySpiderMonitor instance() { return INSTANCE; } public class MyMonitorSpiderListener implements SpiderListener { private final AtomicInteger successCount = new AtomicInteger(0); private final AtomicInteger errorCount = new AtomicInteger(0); private List<String> errorUrls = Collections.synchronizedList(new ArrayList<String>()); @Override public void onSuccess(Request request) { successCount.incrementAndGet(); } @Override public void onError(Request request) { errorUrls.add(request.getUrl()); errorCount.incrementAndGet(); } public AtomicInteger getSuccessCount() { return successCount; } public AtomicInteger getErrorCount() { return errorCount; } public List<String> getErrorUrls() { return errorUrls; } } protected void registerMBean(SpiderStatusMXBean spiderStatus) throws MalformedObjectNameException, InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException { ObjectName objName = new ObjectName(jmxServerName + ":name=" + spiderStatus.getName()); if(mbeanServer.isRegistered(objName)==false) { mbeanServer.registerMBean(spiderStatus, objName); } } }
需要注意的是,SpiderMonitor 中使用的 SpiderStatus 也需要进行一同扩展。
public class MySpiderStatus implements SpiderStatusMXBean { protected final Spider spider; protected Logger logger = LoggerFactory.getLogger(getClass()); protected final MySpiderMonitor.MyMonitorSpiderListener monitorSpiderListener; public MySpiderStatus(Spider spider, MySpiderMonitor.MyMonitorSpiderListener monitorSpiderListener) { this.spider = spider; this.monitorSpiderListener = monitorSpiderListener; } public Spider getSpider() { return this.spider; } public String getName() { return spider.getUUID(); } public int getLeftPageCount() { if (spider.getScheduler() instanceof MonitorableScheduler) { return ((MonitorableScheduler) spider.getScheduler()).getLeftRequestsCount(spider); } logger.warn("Get leftPageCount fail, try to use a Scheduler implement MonitorableScheduler for monitor count!"); return -1; } public int getTotalPageCount() { if (spider.getScheduler() instanceof MonitorableScheduler) { return ((MonitorableScheduler) spider.getScheduler()).getTotalRequestsCount(spider); } logger.warn("Get totalPageCount fail, try to use a Scheduler implement MonitorableScheduler for monitor count!"); return -1; } @Override public int getSuccessPageCount() { return monitorSpiderListener.getSuccessCount().get(); } @Override public int getErrorPageCount() { return monitorSpiderListener.getErrorCount().get(); } public List<String> getErrorPages() { return monitorSpiderListener.getErrorUrls(); } @Override public String getStatus() { return spider.getStatus().name(); } @Override public int getThread() { return spider.getThreadAlive(); } public void start() { spider.start(); } public void stop() { spider.stop(); } @Override public Date getStartTime() { return spider.getStartTime(); } @Override public int getPagePerSecond() { int runSeconds = (int) (System.currentTimeMillis() - getStartTime().getTime()) / 1000; return getSuccessPageCount() / runSeconds; } }
-
重写爬虫启动处代码
@Service public class WebMagicService { @Resource private ApplicationContext context; @Resource private TaskService taskService; public void run(TaskDTO taskDTO, boolean runAsync) throws Exception { MySpiderMonitor spiderMonitor = MySpiderMonitor.instance(); String ruleJson = taskDTO.getTaskRuleJson(); WebMagicConfig config = JSONObject.parseObject(ruleJson, WebMagicConfig.class); SpiderConfig spiderConfig = config.getSpider(); AbstractPageProcess pageProcess = context.getBean(spiderConfig.getProcesser(), AbstractPageProcess.class); pageProcess.init(config); pageProcess.setUuid(taskDTO.getSpiderUUID()); Spider spider = Spider.create(pageProcess).thread(spiderConfig.getThread()); spider.setUUID(taskDTO.getSpiderUUID()); List<String> pipelines = spiderConfig.getPipeline(); for (String pipeline : pipelines) { Pipeline bean = context.getBean(pipeline, Pipeline.class); if (bean != null) { spider.addPipeline(bean); } } // 设置Downloader // 设置Scheduler // 注册爬虫 spiderMonitor.register(spider); spider.addUrl(spiderConfig.getStartUrl()); if (runAsync) { spider.runAsync(); } else { spider.run(); } } /** * 爬虫状态监控 * @return */ public List<TaskDTO> runTaskList() { MySpiderMonitor spiderMonitor = MySpiderMonitor.instance(); Map<String, MySpiderStatus> spiderStatuses = spiderMonitor.getSpiderStatuses(); List<TaskDTO> taskDTOList = taskService.findAll(); for (TaskDTO taskDTO : taskDTOList) { MySpiderStatus spiderStatus = spiderStatuses.get(taskDTO.getSpiderUUID()); if (spiderStatus == null) { taskDTO.setRunState(Spider.Status.Stopped.name()); } else { taskDTO.setRunState(spiderStatus.getStatus()); } } return taskDTOList; } public TaskDTO stop(TaskDTO taskDTO) { MySpiderMonitor spiderMonitor = MySpiderMonitor.instance(); Map<String, MySpiderStatus> spiderStatuses = spiderMonitor.getSpiderStatuses(); MySpiderStatus spiderStatus = spiderStatuses.get(taskDTO.getSpiderUUID()); if (spiderStatus != null) { spiderStatus.stop(); spiderStatus.getSpider().close(); } return taskDTO; } }
创建爬虫时,将爬虫注册到 MySpiderMonitor 中,之后通过 getSpiderStatuses 方法即可获取所有爬虫的状态了。