go练习实现简单爬虫代码

58 阅读1分钟

在这个练习中,使用 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
}