《Go官方指南》练习:Web 爬虫

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

题目:

练习:Web 爬虫

在这个练习中,我们将会使用 Go 的并发特性来并行化一个 Web 爬虫。

修改 Crawl 函数来并行地抓取 URL,并且保证不重复。

提示:你可以用一个 map 来缓存已经获取的 URL,但是要注意 map 本身并不是并发安全的!

思路:

借鉴利用队列实现广度优先算法的思路,把第一个url当做根节点,根节点下面的url当做孩子节点,队列当做管道。

  • 把第一个url作为根节点,查询[showCrawl()函数]
  • 将对应孩子节点集合加入管道。
  • 遍历这个孩子节点集合
  • 重复2~3步骤
  • 直到管道中没有元素

实现:

  1. (完整代码见最后面)
  2. 一些关键性问题在本代码后面
// 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写入管道?

      1. 首先,把每个查询后的url页面下属的url集合放入管道中,要把管道内元素个数++

      2. 不用判断更加方便

      3. 如果没有任何链接,Fetch()会返回一个nil的[]string数组,并且将nil数组加入到管道中

      4. 在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/",
		},
	},
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值