第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()
💪 练习题
- 实现爬取GitHub Trending页面,提取项目名称、Star数、描述
- 添加失败重试机制,失败时自动重试3次
- 实现增量爬取,只爬取新发布的内容
- 添加日志记录,记录每次爬取的统计信息
💡 参考答案
// 失败重试示例
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)
}
⚠️ 法律与道德
使用爬虫时请注意:
- 遵守robots.txt: 查看网站的爬虫协议
- 控制频率: 不要给服务器造成压力
- 尊重版权: 不要用于商业目的
- 合理使用: 仅用于学习和个人研究
🎯 小结
恭喜你完成了第三个实战项目!通过这个项目,你学会了:
✅ HTTP请求的发送和处理
✅ HTML内容的解析和提取
✅ 并发爬取提高效率
✅ 数据的存储和导出
✅ 反爬虫策略的应对
下一个项目我们将实现实时聊天室,学习WebSocket和实时通信!
下一步: 第4个项目 - 实时聊天室 WebSocket →
💬 遇到问题了吗?
- 确保网络连接正常
- 某些网站可能需要登录或验证
- 注意网站结构可能会变化
- 访问 goquery文档 了解更多
