第十五章 爬取维基百科

原文:Chapter 15 Crawling Wikipedia

译者:飞龙

协议:CC BY-NC-SA 4.0

自豪地采用谷歌翻译

在本章中,我展示了上一个练习的解决方案,并分析了 Web 索引算法的性能。然后我们构建一个简单的 Web 爬虫。

15.1 基于 Redis 的索引器

在我的解决方案中,我们在 Redis 中存储两种结构:

  • 对于每个检索词,我们有一个URLSet,它是一个 Redis 集合,包含检索词的 URL。
  • 对于每个网址,我们有一个TermCounter,这是一个 Redis 哈希表,将每个检索词映射到它出现的次数。

我们在上一章讨论了这些数据类型。你还可以在 http://thinkdast.com/redistypes 上阅读 Redis Set和Hash`的信息

JedisIndex中,我提供了一个方法,它可以接受一个检索词并返回 Redis 中它的URLSet的键:

private String urlSetKey(String term) {return "URLSet:" + term;
}

以及一个方法,接受 URL 并返回 Redis 中它的TermCounter的键。

private String termCounterKey(String url) {return "TermCounter:" + url;
}

这里是indexPage的实现。

public void indexPage(String url, Elements paragraphs) {System.out.println("Indexing " + url);// make a TermCounter and count the terms in the paragraphsTermCounter tc = new TermCounter(url);tc.processElements(paragraphs);// push the contents of the TermCounter to RedispushTermCounterToRedis(tc);
}

为了索引页面,我们:

  • 为页面内容创建一个 Java 的TermCounter,使用上一个练习中的代码。
  • TermCounter的内容推送到 Redis。

以下是将TermCounter的内容推送到 Redis 的新代码:

public List<Object> pushTermCounterToRedis(TermCounter tc) {Transaction t = jedis.multi();String url = tc.getLabel();String hashname = termCounterKey(url);// if this page has already been indexed, delete the old hasht.del(hashname);// for each term, add an entry in the TermCounter and a new// member of the indexfor (String term: tc.keySet()) {Integer count = tc.get(term);t.hset(hashname, term, count.toString());t.sadd(urlSetKey(term), url);}List<Object> res = t.exec();return res;
}

该方法使用Transaction来收集操作,并将它们一次性发送到服务器,这比发送一系列较小操作要快得多。

它遍历TermCounter中的检索词。对于每一个,它:

  • 在 Redis 上寻找或者创建TermCounter,然后为新的检索词添加字段。
  • 在 Redis 上寻找或创建URLSet,然后添加当前的 URL。

如果页面已被索引,则TermCounter在推送新内容之前删除旧页面 。

新的页面的索引就是这样。

练习的第二部分要求你编写getCounts,它需要一个检索词,并从该词出现的每个网址返回一个映射。这是我的解决方案:

    public Map<String, Integer> getCounts(String term) {Map<String, Integer> map = new HashMap<String, Integer>();Set<String> urls = getURLs(term);for (String url: urls) {Integer count = getCount(url, term);map.put(url, count);}return map;}

此方法使用两种辅助方法:

  • getURLs接受检索词并返回该字词出现的网址集合。
  • getCount接受 URL 和检索词,并返回该术语在给定 URL 处显示的次数。

以下是实现:

    public Set<String> getURLs(String term) {Set<String> set = jedis.smembers(urlSetKey(term));return set;}public Integer getCount(String url, String term) {String redisKey = termCounterKey(url);String count = jedis.hget(redisKey, term);return new Integer(count);}

由于我们设计索引的方式,这些方法简单而高效。

15.2 查找的分析

假设我们索引了N个页面,并发现了M个唯一的检索词。检索词的查询需要多长时间?在继续之前,先考虑一下你的答案。

要查找一个检索词,我们调用getCounts,其中:

  • 创建映射。
  • 调用getURLs来获取 URL 的集合。
  • 对于集合中的每个 URL,调用getCount并将条目添加到HashMap

getURLs所需时间与包含检索词的网址数成正比。对于罕见的检索词,这可能是一个很小的数字,但是对于常见检索词,它可能和N一样大。

在循环中,我们调用了getCount,它在 Redis 上寻找TermCounter,查找一个检索词,并向HashMap添加一个条目。那些都是常数时间的操作,所以在最坏的情况下,getCounts的整体复杂度是O(N)。然而实际上,运行时间正比于包含检索词的页面数量,通常比N小得多。

这个算法根据复杂性是有效的,但是它非常慢,因为它向 Redis 发送了许多较小的操作。你可以使用Transaction来加快速度 。你可能留作一个练习,或者你可以在RedisIndex.java中查看我的解决方案。

15.3 索引的分析

使用我们设计的数据结构,页面的索引需要多长时间?再次考虑你的答案,然后再继续。

为了索引页面,我们遍历其 DOM 树,找到所有TextNode对象,并将字符串拆分成检索词。这一切都与页面上的单词数成正比。

对于每个检索词,我们在HashMap中增加一个计数器,这是一个常数时间的操作。所以创建TermCounter的所需时间与页面上的单词数成正比。

TermCounter推送到 Redis ,需要删除TermCounter,对于唯一检索词的数量是线性的。那么对于每个检索词,我们必须:

  • URLSet添加元素,并且
  • 向 RedisTermCounter添加元素。

这两个都是常数时间的操作,所以推送TermCounter的总时间对于唯一检索词的数量是线性的。

总之,TermCounter的创建与页面上的单词数成正比。向 Redis 推送TermCounter与唯一检索词的数量成正比。

由于页面上的单词数量通常超过唯一检索词的数量,因此整体复杂度与页面上的单词数成正比。理论上,一个页面可能包含索引中的所有检索词,因此最坏的情况是O(M),但实际上我们并不期待看到更糟糕的情况。

这个分析提出了一种提高效率的方法:我们应该避免索引很常见的词语。首先,他们占用了大量的时间和空间,因为它们出现在几乎每一个URLSetTermCounter中。此外,它们不是很有用,因为它们不能帮助识别相关页面。

大多数搜索引擎避免索引常用单词,这在本文中称为停止词(http://thinkdast.com/stopword)。

15.4 图的遍历

如果你在第七章中完成了“到达哲学”练习,你已经有了一个程序,它读取维基百科页面,找到第一个链接,使用链接加载下一页,然后重复。这个程序是一种专用的爬虫,但是当人们说“网络爬虫”时,他们通常意味着一个程序:

加载起始页面并对内容进行索引,
查找页面上的所有链接,并将链接的 URL 添加到集合中
通过收集,加载和索引页面,以及添加新的 URL,来按照它的方式工作。
如果它找到已经被索引的 URL,会跳过它。

你可以将 Web 视为图,其中每个页面都是一个节点,每个链接都是从一个节点到另一个节点的有向边。如果你不熟悉图,可以阅读 http://thinkdast.com/graph。

从源节点开始,爬虫程序遍历该图,访问每个可达节点一次。

我们用于存储 URL 的集合决定了爬虫程序执行哪种遍历:

  • 如果它是先进先出(FIFO)的队列,则爬虫程序将执行广度优先遍历。
  • 如果它是后进先出(LIFO)的栈,则爬虫程序将执行深度优先遍历。
  • 更通常来说,集合中的条目可能具有优先级。例如,我们可能希望对尚未编入索引的页面给予较高的优先级。

你可以在 http://thinkdast.com/graphtrav 上阅读图的遍历的更多信息 。

15.5 练习 12

现在是时候写爬虫了。在本书的仓库中,你将找到此练习的源文件:

  • WikiCrawler.java,包含你的爬虫的其实代码。
  • WikiCrawlerTest.java,包含WikiCrawler的测试代码。
  • JedisIndex.java,这是我以前的练习的解决方案。

你还需要一些我们以前练习中使用过的辅助类:

  • JedisMaker.java
  • WikiFetcher.java
  • TermCounter.java
  • WikiNodeIterable.java

在运行JedisMaker之前,你必须提供一个文件,关于你的 Redis 服务器信息。如果你在上一个练习中这样做,你应该全部配置好了。否则,你可以在 14.3 节中找到说明。

运行ant build来编译源文件,然后运行ant JedisMaker来确保它配置为连接到你的 Redis 服务器。

现在运行ant WikiCrawlerTest。它应该失败,因为你有工作要做!

这是我提供的WikiCrawler类的起始:

public class WikiCrawler {public final String source;private JedisIndex index;private Queue<String> queue = new LinkedList<String>();final static WikiFetcher wf = new WikiFetcher();public WikiCrawler(String source, JedisIndex index) {this.source = source;this.index = index;queue.offer(source);}public int queueSize() {return queue.size();}

实例变量是:

  • source是我们开始抓取的网址。
  • indexJedisIndex,结果应该放进这里。
  • queueLinkedList,这里面我们跟踪已发现但尚未编入索引的网址。
  • wfWikiFetcher,我们用来读取和解析网页。

你的工作是填写crawl。这是原型:

public String crawl(boolean testing) throws IOException {}

当这个方法在WikiCrawlerTest中调用时,testing参数为true,否则为false

如果testingtruecrawl方法应该:

  • 以 FIFO 的顺序从队列中选择并移除一个 URL。
  • 使用WikiFetcher.readWikipedia读取页面的内容,它读取仓库中包含的,页面的缓存副本来进行测试(如果维基百科的版本更改,则避免出现问题)。
  • 它应该索引页面,而不管它们是否已经被编入索引。
  • 它应该找到页面上的所有内部链接,并按他们出现的顺序将它们添加到队列中。“内部链接”是指其他维基百科页面的链接。
  • 它应该返回其索引的页面的 URL。

如果testingfalse,这个方法应该:

  • 以 FIFO 的顺序从队列中选择并移除一个 URL。
  • 如果 URL 已经被编入索引,它不应该再次索引,并应该返回null
  • 否则它应该使用WikiFetcher.fetchWikipedia读取页面内容,从 Web 中读取当前内容。
  • 然后,它应该对页面进行索引,将链接添加到队列,并返回其索引的页面的 URL。

WikiCrawlerTest加载具有大约200个链接的队列,然后调用crawl三次。每次调用后,它将检查队列的返回值和新长度。

当你的爬虫按规定工作时,此测试应通过。祝你好运!

数据结构思维 第十五章 爬取维基百科相关推荐

  1. [python学习] 简单爬取维基百科程序语言消息盒

    文章主要讲述如何通过Python爬取维基百科的消息盒(Infobox),主要是通过正则表达式和urllib实现:后面的文章可能会讲述通过BeautifulSoup实现爬取网页知识.由于这方面的文章还是 ...

  2. java 爬 维基百科_爬取维基百科词条

    python爬取维基百科词条,获得某词汇页面中释义段落的加粗词汇.链接词汇以及所爬词汇对应的分类,为自动构建同义词汇树准备数据. 以下代码可能为一次性代码,因为解析html靠的是标签的相对位置,维基百 ...

  3. Python爬虫实战:爬取维基百科

    我们知道,百度百科一般极少收录英文词条 类似的,很容易想到爬取维基百科,思路也和爬取百度百科一样,只需处理一下请求地址和返回结果就好 下面也是直接放上代码,有不明白的地方可以看看注释: from lx ...

  4. 程序员编程艺术第三十四 三十五章 格子取数问题,完美洗牌算法

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 第三十四 ...

  5. 数据结构思维 第十四章 持久化

    第十四章 持久化 原文:Chapter 14 Persistence 译者:飞龙 协议:CC BY-NC-SA 4.0 自豪地采用谷歌翻译 在接下来的几个练习中,我们将返回到网页搜索引擎的构建.为了回 ...

  6. 数据结构思维 第十二章 `TreeMap`

    第十二章 TreeMap 原文:Chapter 12 TreeMap 译者:飞龙 协议:CC BY-NC-SA 4.0 自豪地采用谷歌翻译 这一章展示了二叉搜索树,它是个Map接口的高效实现.如果我们 ...

  7. 四十五、爬取QQ音乐Lemon 日语歌的评论

    @Author:Runsen 我们继续学习Python爬虫.本次爬取的对象是QQ音乐Lemon 日语歌的评论 首先查看一下,我们要爬取的网页,是否反爬(各种侵害人家服务器的事情,我们不能干) QQ音乐 ...

  8. 二十五、爬取毛豆新车的数据

    最近发现有篇公众号爬虫文章没有更新到CSDN爬虫篇|爬虫实战(十),一般都是CSDN没有更新到公众号中. 前言: 对于爬虫还有一点小知识:fake_useragent的使用 fake_useragen ...

  9. python爬虫十五:爬取12306火车票信息

    转:https://zhuanlan.zhihu.com/p/26701898 # -*- coding: utf-8 -*- ''' 获取12306城市名和城市代码的数据 文件名: parse_st ...

最新文章

  1. Linux运维课程 第一阶段 重难点摘要(六)CISCO
  2. Linux基础之grep攻坚
  3. 数据中台建设是噱头还是黑科技?
  4. OpenGL ES之着色语言GLSL的使用说明及API
  5. 事件委托 EventHandler 。
  6. bzoj15523506 robotic sort
  7. php不支持redis
  8. 巴西矿坝决堤事故已致58人死亡 多达300人失踪
  9. 利用设计模式优化项目实际的申报业务
  10. DBConvert Studio - 数据库迁移和同步 - Crack
  11. 去掉flash player自带的fff新推荐广告插件
  12. 共享单车数据集_共享单车数据的数据可视化
  13. linaro软件源更新问题
  14. 类动态规划求解较小规模的最大团问题(Python实现)
  15. 去哪找计算机相关论文,计算机行业论文在哪发表有影响力
  16. 前端下载文件流PDF文件显示空白 或 无法打开
  17. 从“游击队”到“正规军”:虾神成长史
  18. C# 串口接收含有asic2=0x3f时发生的故障
  19. 时钟(CLOCK)置换算法
  20. 固本培元之三:Convert、运算符、流程控制语句、ref/out/in三种参数类型

热门文章

  1. (60)FPGA比较器实现(function)
  2. ubuntu jdk tomcat mysql_Ubuntu下安装JDK+TOMCAT+MYSQL
  3. 学计算机修图,宅家修图很简单 手机电脑都可以修出大片的感觉
  4. php声明js变量类型,js中变量是什么以及有哪些类型
  5. java双缓冲绘图_Java双缓冲技术-绘图应用
  6. 物联网-移远M26模块OpenCPU开发第1讲
  7. 高性能HTTP加速器Varnish(安装配置篇)
  8. 三星U-Boot-1.1.6源码分析lowlevel_init.S (board\samsung\smdk6410)
  9. 深入理解ARM体系架构(S3C6410)---PWM实例
  10. 进入全屏 nodejs+express+mysql实现restful风格的增删改查示例