开源JAVA爬虫crawler4j源码分析 - 3 线程管理

要做一个大的系统,得要聘请多个程序员一起工作,同样,要爬取内容庞大的互联网数据,如果只有一只爬虫,当然是不够的。

使用crawler4j只需配置指定数量的线程,就会有指定数量的爬虫线程一起抓取指定的网页内容:

controller.start(BasicCrawler.class, numberOfCrawlers);//numberOfCrawlers:线程数

那么,crawler4j到底是怎样管理这些线程的呢?以下假定指定的是10个线程。

进入CrawlController的start方法:

首先,定义两个List,分别用来存储所有的爬虫线程和实际的爬虫逻辑类:

			final List<Thread> threads = new ArrayList<>();
			final List<T> crawlers = new ArrayList<>();

			for (int i = 1; i <= numberOfCrawlers; i++) {
				T crawler = _c.newInstance();
				Thread thread = new Thread(crawler, "Crawler " + i);
				crawler.setThread(thread);
				crawler.init(i, this);
				thread.start();
				crawlers.add(crawler);
				threads.add(thread);
				logger.info("Crawler " + i + " started.");
			}

循环10次,创建10个线程并启动它们。这时爬虫线程就已经开始工作了,不断的循环爬取页面,我们暂且不管具体单个爬虫是怎么工作的,本文只关注线程管理。

既然线程已经启动并正在工作了,怎么监控它们呢? 往下:

			Thread monitorThread = new Thread(new Runnable() {

				@Override
				public void run() {/*...此处省略N个字...*/}
			}

			monitorThread.start();

			if (isBlocking) {
				waitUntilFinish();
			}

创建一个监控的线程并启动,然后主线程开始睡觉直到完成。监控线程monitor不断的、每隔10秒检查一次:

								sleep(10);
								boolean someoneIsWorking = false;
								for (int i = 0; i < threads.size(); i++) {
									Thread thread = threads.get(i);
									if (!thread.isAlive()) {
										if (!shuttingDown) {
											logger.info("Thread " + i + " was dead, I'll recreate it.");
											T crawler = _c.newInstance();
											thread = new Thread(crawler, "Crawler " + (i + 1));
											threads.remove(i);
											threads.add(i, thread);
											crawler.setThread(thread);
											crawler.init(i + 1, controller);
											thread.start();
											crawlers.remove(i);
											crawlers.add(i, crawler);
										}
									} else if (crawlers.get(i).isNotWaitingForNewURLs()) {
										someoneIsWorking = true;
									}
								}

循环遍历每一个爬虫线程,首先看是否还活着,如果线程已死但是爬取工作却还没有结束,则重新创建一个新的线程加入列表,并删除原来已经die的线程,这样可确保一直都有10个线程在工作,防止某个工人不小心掉井下挂了然后影响工期 :)

如果活着,接着检查线程是不是正在干活,即不在等新的URL。什么意思呢?稍微有点绕,1个爬虫线程开始工作后会先去URL地址库领取50个URL,然后对个50个URL进行抓取,把抓取到的新的URL填入URL地址库,完了再去领……10个爬虫线程开始工作后就在URL地址库窗口排队领取,如果第X个领完就木有了,后面那10-X个就只能坐在那打牌了,等着前面X个找到了新URL。这时,只要X>0,则说明整个爬取工作还在进行。假设那X个线程没找着新的URL就回来了,那只能跟着排在后面押宝了。这时monitor线程发现,10个家伙都在打牌押宝,好,说明工作可能完成了,该验收了。

接下来:

								if (!someoneIsWorking) {
									// Make sure again that none of the threads
									// are
									// alive.
									logger.info("It looks like no thread is working, waiting for 10 seconds to make sure...");
									sleep(10);

									someoneIsWorking = false;
									for (int i = 0; i < threads.size(); i++) {
										Thread thread = threads.get(i);
										if (thread.isAlive() && crawlers.get(i).isNotWaitingForNewURLs()) {
											someoneIsWorking = true;
										}
									}
									if (!someoneIsWorking) {
										if (!shuttingDown) {
											long queueLength = frontier.getQueueLength();
											if (queueLength > 0) {
												continue;
											}
											logger.info("No thread is working and no more URLs are in queue waiting for another 10 seconds to make sure...");
											sleep(10);
											queueLength = frontier.getQueueLength();
											if (queueLength > 0) {
												continue;
											}
										}

为了保险起见,再检查一下看看10个工人是不是都还活着并且在打牌。

如果是的,再检查一下URL地址库还有没有URL,如果有,则继续等10秒后再重新查。为什么要这样呢?这是为了防止低概率事件发生,比如第1个工人正在打牌,第10个刚好带着新的URL回来了,也加入打牌队伍,第1个工人正要去领URL时监工monitor来了,看到都在,以为可以收工了,实际上URL地址库还有,所以此时monitor再查一下还有没有剩余的URL没爬取。如果没有了,这时再等10秒看下是否真的没有了,防止刚刚第10个工人刚带回新URL来而错过了。

10秒后发现也没有,收工:

										logger.info("All of the crawlers are stopped. Finishing the process...");
										// At this step, frontier notifies the
										// threads that were
										// waiting for new URLs and they should
										// stop
										frontier.finish();
										for (T crawler : crawlers) {
											crawler.onBeforeExit();
											crawlersLocalData.add(crawler.getMyLocalData());
										}

										logger.info("Waiting for 10 seconds before final clean up...");
										sleep(10);

										frontier.close();
										docIdServer.close();
										pageFetcher.shutDown();

										finished = true;
										waitingLock.notifyAll();

										return;

这些都是收尾工作了。 不过作者在这少收了一个,就是忘了把Environment close了,这导致程序不结束的话临时文件也删不掉,把Environment env变成field然后在这加上env.close()就好了。

因主线程还一直在做梦,waitingLock.notifyAll();是叫醒主线程可以结束了,下面是主线程睡觉的位置:

	public void waitUntilFinish() {
		while (!finished) {
			synchronized (waitingLock) {
				if (finished) {
					return;
				}
				try {
					waitingLock.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}

这就是一个JAVA爬虫的线程管理机制,crawler4j以一种相对比较简单但是又比较安全的方法实现了,值得借鉴。

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值