Go编写的Web crawler仅爬取少量图片无报错停止问题求助
问题根因排查
异常1:下载图片不在目标站点首页存在的根因
- 缺失首页图片处理逻辑:main函数拿到首页DOM后直接遍历a标签爬取子页面,完全没有提取首页本身的图片资源
- 未做同站域名校验:所有http开头的链接都会被爬取,包含跳转至外站的链接,自然会下载到大量目标站点不存在的外站图片
- 变量遮蔽逻辑混乱:全局变量
currWebsite被processElement内的局部变量同名覆盖,站点根域名判断逻辑失效
异常2:爬虫中途无报错停止的根因
- 无完整的链接遍历逻辑:原代码仅爬取「首页→首页外链」两层页面,子页面内的新链接完全没有采集,两层资源爬取完成后程序直接正常退出
- 错误处理滥用
log.Fatal:该方法会打印日志后直接终止整个进程,任意页面请求、图片请求出现错误(如404、超时、资源不存在)都会直接退出程序,部分场景错误日志被冲刷后会表现为无报错停止 - 缺少HTTP请求超时配置:默认
http.Client无超时限制,遇到响应极慢的站点会无限阻塞,看起来像程序停止 - 无链接去重机制:重复爬取相同链接浪费资源,出现循环链接时会导致程序无限卡住
解决方案
- 补充首页图片下载逻辑,统一封装页面处理函数,爬取任意页面时先处理当前页图片再提取链接
- 新增同站点域名校验,仅爬取目标根域名下的链接,避免爬取外站资源
- 引入待爬队列+已爬去重map,实现完整的遍历逻辑,直到所有符合要求的链接处理完成
- 替换所有
log.Fatal为普通错误打印,单个资源请求失败不终止整个程序 - 自定义带超时的
http.Client,避免无限阻塞 - 移除无用全局变量,避免变量遮蔽问题,可根据图片实际后缀生成文件名提升兼容性
修正后完整代码
package main import ( "fmt" "io" "log" "net/http" "os" "strconv" "strings" "time" "github.com/PuerkitoBio/goquery" ) var ( rootDomain string = "https://www.youtube.com" imageCount int = 0 visited = make(map[string]bool) // 配置带10秒超时的HTTP客户端,避免无限阻塞 httpClient = &http.Client{Timeout: 10 * time.Second} ) // 爬取单个页面,返回页面内符合要求的同站未爬链接,同时下载当前页所有图片 func crawlPage(pageUrl string) ([]string, error) { var newLinks []string // 请求页面 response, err := httpClient.Get(pageUrl) if err != nil { log.Printf("页面[%s]请求失败:%v,跳过该页面", pageUrl, err) return newLinks, err } defer response.Body.Close() if response.StatusCode != 200 { log.Printf("页面[%s]响应状态码异常:%d,跳过该页面", pageUrl, response.StatusCode) return newLinks, fmt.Errorf("status code %d", response.StatusCode) } // 解析DOM document, err := goquery.NewDocumentFromReader(response.Body) if err != nil { log.Printf("页面[%s]DOM解析失败:%v,跳过该页面", pageUrl, err) return newLinks, err } // 第一步:下载当前页所有图片 document.Find("img").Each(func(index int, element *goquery.Selection) { imgSrc, exists := element.Attr("src") if !exists || !strings.HasPrefix(imgSrc, "http") { return } // 可根据imgSrc提取真实后缀,此处保持原逻辑兼容 fileName := fmt.Sprintf("./images/img%d.jpg", imageCount) fmt.Println("[+] 下载图片:", imgSrc) if err := DownloadFile(fileName, imgSrc); err == nil { imageCount++ } }) // 第二步:提取当前页所有同站未爬链接 document.Find("a").Each(func(index int, element *goquery.Selection) { href, exists := element.Attr("href") if !exists { return } // 仅保留同站链接、未爬取过的链接 if strings.HasPrefix(href, rootDomain) && !visited[href] { newLinks = append(newLinks, href) visited[href] = true } }) return newLinks, nil } func main() { // 创建图片存储目录 err := os.MkdirAll("./images/", 0777) if err != nil { log.Fatalln("图片目录创建失败:", err) } // 待爬队列,初始加入根域名 var crawlQueue []string crawlQueue = append(crawlQueue, rootDomain) visited[rootDomain] = true // 循环处理队列直到为空 for len(crawlQueue) > 0 { // 取出队首元素 currentUrl := crawlQueue[0] crawlQueue = crawlQueue[1:] log.Println("当前处理页面:", currentUrl) // 爬取页面,获取新链接 newLinks, err := crawlPage(currentUrl) if err != nil { continue } // 新链接加入队列 crawlQueue = append(crawlQueue, newLinks...) } log.Println("所有页面处理完成,累计下载图片:", imageCount) } func DownloadFile(filepath string, url string) error { response, err := httpClient.Get(url) if err != nil { log.Printf("图片[%s]请求失败:%v,跳过", url, err) return err } defer response.Body.Close() if response.StatusCode != 200 { log.Printf("图片[%s]响应状态码异常:%d,跳过", url, response.StatusCode) return fmt.Errorf("status code %d", response.StatusCode) } file, err := os.Create(filepath) if err != nil { log.Printf("图片[%s]本地文件创建失败:%v,跳过", url, err) return err } defer file.Close() _, err = io.Copy(file, response.Body) if err != nil { log.Printf("图片[%s]内容写入失败:%v,跳过", url, err) return err } return nil }
内容的提问来源于stack exchange,提问作者worrath
相关产品推荐
相关产品推荐

