Skip to content

大致介绍

Crawler 并不是该课程的 Lab,只是第二节课里讲述并发 IO 和互斥锁的一个例子。

但是这个例子很重要,它涉及并发编程的核心思想,即锁和线程通信。

这个例子里使用里共享内存和管道通信两种方式来实现网页爬取任务。

具体代码

crawler.go

该方法采取共享内存,对 Map 使用锁来避免死锁

go
package main

import (
	"fmt"
	"sync"
)

// 网页获取器接口
type Fetcher interface {
	Fetch(url string) (body string, urls []string, err error)
}

// 已访问 URL 集合(带互斥锁保护)
var visited = struct {
	sync.Mutex
	m map[string]bool
}{m: make(map[string]bool)}

// 递归爬取网页
func Crawl(url string, depth int, fetcher Fetcher, wg *sync.WaitGroup) {
	defer wg.Done()

	if depth <= 0 {
		return
	}

	// 检查是否已访问,去重
	visited.Lock()
	if visited.m[url] {
		visited.Unlock()
		return
	}
	visited.m[url] = true
	visited.Unlock()

	body, urls, err := fetcher.Fetch(url)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Printf("found %s %q\n", url, body)

	// 并发爬取子链接
	for _, url := range urls {
		wg.Add(1)
		go Crawl(url, depth+1, fetcher, wg)
	}
}

func main() {
	var wg sync.WaitGroup
	wg.Add(1)
	go Crawl("https://golang.org/", 3, fetcher, &wg)
	wg.Wait()
}

// 模拟获取器
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)
}

// 模拟数据
var fetcher = fakeFetcher{
	"https://golang.org/": {
		"The Go Programming Language",
		[]string{
			"https://golang.org/pkg/",
			"https://golang.org/cmd/",
		},
	},
	"https://golang.org/pkg/": {
		"Packages",
		[]string{
			"https://golang.org/",
			"https://golang.org/cmd/",
			"https://golang.org/pkg/fmt/",
			"https://golang.org/pkg/os/",
		},
	},
	"https://golang.org/pkg/fmt/": {
		"Package fmt",
		[]string{
			"https://golang.org/",
			"https://golang.org/pkg/",
		},
	},
	"https://golang.org/pkg/os/": {
		"Package os",
		[]string{
			"https://golang.org/",
			"https://golang.org/pkg/",
		},
	},
}

crawler_chan.go

该方法使用 channel 通信来共享内存

go
package main

import (
	"fmt"
	"sync"
)

// 爬取任务
type fetchTask struct {
	url   string
	depth int
}

// 使用 Channel 实现的并发爬虫(去掉了显式锁)
func CrawlChan(url string, depth int, fetcher Fetcher) {
	tasks := make(chan fetchTask)
	done := make(chan struct{})

	// 统一管理 visited 状态,无需加锁
	go func() {
		visited := make(map[string]bool)
		var wg sync.WaitGroup

		// 提交任务
		submit := func(u string, d int) {
			if d <= 0 || visited[u] {
				return
			}
			visited[u] = true
			wg.Add(1)

			go func() {
				defer wg.Done()
				body, urls, err := fetcher.Fetch(u)
				if err != nil {
					fmt.Println(err)
					return
				}
				fmt.Printf("found %s %q\n", u, body)
				for _, next := range urls {
					tasks <- fetchTask{next, d - 1}
				}
			}()
		}

		// 从 channel 读取新任务
		go func() {
			for t := range tasks {
				submit(t.url, t.depth)
			}
		}()

		submit(url, depth)
		wg.Wait()
		close(tasks)
		close(done)
	}()

	<-done
}

crawler_test.go

测试文件

go
package main

import (
	"sync"
	"testing"
)

// 测试 Crawl 是否正常爬取
func TestCrawl(t *testing.T) {
	var wg sync.WaitGroup
	wg.Add(1)
	go Crawl("https://golang.org/", 3, fetcher, &wg)
	wg.Wait()

	visited.Lock()
	_, ok := visited.m["https://golang.org/"]
	visited.Unlock()

	if !ok {
		t.Errorf("expected https://golang.org/ to be visited")
	}
}

测试结果

![[Pasted image 20260621234550.png]]