昨天研究了一下,关于基于Go1.19的站点模板爬虫,分享一下代码和记录,希望可以帮助大家:
package main
import (
"fmt"
"net/http"
"io/ioutil"
"log"
"golang.org/x/net/html"
"strings"
"sync"
)
// URLVisited是一个包含已访问URL的集合,防止重复访问
var URLVisited = make(map[string]bool)
var mu sync.Mutex
// FetchURL 负责从给定的URL获取内容
func FetchURL(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("error: status code %d", resp.StatusCode)
}
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(bodyBytes), nil
}
// ParseHTML 解析HTML内容并提取所有链接
func ParseHTML(body string) ([]string, error) {
doc, err := html.Parse(strings.NewReader(body))
if err != nil {
return nil, err
}
var links []string
var f func(*html.Node)
f = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "a" {
for _, attr := range n.Attr {
if attr.Key == "href" {
link := attr.Val
if strings.HasPrefix(link, "http") {
links = append(links, link)
}
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
f(c)
}
}
f(doc)
return links, nil
}
// Crawl 开始爬取给定的URL,并递归爬取所有发现的链接
func Crawl(url string, wg *sync.WaitGroup) {
defer wg.Done()
mu.Lock()
if URLVisited[url] {
mu.Unlock()
return
}
URLVisited[url] = true
mu.Unlock()
fmt.Println("Crawling:", url)
body, err := FetchURL(url)
if err != nil {
log.Println("Error fetching URL:", err)
return
}
links, err := ParseHTML(body)
if err != nil {
log.Println("Error parsing HTML:", err)
return
}
for _, link := range links {
wg.Add(1)
go Crawl(link, wg)
}
}
func main() {
var wg sync.WaitGroup
startURL := "https://example.com"
wg.Add(1)
go Crawl(startURL, &wg)
wg.Wait()
fmt.Println("Crawling finished.")
}
代码解释
主函数(main)
func main() {
var wg sync.WaitGroup
startURL := "https://example.com"
wg.Add(1)
go Crawl(startURL, &wg)
wg.Wait()
fmt.Println("Crawling finished.")
}
主函数定义了一个起始URL,并使用sync.WaitGroup
来跟踪并发的爬取任务。wg.Add(1)
用于增加等待组的计数,go Crawl(startURL, &wg)
则启动了一个新的goroutine来爬取起始URL。wg.Wait()
等待所有爬取任务完成后,打印“Crawling finished.”消息。
Crawl函数
func Crawl(url string, wg *sync.WaitGroup) {
defer wg.Done()
mu.Lock()
if URLVisited[url] {
mu.Unlock()
return
}
URLVisited[url] = true
mu.Unlock()
fmt.Println("Crawling:", url)
body, err := FetchURL(url)
if err != nil {
log.Println("Error fetching URL:", err)
return
}
links, err := ParseHTML(body)
if err != nil {
log.Println("Error parsing HTML:", err)
return
}
for _, link := range links {
wg.Add(1)
go Crawl(link, wg)
}
}
Crawl
函数是递归爬虫的核心。它接收一个URL和一个等待组指针。首先,它检查URL是否已经被访问过,如果是则直接返回。否则,它将URL标记为已访问,并获取页面内容。接着,解析HTML并提取所有链接,为每个链接启动新的goroutine来继续爬取。
FetchURL函数
func FetchURL(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("error: status code %d", resp.StatusCode)
}
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(bodyBytes), nil
}
FetchURL
函数负责获取给定URL的内容。它使用http.Get
发送HTTP请求,并检查响应状态码是否为200(OK)。如果响应状态码不是200,则返回错误。然后读取响应体并将其转换为字符串。
ParseHTML函数
func ParseHTML(body string) ([]string, error) {
doc, err := html.Parse(strings.NewReader(body))
if err != nil {
return nil, err
}
var links []string
var f func(*html.Node)
f = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "a" {
for _, attr := range n.Attr {
if attr.Key == "href" {
link := attr.Val
if strings.HasPrefix(link, "http") {
links = append(links, link)
}
}
}
}
for c := n.FirstChild; c != nil; c = n.FirstChild {
f(c)
}
}
f(doc)
return links, nil
}
ParseHTML
函数解析给定的HTML内容并提取所有链接。它使用golang.org/x/net/html
包来解析HTML,并使用递归函数遍历节点树。当发现<a>
元素时,提取其href
属性中的链接并将其添加到链接列表中。
互斥锁
为了确保多线程访问URLVisited
集合时的数据一致性,我们使用sync.Mutex
。在访问和修改URLVisited
时,我们使用mu.Lock()
和mu.Unlock()
来锁定和解锁集合。