webmagic简介
webmagic是一个开源的Java爬虫框架,中文文档:http://webmagic.io/docs/zh。
一个好的框架必然凝聚了领域知识。WebMagic的设计参考了业界最优秀的爬虫Scrapy,而实现则应用了HttpClient、Jsoup等Java世界最成熟的工具,目标就是做一个Java语言Web爬虫的教科书般的实现。
如果你是爬虫开发老手,那么WebMagic会非常容易上手,它几乎使用Java原生的开发方式,只不过提供了一些模块化的约束,封装一些繁琐的操作,并且提供了一些便捷的功能。
如果你是爬虫开发新手,那么使用并了解WebMagic会让你了解爬虫开发的常用模式、工具链、以及一些问题的处理方式。熟练使用之后,相信自己从头开发一个爬虫也不是什么难事。
因为这个目标,WebMagic的核心非常简单——在这里,功能性是要给简单性让步的。
虽然简单,但是完成所有功能,还是要花很多时间的。这里之分析webmagic-core。
webmagic架构图
![]()
webmagic有四个组件:Downloader、PageProcessor、Scheduler、Pipline。
三个数据对象:Request、Page、ResultItems。
spider的实例
1 2 3 4 5 6 7 8 9
| Spider.create(new XXXProcessor()) .setScheduler(new FileCacheQueueScheduler("D:\\webcrawler_test")) .addUrl("https://github.com/code4craft").addPipeline(new LinuxidcPipline()) .thread(5) .run();
|
webmagic线程模型
webmagic是典型的生产者消费者模式,spider实现了Runnable接口,作为生产线程,从Scheduler中取出request,提交给线程池CountableThreadPool运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| public void run() { checkRunningStat(); initComponent(); logger.info("Spider {} started!",getUUID()); while (!Thread.currentThread().isInterrupted() && stat.get() == STAT_RUNNING) { final Request request = scheduler.poll(this); if (request == null) { if (threadPool.getThreadAlive() == 0 && exitWhenComplete) { break; } System.out.println("spider wait"); waitNewUrl(); } else { threadPool.execute(new Runnable() { @Override public void run() { try { processRequest(request); onSuccess(request); } catch (Exception e) { onError(request); logger.error("process request " + request + " error", e); } finally { pageCount.incrementAndGet(); signalNewUrl(); } } }); } } stat.set(STAT_STOPPED); if (destroyWhenExit) { close(); } logger.info("Spider {} closed! {} pages downloaded.", getUUID(), pageCount.get()); }
|
spider的停止逻辑是:第一种情况,队列中没有新的url了,并且线程池中没有正在下载的队列,也就是说不会产生新的url了。如果设置了完成后退出,就会退出。第二种情况,队列中没有新的url了,但线程池还有下载队列,可能产生新的url,就在这里wait。等待线程池中线程下载完后signal。exitWhenComplete控制spider是否爬完就退出,如果不退出,spider会在队列空后等待,然后每隔一段时间唤醒,检查队列是否为空。添加新url也会唤醒。
spider在while循环中不断从队列中获得request,封装成Runnable交给线程池,如果队列很大,会不会封装很多的Runnable,从而造成内存不足呢?答案是不会的,作者封装了一个CountableThreadPool,spider初始化它的实例的时候,设置了一个threadNum,如果线程池满了,spider的生产线程要wait。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| public void execute(final Runnable runnable) {
if (threadAlive.get() >= threadNum) { System.out.println("进来了"); try { reentrantLock.lock(); while (threadAlive.get() >= threadNum) { try { condition.await(); } catch (InterruptedException e) { } } } finally { reentrantLock.unlock(); } } threadAlive.incrementAndGet(); executorService.execute(new Runnable() { @Override public void run() { try { runnable.run(); } finally { try { reentrantLock.lock(); threadAlive.decrementAndGet(); condition.signal(); } finally { reentrantLock.unlock(); } } } }); }
|
- 多个线程下载,spider中只有一个downloader实例。查看spider run()中的initComponent()
1 2 3 4
| protected void initComponent() { if (downloader == null) { this.downloader = new HttpClientDownloader(); }
|
原因是downloader使用PoolingHttpClientConnectionManager,默认就支持多线程,传入的poolSize就是spider中的threadNum,线程池的大小也是threadNum,所以没有问题。
1 2 3 4 5 6 7
| public HttpClientGenerator setPoolSize(int poolSize) { connectionManager.setMaxTotal(poolSize); return this; }
|
webmagic组件
组件没什么可说的,就是各种设计模式。downloader组件封装了Apache HttpClient,考虑了ssl,cookie,redirect。可以借鉴一下。