题目:
练习:Web 爬虫
在这个练习中,我们将会使用 Go 的并发特性来并行化一个 Web 爬虫。
修改 Crawl 函数来并行地抓取 URL,并且保证不重复。
提示:你可以用一个 map 来缓存已经获取的 URL,但是要注意 map 本身并不是并发安全的!
思路:
借鉴利用队列实现广度优先算法的思路,把第一个url当做根节点,根节点下面的url当做孩子节点,队列当做管道。
- 把第一个url作为根节点,查询[showCrawl()函数]
- 将对应孩子节点集合加入管道。
- 遍历这个孩子节点集合
- 重复2~3步骤
- 直到管道中没有元素
实现:
- (完整代码见最后面)
- 一些关键性问题在本代码后面
// Crawl 使用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。
func Crawl(url string, depth int, fetcher Fetcher) {
// 初始化工作
if depth < 1 {
return
}
Urls := make(chan []string, 10) //相当于队列
go showCrawl(url, Urls, fetcher) // 根节点遍历
flag := 1 // 用于探测管道中还有多少个元素
// 2. 根据要求的深度(depth)查询 : 向下查询多少层
for i := depth; i > 0; i-- {
// 只要管道里有元素,就读取
if flag > 0 {
urls := <- Urls // urls == []string
flag-- // 管道元素数量--
for _, url := range urls { // 一个页面下的url集合
fmt.Printf("接收到的url:%s\n", url)
if _, ok := store[url]; !ok { // 判断是否查询过
go showCrawl(url, Urls, fetcher)
flag++
}
}
}
}
查询函数:
// 查询页面,并将页面下的url放到管道中
func showCrawl(url string, Urls chan []string, fetcher Fetcher) {
// 1. 搜索url并存储
// 2. 将本页面下属的url写入管道
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println(err)
} else {
// 显示并存入字典中
fmt.Printf("found: %s %q\n", url, body)
// flag++
}
store[url] = body // 查询过,但是页面没有内容
Urls <- urls
}
一些关键问题说明:
-
为什么要使用map来存储url而不用数组?
-
虽然只需要存储一个数据(url),但是通过map[url] (通过key来作为索引) 可以判断url是否存在,而数组判断某个值是否存在相对麻烦。
-
map 可以通过语句
_, ok := map[url]来判断某个key-value 对是否存在。当这个索引不存在 OK == false
-
-
管道如何设置"当管道为空时" 停止读取?
- 设置一个
flag int用于记录管道中还有多少个元素,每次取元素之前判断flag 是否为0
- 设置一个
-
关于管道记录数flag,为什么不判断直接++呢?万一这个url下面没有任何链接呢,那么就可以不用将[]string写入管道?
-
-
首先,把每个查询后的url页面下属的url集合放入管道中,要把管道内元素个数++
-
不用判断更加方便
-
如果没有任何链接,Fetch()会返回一个nil的[]string数组,并且将nil数组加入到管道中
-
在Crawl()中执行 for _, url := range urls时,这个for循环不会执行 。正因为这一条,我们可以每次执行查询命令都直接将nil 的[]string加入管道,统一管理
注意,在for…range 空数组时,只是不会有任何输出,但不会报错。
-
-
全部代码:
package main
import (
"fmt"
)
type Fetcher interface {
// Fetch 返回 URL 的 body 内容,并且将在这个页面上找到的 URL 放到一个 slice 中。
Fetch(url string) (body string, urls []string, err error)
}
// 存储已经找到url及其body
var store = make(map[string]string)
// 查询页面,并将页面下的url放到管道中
func showCrawl(url string, Urls chan []string, fetcher Fetcher) {
// 1. 搜索url并存储
// 2. 将本页面下属的url写入管道
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println(err)
} else {
// 显示并存入字典中
fmt.Printf("found: %s %q\n", url, body)
// flag++
}
store[url] = body // 查询过,但是页面没有内容
Urls <- urls
}
// Crawl 使用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。
func Crawl(url string, depth int, fetcher Fetcher) {
// TODO: 并行的抓取 URL。
// TODO: 不重复抓取页面。
// 下面并没有实现上面两种情况:
// 初始化工作
if depth < 1 {
return
}
Urls := make(chan []string, 10) //需要有缓存的管道:广度优先遍历需要一层层存储
go showCrawl(url, Urls, fetcher) // 用于开篇第一次的查询
flag := 1 // 用于探测管道中还有多少组数据(一个页面的全部下属url为一组)
// 2. 根据要求的深度(depth)查询 : 向下查询多少层
for i := depth; i > 0; i-- {
// 广度优先查询下属url:不一定是广度优先,只要管道里有,就读取
// fmt.Print("dfff")
if flag > 0 {
urls := <-Urls // urls == []string
fmt.Printf("i:%d, flag:%d urls:%s\n", i, flag, urls)
flag-- // 读走一个集合
for _, url := range urls { // 一个页面下的url集合
fmt.Printf("接收到的url:%s\n", url)
if _, ok := store[url]; !ok { // 判断是否查询过
go showCrawl(url, Urls, fetcher)
flag++ //把每个查询后的url页面下属的url集合放入管道中,要把管道内元素个数++
//为什么不判断直接++呢?万一这个url下面没有任何链接呢?
//1. 不用判断更加方便 2. 如果没有任何链接,Feth()会返回一个nil数组,并且将nil数组加入到管道中
// 在Crawl()中执行 for _, url := range urls时,这个for循环不会执行
}
}
}
}
}
func main() {
// flag := 1
Crawl("https://golang.org/", 6, fetcher)
}
// fakeFetcher 是返回若干结果的 Fetcher。
type fakeFetcher map[string]*fakeResult
type fakeResult struct {
body string
urls []string
}
func (f fakeFetcher) Fetch(url string) (string, []string, error) {
if res, ok := f[url]; ok {
return res.body, res.urls, nil
}
return "", nil, fmt.Errorf("not found: %s", url)
}
// fetcher 是填充后的 fakeFetcher。
var fetcher = fakeFetcher{
"https://golang.org/": &fakeResult{
"The Go Programming Language",
[]string{
"https://golang.org/pkg/",
"https://golang.org/cmd/",
},
},
"https://golang.org/pkg/": &fakeResult{
"Packages",
[]string{
"https://golang.org/",
"https://golang.org/cmd/",
"https://golang.org/pkg/fmt/",
"https://golang.org/pkg/os/",
},
},
"https://golang.org/pkg/fmt/": &fakeResult{
"Package fmt",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
"https://golang.org/pkg/os/": &fakeResult{
"Package os",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
}

本文介绍了一个使用Go语言并发特性的Web爬虫练习。通过建立一个队列模拟广度优先搜索,利用map存储已抓取URL并解决并发安全性问题。详细讨论了为何选择map而非数组,以及如何通过计数器管理管道空状态,确保爬虫正确停止。
1295

被折叠的 条评论
为什么被折叠?



