Golang协程与网络爬虫的实现

Golang协程与网络爬虫的实现 以下示例将获取第一个URL后程序就会退出,因为我在运行goroutine go crawl() 如何等待爬虫完成工作(除了在最后使用fmt.Scanln())?

func Crawl(url string, depth int) {
 if depth <= 0 {
  return 
 }

 urls, err := Fetch(url)
 if err != nil  {
  fmt.Println(err)
  return
 }
 fmt.Println("Found urls in ", url)

 for _, u := urls {  
  go Crawl(u, depth-1)
 }

 return
}

func Fetch(url string) []string, error {
 // ... 从URL获取/处理数据
 if urls, ok := data[url]; ok {
  return urls, nil
 } 
 return nil, fmt.Errorf("error %s", url) 
}

func main() {
 Crawl("golang.org", 4)
}

注: 这是来自Go语言之旅:网络爬虫练习的练习题


更多关于Golang协程与网络爬虫的实现的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

使用 sync.WaitGroup。请参考此示例

更多关于Golang协程与网络爬虫的实现的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


可以使用 sync.WaitGroup 来等待所有爬虫协程完成工作。以下是修改后的代码:

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

func Crawl(url string, depth int) {
    defer wg.Done() // 协程结束时通知WaitGroup
    
    if depth <= 0 {
        return
    }

    urls, err := Fetch(url)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("Found %d urls in %s\n", len(urls), url)

    for _, u := range urls {
        wg.Add(1) // 为每个新的爬虫协程增加计数
        go Crawl(u, depth-1)
    }
}

func Fetch(url string) ([]string, error) {
    // 模拟获取URL数据
    data := map[string][]string{
        "golang.org": {"golang.org/pkg", "golang.org/doc"},
        "golang.org/pkg": {"golang.org/pkg/fmt", "golang.org/pkg/os"},
        "golang.org/doc": {"golang.org/doc/tutorial", "golang.org/doc/effective_go"},
    }
    
    if urls, ok := data[url]; ok {
        return urls, nil
    }
    return nil, fmt.Errorf("error fetching %s", url)
}

func main() {
    wg.Add(1) // 为主爬虫任务增加计数
    go Crawl("golang.org", 4)
    wg.Wait() // 等待所有爬虫协程完成
    fmt.Println("All crawling tasks completed")
}

另一个更优雅的解决方案是使用带缓冲的channel来限制并发数量:

package main

import (
    "fmt"
    "sync"
)

var tokens = make(chan struct{}, 20) // 限制最多20个并发爬虫

func Crawl(url string, depth int, wg *sync.WaitGroup) {
    defer wg.Done()
    
    if depth <= 0 {
        return
    }

    tokens <- struct{}{} // 获取令牌
    urls, err := Fetch(url)
    <-tokens // 释放令牌
    
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("Found %d urls in %s\n", len(urls), url)

    for _, u := range urls {
        wg.Add(1)
        go Crawl(u, depth-1, wg)
    }
}

func Fetch(url string) ([]string, error) {
    // 模拟获取URL数据
    data := map[string][]string{
        "golang.org": {"golang.org/pkg", "golang.org/doc"},
        "golang.org/pkg": {"golang.org/pkg/fmt", "golang.org/pkg/os"},
        "golang.org/doc": {"golang.org/doc/tutorial", "golang.org/doc/effective_go"},
    }
    
    if urls, ok := data[url]; ok {
        return urls, nil
    }
    return nil, fmt.Errorf("error fetching %s", url)
}

func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    go Crawl("golang.org", 4, &wg)
    wg.Wait()
    fmt.Println("All crawling tasks completed")
}

关键修改:

  1. 使用 sync.WaitGroup 来跟踪所有协程的完成状态
  2. 在每个协程开始时调用 wg.Add(1),结束时调用 wg.Done()
  3. 在主函数中调用 wg.Wait() 阻塞直到所有协程完成
  4. 第二个示例还添加了并发限制,避免创建过多协程
回到顶部