You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Go编写的Web crawler仅爬取少量图片无报错停止问题求助

问题根因排查

异常1:下载图片不在目标站点首页存在的根因

  • 缺失首页图片处理逻辑:main函数拿到首页DOM后直接遍历a标签爬取子页面,完全没有提取首页本身的图片资源
  • 未做同站域名校验:所有http开头的链接都会被爬取,包含跳转至外站的链接,自然会下载到大量目标站点不存在的外站图片
  • 变量遮蔽逻辑混乱:全局变量currWebsite被processElement内的局部变量同名覆盖,站点根域名判断逻辑失效

异常2:爬虫中途无报错停止的根因

  • 无完整的链接遍历逻辑:原代码仅爬取「首页→首页外链」两层页面,子页面内的新链接完全没有采集,两层资源爬取完成后程序直接正常退出
  • 错误处理滥用log.Fatal:该方法会打印日志后直接终止整个进程,任意页面请求、图片请求出现错误(如404、超时、资源不存在)都会直接退出程序,部分场景错误日志被冲刷后会表现为无报错停止
  • 缺少HTTP请求超时配置:默认http.Client无超时限制,遇到响应极慢的站点会无限阻塞,看起来像程序停止
  • 无链接去重机制:重复爬取相同链接浪费资源,出现循环链接时会导致程序无限卡住

解决方案
  1. 补充首页图片下载逻辑,统一封装页面处理函数,爬取任意页面时先处理当前页图片再提取链接
  2. 新增同站点域名校验,仅爬取目标根域名下的链接,避免爬取外站资源
  3. 引入待爬队列+已爬去重map,实现完整的遍历逻辑,直到所有符合要求的链接处理完成
  4. 替换所有log.Fatal为普通错误打印,单个资源请求失败不终止整个程序
  5. 自定义带超时的http.Client,避免无限阻塞
  6. 移除无用全局变量,避免变量遮蔽问题,可根据图片实际后缀生成文件名提升兼容性

修正后完整代码
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.12 01:57:02