串行爬虫
首先用最简单的方法实现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可能成为性能的瓶颈。