Go语言快速入门Go语言快速入门
首页
基础篇
进阶篇
高阶篇
实战篇
Go官方网站
编程指南
首页
基础篇
进阶篇
高阶篇
实战篇
Go官方网站
编程指南
  • 实战篇

    • 🚀 实战篇
    • 第1个项目 - 命令行文件管理器
    • 第2个项目 - RESTful API Todo服务
    • 第3个项目 - Web爬虫 新闻采集器
    • 第4个项目 - 实时聊天室 WebSocket
    • 第5个项目 - URL短链接服务
    • 第6个项目 - 完整博客系统

第3个项目 - Web爬虫 新闻采集器

嘿,朋友们!我是长安。

今天我们要做的第三个实战项目是Web爬虫 - 新闻采集器。这个项目会教你如何从网站上抓取数据,是数据采集、数据分析的基础技能。

说实话,爬虫是一个超级实用的技能!无论是采集竞品数据、监控价格变化,还是收集学习资料,都离不开爬虫技术。我自学的时候就经常用爬虫来采集各种教程和资料。Go的并发特性让爬虫效率特别高!

🎯 项目目标

实现一个新闻采集器,支持以下功能:

功能说明技术点
网页抓取获取网页HTML内容HTTP Client
内容解析提取标题、链接、时间goquery
并发爬取同时爬取多个页面Goroutine + Channel
数据存储保存到JSON/CSV文件文件操作
防反爬User-Agent、延迟请求HTTP Header

📁 项目结构

web-scraper/
├── main.go              # 程序入口
├── crawler/
│   ├── crawler.go      # 爬虫核心逻辑
│   └── parser.go       # HTML解析器
├── models/
│   └── news.go         # 新闻数据模型
├── storage/
│   ├── json.go         # JSON存储
│   └── csv.go          # CSV存储
└── go.mod

🚀 第一步:项目初始化

# 创建项目目录
mkdir web-scraper
cd web-scraper

# 初始化Go模块
go mod init github.com/yourusername/web-scraper

# 安装依赖
go get -u github.com/PuerkitoBio/goquery
go get -u github.com/gocolly/colly/v2

# 创建目录结构
mkdir crawler models storage

📦 第二步:定义数据模型

文件: models/news.go

package models

import "time"

// News 新闻结构体
type News struct {
	Title       string    `json:"title"`        // 标题
	Link        string    `json:"link"`         // 链接
	Summary     string    `json:"summary"`      // 摘要
	Author      string    `json:"author"`       // 作者
	PublishTime string    `json:"publish_time"` // 发布时间
	Source      string    `json:"source"`       // 来源
	CrawledAt   time.Time `json:"crawled_at"`   // 爬取时间
}

🕷️ 第三步:实现爬虫核心

文件: crawler/crawler.go

package crawler

import (
	"fmt"
	"log"
	"net/http"
	"sync"
	"time"

	"github.com/PuerkitoBio/goquery"
	"github.com/yourusername/web-scraper/models"
)

// Crawler 爬虫结构体
type Crawler struct {
	client    *http.Client
	userAgent string
	delay     time.Duration
}

// NewCrawler 创建新的爬虫实例
func NewCrawler() *Crawler {
	return &Crawler{
		client: &http.Client{
			Timeout: 30 * time.Second,
		},
		userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
		delay:     2 * time.Second, // 每次请求延迟2秒
	}
}

// FetchPage 获取网页内容
func (c *Crawler) FetchPage(url string) (*goquery.Document, error) {
	// 创建请求
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}

	// 设置User-Agent
	req.Header.Set("User-Agent", c.userAgent)

	// 发送请求
	resp, err := c.client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	// 检查状态码
	if resp.StatusCode != 200 {
		return nil, fmt.Errorf("状态码错误: %d", resp.StatusCode)
	}

	// 解析HTML
	doc, err := goquery.NewDocumentFromReader(resp.Body)
	if err != nil {
		return nil, err
	}

	return doc, nil
}

// CrawlNewsFromHackerNews 爬取Hacker News首页新闻
func (c *Crawler) CrawlNewsFromHackerNews() ([]models.News, error) {
	url := "https://news.ycombinator.com/"
	log.Printf("开始爬取: %s", url)

	doc, err := c.FetchPage(url)
	if err != nil {
		return nil, err
	}

	var newsList []models.News

	// 解析新闻列表
	doc.Find(".athing").Each(func(i int, s *goquery.Selection) {
		// 获取标题和链接
		titleElem := s.Find(".titleline > a").First()
		title := titleElem.Text()
		link, _ := titleElem.Attr("href")

		// 获取下一行的信息(分数、作者等)
		subtext := s.Next()
		score := subtext.Find(".score").Text()
		author := subtext.Find(".hnuser").Text()
		age := subtext.Find(".age").Text()

		news := models.News{
			Title:       title,
			Link:        link,
			Summary:     score,
			Author:      author,
			PublishTime: age,
			Source:      "Hacker News",
			CrawledAt:   time.Now(),
		}

		newsList = append(newsList, news)
	})

	log.Printf("爬取完成,共获取 %d 条新闻", len(newsList))
	return newsList, nil
}

// CrawlMultiplePages 并发爬取多个页面
func (c *Crawler) CrawlMultiplePages(urls []string) []models.News {
	var wg sync.WaitGroup
	newsChan := make(chan []models.News, len(urls))

	// 并发爬取每个URL
	for _, url := range urls {
		wg.Add(1)
		go func(url string) {
			defer wg.Done()

			// 延迟执行,避免频繁请求
			time.Sleep(c.delay)

			// 根据不同网站使用不同的解析方法
			if url == "https://news.ycombinator.com/" {
				newsList, err := c.CrawlNewsFromHackerNews()
				if err != nil {
					log.Printf("爬取失败 %s: %v", url, err)
					return
				}
				newsChan <- newsList
			}
		}(url)
	}

	// 等待所有goroutine完成
	go func() {
		wg.Wait()
		close(newsChan)
	}()

	// 收集所有结果
	var allNews []models.News
	for newsList := range newsChan {
		allNews = append(allNews, newsList...)
	}

	return allNews
}

// CrawlWithRateLimit 带限流的爬取
func (c *Crawler) CrawlWithRateLimit(urls []string, maxConcurrent int) []models.News {
	var wg sync.WaitGroup
	newsChan := make(chan []models.News, len(urls))
	semaphore := make(chan struct{}, maxConcurrent) // 限制并发数

	for _, url := range urls {
		wg.Add(1)
		go func(url string) {
			defer wg.Done()

			// 获取信号量
			semaphore <- struct{}{}
			defer func() { <-semaphore }()

			time.Sleep(c.delay)

			if url == "https://news.ycombinator.com/" {
				newsList, err := c.CrawlNewsFromHackerNews()
				if err != nil {
					log.Printf("爬取失败 %s: %v", url, err)
					return
				}
				newsChan <- newsList
			}
		}(url)
	}

	go func() {
		wg.Wait()
		close(newsChan)
	}()

	var allNews []models.News
	for newsList := range newsChan {
		allNews = append(allNews, newsList...)
	}

	return allNews
}

💾 第四步:实现存储功能

文件: storage/json.go

package storage

import (
	"encoding/json"
	"os"

	"github.com/yourusername/web-scraper/models"
)

// SaveToJSON 保存新闻到JSON文件
func SaveToJSON(newsList []models.News, filename string) error {
	// 创建文件
	file, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer file.Close()

	// 序列化为JSON(格式化输出)
	encoder := json.NewEncoder(file)
	encoder.SetIndent("", "  ")
	
	return encoder.Encode(newsList)
}

// LoadFromJSON 从JSON文件加载新闻
func LoadFromJSON(filename string) ([]models.News, error) {
	file, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	var newsList []models.News
	decoder := json.NewDecoder(file)
	err = decoder.Decode(&newsList)
	
	return newsList, err
}

文件: storage/csv.go

package storage

import (
	"encoding/csv"
	"os"

	"github.com/yourusername/web-scraper/models"
)

// SaveToCSV 保存新闻到CSV文件
func SaveToCSV(newsList []models.News, filename string) error {
	// 创建文件
	file, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer file.Close()

	// 创建CSV writer
	writer := csv.NewWriter(file)
	defer writer.Flush()

	// 写入表头
	headers := []string{"标题", "链接", "摘要", "作者", "发布时间", "来源", "爬取时间"}
	if err := writer.Write(headers); err != nil {
		return err
	}

	// 写入数据
	for _, news := range newsList {
		record := []string{
			news.Title,
			news.Link,
			news.Summary,
			news.Author,
			news.PublishTime,
			news.Source,
			news.CrawledAt.Format("2006-01-02 15:04:05"),
		}
		if err := writer.Write(record); err != nil {
			return err
		}
	}

	return nil
}

🌐 第五步:实现主程序

文件: main.go

package main

import (
	"flag"
	"fmt"
	"log"

	"github.com/yourusername/web-scraper/crawler"
	"github.com/yourusername/web-scraper/storage"
)

func main() {
	// 命令行参数
	outputFormat := flag.String("format", "json", "输出格式 (json/csv)")
	outputFile := flag.String("output", "news.json", "输出文件名")
	concurrent := flag.Int("concurrent", 3, "最大并发数")
	flag.Parse()

	// 创建爬虫
	c := crawler.NewCrawler()

	// 要爬取的URL列表
	urls := []string{
		"https://news.ycombinator.com/",
	}

	log.Println("🕷️  开始爬取新闻...")

	// 并发爬取
	newsList := c.CrawlWithRateLimit(urls, *concurrent)

	if len(newsList) == 0 {
		log.Println("⚠️  没有爬取到任何新闻")
		return
	}

	// 打印前5条新闻
	fmt.Println("\n📰 最新新闻:")
	for i, news := range newsList {
		if i >= 5 {
			break
		}
		fmt.Printf("\n%d. %s\n", i+1, news.Title)
		fmt.Printf("   链接: %s\n", news.Link)
		fmt.Printf("   作者: %s | 时间: %s\n", news.Author, news.PublishTime)
	}

	// 保存到文件
	var err error
	if *outputFormat == "csv" {
		*outputFile = "news.csv"
		err = storage.SaveToCSV(newsList, *outputFile)
	} else {
		*outputFile = "news.json"
		err = storage.SaveToJSON(newsList, *outputFile)
	}

	if err != nil {
		log.Fatalf("❌ 保存失败: %v", err)
	}

	log.Printf("✅ 成功保存 %d 条新闻到 %s", len(newsList), *outputFile)
}

🧪 第六步:运行测试

# 1. 基本运行(默认JSON格式)
go run main.go

# 2. 指定输出格式为CSV
go run main.go -format=csv

# 3. 自定义输出文件名
go run main.go -output=hacker_news.json

# 4. 设置最大并发数
go run main.go -concurrent=5

📊 输出示例

$ go run main.go

2025/12/27 10:30:00 🕷️  开始爬取新闻...
2025/12/27 10:30:00 开始爬取: https://news.ycombinator.com/
2025/12/27 10:30:02 爬取完成,共获取 30 条新闻

📰 最新新闻:

1. Show HN: I built a tool to visualize Git commits
   链接: https://github.com/...
   作者: johndoe | 时间: 2 hours ago

2. The Rise of Rust in Production
   链接: https://example.com/...
   作者: janedoe | 时间: 4 hours ago

...

2025/12/27 10:30:02 ✅ 成功保存 30 条新闻到 news.json

💡 扩展思考

1. 支持更多网站

添加不同网站的解析器:

// 爬取Reddit
func (c *Crawler) CrawlReddit() ([]models.News, error) {
	// 实现Reddit解析逻辑
}

// 爬取GitHub Trending
func (c *Crawler) CrawlGitHubTrending() ([]models.News, error) {
	// 实现GitHub解析逻辑
}

2. 增强反爬能力

  • 随机User-Agent
  • 使用代理IP
  • 添加Cookies管理
  • 模拟浏览器行为
var userAgents = []string{
	"Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
	"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...",
	"Mozilla/5.0 (X11; Linux x86_64)...",
}

func randomUserAgent() string {
	return userAgents[rand.Intn(len(userAgents))]
}

3. 数据去重

使用哈希表去重:

func removeDuplicates(newsList []models.News) []models.News {
	seen := make(map[string]bool)
	unique := []models.News{}
	
	for _, news := range newsList {
		if !seen[news.Link] {
			seen[news.Link] = true
			unique = append(unique, news)
		}
	}
	
	return unique
}

4. 定时任务

使用cron实现定时爬取:

go get -u github.com/robfig/cron/v3
c := cron.New()
c.AddFunc("0 */6 * * *", func() {
	// 每6小时爬取一次
	crawlNews()
})
c.Start()

💪 练习题

  1. 实现爬取GitHub Trending页面,提取项目名称、Star数、描述
  2. 添加失败重试机制,失败时自动重试3次
  3. 实现增量爬取,只爬取新发布的内容
  4. 添加日志记录,记录每次爬取的统计信息
💡 参考答案
// 失败重试示例
func (c *Crawler) fetchWithRetry(url string, maxRetries int) (*goquery.Document, error) {
	var doc *goquery.Document
	var err error
	
	for i := 0; i < maxRetries; i++ {
		doc, err = c.FetchPage(url)
		if err == nil {
			return doc, nil
		}
		
		log.Printf("重试 %d/%d: %v", i+1, maxRetries, err)
		time.Sleep(time.Duration(i+1) * time.Second)
	}
	
	return nil, fmt.Errorf("重试%d次后仍然失败: %v", maxRetries, err)
}

⚠️ 法律与道德

使用爬虫时请注意:

  1. 遵守robots.txt: 查看网站的爬虫协议
  2. 控制频率: 不要给服务器造成压力
  3. 尊重版权: 不要用于商业目的
  4. 合理使用: 仅用于学习和个人研究

🎯 小结

恭喜你完成了第三个实战项目!通过这个项目,你学会了:

✅ HTTP请求的发送和处理
✅ HTML内容的解析和提取
✅ 并发爬取提高效率
✅ 数据的存储和导出
✅ 反爬虫策略的应对

下一个项目我们将实现实时聊天室,学习WebSocket和实时通信!

下一步: 第4个项目 - 实时聊天室 WebSocket →


💬 遇到问题了吗?

  • 确保网络连接正常
  • 某些网站可能需要登录或验证
  • 注意网站结构可能会变化
  • 访问 goquery文档 了解更多
最近更新: 2025/12/27 13:26
Contributors: 王长安
Prev
第2个项目 - RESTful API Todo服务
Next
第4个项目 - 实时聊天室 WebSocket