Go设计并发Web爬虫

串行爬虫

首先用最简单的方法实现crawler,用串行的方式爬取页面:
在这里用广度优先搜索,将搜索到的页面放到一个队列中,每次再从队列中拿出一个页面进行处理。

//crawl函数能爬取一个页面的所有链接
func crawl(url string) []string {
	fmt.Println(url)
	list, err := links.Extract(url)
	if err != nil {
		log.Print(err)
	}
	return list
}

func main() {
	seen := make(map[string]bool)
	var queue []string
	for _, n := range os.Args[1:] {
		queue = append(queue, n)
	}
	for len(queue) > 0 {
		link := queue[0]
		queue = queue[1:]
		if(!seen[link]) {
			seen[link] = true
			links := crawl(link)
			for _, n := range links {
				if(!seen[n]) {
					queue = append(queue, n)
				}
			}
		}
	}
}

高度并行的爬虫

其次考虑对每个页面都创建一个goroutine。

func main() {
	worklist := make(chan []string)

	// Start with the command-line arguments.
	go func() { worklist <- os.Args[1:] }()

	// Crawl the web concurrently.
	seen := make(map[string]bool)
	for list := range worklist {
		for _, link := range list {
			if !seen[link] {
				seen[link] = true
				go func(link string) {
					worklist <- crawl(link)
				}(link)
			}
		}
	}
}

其中的通道worklist相当于一个具有并发控制功能的队列,主goroutine每次从中取出一个链接并调用crawl
这个实现的问题是并发程度太高了,在执行的过程中创建了大量的goroutine。

降低并行程度

我们可以使用容量为n的缓冲通道来建立一个并发原语,对于缓冲通道的n个空闲槽,每一个代表一个令牌,持有者可以执行,这样我们得到第三个版本的crawler:

// tokens is a counting semaphore used to
// enforce a limit of 20 concurrent requests.
var tokens = make(chan struct{}, 20)

func crawl(url string) []string {
	fmt.Println(url)
	tokens <- struct{}{} // acquire a token
	list, err := links.Extract(url)
	<-tokens // release the token

	if err != nil {
		log.Print(err)
	}
	return list
}

func main() {
	worklist := make(chan []string)
	var n int // number of pending sends to worklist

	// Start with the command-line arguments.
	n++
	go func() { worklist <- os.Args[1:] }()

	// Crawl the web concurrently.
	seen := make(map[string]bool)
	for ; n > 0; n-- {
		list := <-worklist
		for _, link := range list {
			if !seen[link] {
				seen[link] = true
				n++
				go func(link string) {
					worklist <- crawl(link)
				}(link)
			}
		}
	}
}

crawl函数中,每次调用都要先获取一个令牌,否则该goroutine进入休眠直至得到令牌。
通过主goroutine的计数器n,当已经爬完所有页面时,主goroutine将退出。

解决过度并发的另一个方法

思路是事先创建好20个goroutine,让他们同时从一个通道中读数据并处理。

func crawl(url string) []string {
	fmt.Println(url)
	list, err := links.Extract(url)
	if err != nil {
		log.Print(err)
	}
	return list
}

func main() {
	worklist := make(chan []string)  // lists of URLs, may have duplicates
	unseenLinks := make(chan string) // de-duplicated URLs

	// Add command-line arguments to worklist.
	go func() { worklist <- os.Args[1:] }()

	// Create 20 crawler goroutines to fetch each unseen link.
	for i := 0; i < 20; i++ {
		go func() {
			for link := range unseenLinks {
				foundLinks := crawl(link)
				go func() { worklist <- foundLinks }()
			}
		}()
	}

	// The main goroutine de-duplicates worklist items
	// and sends the unseen ones to the crawlers.
	seen := make(map[string]bool)
	for list := range worklist {
		for _, link := range list {
			if !seen[link] {
				seen[link] = true
				unseenLinks <- link
			}
		}
	}
}

主goroutine首先创建了20个goroutine负责爬取页面,他们分别等待通道unseenLinks的一个数据,然后爬取该页面的所有链接,最后将这些链接加入到worklist中。
主goroutine的工作是查看worklist通道中的数据,如果发现一个未遍历过的页面,则将它放到unseenLinks通道中。这个方法的问题是主goroutine可能成为性能的瓶颈。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值