在这个练习中,使用 Go 的并发特性来并行,且不重复爬取页面。并将测试的虚假网页内容更换为了真实的网址,测试sina网址通过。
package main
import (
"fmt"
"net"
"net/http"
"sync"
"golang.org/x/net/html"
)
type Fetcher interface {
// Fetch 返回 URL 所指向页面的 body 内容,
// 并将该页面上找到的所有 URL 放到一个切片中。
Fetch(url string) (body string, urls []string, err error)
}
var store sync.Map // 使用 sync.Map 来存储已爬取的 URL
// Crawl 用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。
func Crawl(url string, depth int, fetcher Fetcher) {
// TODO: 不重复爬取页面。
// 下面并没有实现上面两种情况:
if depth <= 0 {
return
}
// 检查 URL 是否已被爬取
if _, loaded := store.LoadOrStore(url, true); loaded {
return
}
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("found: %s %q\n", url, body)
// 创建一个 WaitGroup 来等待所有 goroutine 完成
var wg sync.WaitGroup
for _, u := range urls {
wg.Add(1) // 增加计数
go func(url string) {
defer wg.Done() // 完成时减少计数
Crawl(u, depth-1, fetcher)
}(u)
}
wg.Wait() // 等待所有 goroutine 完成
}
func main() {
fetcher := realFetcher{}
Crawl("https://www.sina.com.cn/", 4, fetcher)
}
type realFetcher struct{}
func (f realFetcher) Fetch(url string) (string, []string, error) {
resp, err := http.Get(url)
if err != nil {
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
panic(fmt.Sprintf("timeout occurred while fetching %s", url))
}
return "", nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", nil, fmt.Errorf("error: status code %d", resp.StatusCode)
}
// 解析 HTML 内容
doc, err := html.Parse(resp.Body)
if err != nil {
return "", nil, err
}
var body string
var urls []string
// 提取 body 内容和 URL
var f2 func(*html.Node)
f2 = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "body" {
if n.FirstChild != nil {
body = n.FirstChild.Data // 获取 body 内容
}
}
if n.Type == html.ElementNode && n.Data == "a" {
for _, attr := range n.Attr {
if attr.Key == "href" {
urls = append(urls, attr.Val) // 获取链接
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
f2(c)
}
}
f2(doc)
return body, urls, nil
}