Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
OpenDocCN
think-dast-zh
提交
91054447
T
think-dast-zh
项目概览
OpenDocCN
/
think-dast-zh
8 个月 前同步成功
通知
0
Star
26
Fork
13
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
T
think-dast-zh
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
前往新版Gitcode,体验更适合开发者的 AI 搜索 >>
提交
91054447
编写于
9月 23, 2017
作者:
W
wizardforcel
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
ch15.
上级
f19aef01
变更
1
隐藏空白更改
内联
并排
Showing
1 changed file
with
187 addition
and
0 deletion
+187
-0
15.md
15.md
+187
-0
未找到文件。
15.md
0 → 100644
浏览文件 @
91054447
# 第十五章 爬取维基百科
在本章中,我展示了上一个练习的解决方案,并分析了 Web 索引算法的性能。然后我们构建一个简单的 Web 爬虫。
## 15.1 基于 Redis 的索引器
在我的解决方案中,我们在 Redis 中存储两种结构:
+
对于每个检索词,我们有一个
`URLSet`
,它是一个 Redis 集合,包含检索词的 URL。
+
对于每个网址,我们有一个
`TermCounter`
,这是一个 Redis 哈希表,将每个检索词映射到它出现的次数。
我们在上一章讨论了这些数据类型。你还可以在
<http://thinkdast.com/redistypes>
上阅读 Redis
`Set和`
Hash
`的信息
在`
JedisIndex
`中,我提供了一个方法,它可以接受一个检索词并返回 Redis 中它的`
URLSet
`的键:
```java
private String urlSetKey(String term) {
return "URLSet:" + term;
}
```
以及一个方法,接受 URL 并返回 Redis 中它的`
TermCounter
`的键。
```java
private String termCounterKey(String url) {
return "TermCounter:" + url;
}
```
这里是`
indexPage
`的实现。
```java
public void indexPage(String url, Elements paragraphs) {
System.out.println("Indexing " + url);
// make a TermCounter and count the terms in the paragraphs
TermCounter tc = new TermCounter(url);
tc.processElements(paragraphs);
// push the contents of the TermCounter to Redis
pushTermCounterToRedis(tc);
}
```
为了索引页面,我们:
+ 为页面内容创建一个 Java 的`
TermCounter
`,使用上一个练习中的代码。
+ 将`
TermCounter
`的内容推送到 Redis。
以下是将`
TermCounter
`的内容推送到 Redis 的新代码:
```java
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 hash
t.del(hashname);
// for each term, add an entry in the TermCounter and a new
// member of the index
for (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
`,它需要一个检索词,并从该词出现的每个网址返回一个映射。这是我的解决方案:
```java
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 处显示的次数。
以下是实现:
```java
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
`添加元素,并且
+ 向 Redis`
TermCounter
`添加元素。
这两个都是常数时间的操作,所以推送`
TermCounter
`的总时间对于唯一检索词的数量是线性的。
总之,`
TermCounter
`的创建与页面上的单词数成正比。向 Redis 推送`
TermCounter
`与唯一检索词的数量成正比。
由于页面上的单词数量通常超过唯一检索词的数量,因此整体复杂度与页面上的单词数成正比。理论上,一个页面可能包含索引中的所有检索词,因此最坏的情况是`
O(M)
`,但实际上我们并不期待看到更糟糕的情况。
这个分析提出了一种提高效率的方法:我们应该避免索引很常见的词语。首先,他们占用了大量的时间和空间,因为它们出现在几乎每一个`
URLSet
`和`
TermCounter
`
中。此外,它们不是很有用,因为它们不能帮助识别相关页面。
大多数搜索引擎避免索引常用单词,这在本文中称为停止词(
<http://thinkdast.com/stopword>
)。
## 15.4 图的遍历
如果你在第七章中完成了“到达哲学”练习,你已经有了一个程序,它读取维基百科页面,找到第一个链接,使用链接加载下一页,然后重复。这个程序是一种专用的爬虫,但是当人们说“网络爬虫”时,他们通常意味着一个程序:
加载起始页面并对内容进行索引,
查找页面上的所有链接,并将链接的 URL 添加到集合中
通过收集,加载和索引页面,以及添加新的 URL,来按照它的方式工作。
如果它找到已经被索引的 URL,会跳过它。
你可以将 Web 视为图,其中每个页面都是一个节点,每个链接都是从一个节点到另一个节点的有向边。如果你不熟悉图,可以阅读
<http://thinkdast.com/graph>
。
从源节点开始,爬虫程序遍历该图,访问每个可达节点一次。
我们用于存储 URL 的集合决定了爬网程序执行哪种遍历:
+
如果它是先进先出(FIFO)队列,则爬虫程序将执行广度优先遍历。
+
如果它是后进先出(LIFO)栈,则爬虫程序将执行深度优先遍历。
+
更通常来说,集合中的条目可能具有优先级。例如,我们可能希望对尚未编入索引的页面给予较高的优先级。
你可以在
<http://thinkdast.com/graphtrav>
上阅读图的遍历的更多信息 。
## 15.5 练习 12
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录