📚 原创系列: “Go语言爬虫系列”
🔄 转载说明: 本文最初发布于"Gopher部落"微信公众号,经原作者授权转载。
🔗 关注原创: 欢迎扫描文末二维码,关注"Gopher部落"微信公众号获取第一手Go技术文章。
📑 Go语言爬虫系列导航
🚀 Go爬虫系列:共14篇本文是【Go语言爬虫系列】的第4篇,点击下方链接查看更多文章
- 爬虫入门与Colly框架基础
- HTML解析与Goquery技术详解
- 并发控制与错误处理
- 数据存储与导出 👈 当前位置
- 反爬虫策略应对技术
- 模拟登录与会话维持
- 分布式爬虫架构
- JavaScript渲染页面抓取
- 移动应用数据抓取
- 爬虫性能优化技术
- 爬虫数据分析与应用
- 爬虫系统安全与伦理
- 爬虫系统监控与运维
- 综合项目实战:新闻聚合系统 ⏳ 开发中 - 关注公众号获取发布通知!
📢 特别提示:《综合项目实战:新闻聚合系统》正在精心制作中!这将是一个完整的实战项目,带您从零构建一个多站点新闻聚合系统。扫描文末二维码关注公众号并回复「新闻聚合」,获取项目发布通知和源码下载链接!
📖 文章导读
在前三篇文章中,我们学习了爬虫的基础知识、HTML解析技术以及并发控制与错误处理。获取数据后,如何高效存储和管理这些数据成为关键问题。本文作为系列的第四篇,将深入探讨爬虫数据的存储与导出技术,主要内容包括:
- 爬虫数据存储方案的比较与选择
- 文件系统存储实现(CSV、JSON、XML)
- 关系型数据库存储(MySQL)实战
- NoSQL数据库(MongoDB)在爬虫中的应用
- 数据导出与格式转换的有效方法
- 爬虫数据的备份与恢复策略
通过本文的学习,您将能够为爬虫系统选择最适合的数据存储方案,并掌握数据管理的最佳实践,构建完整的爬虫数据处理流程。
一、爬虫数据存储方案比较
1.1 数据存储的重要性与挑战
在爬虫系统中,数据存储是连接数据采集和数据分析的关键环节。一个优秀的存储方案应当能够:
- 高效处理大量数据:爬虫可能在短时间内采集海量信息
- 支持多样化数据结构:不同网站的数据结构差异很大
- 便于数据查询与分析:支持灵活的检索和统计功能
- 确保数据一致性:防止重复或丢失数据
- 具备良好扩展性:能够随着数据量增长进行扩展
然而,爬虫数据存储也面临着特殊挑战:
- 数据量大且增长快:某些爬虫项目可能每天产生GB甚至TB级数据
- 数据结构多变:网站结构可能随时变化,存储方案需要适应这种变化
- 读写性能平衡:既要满足高速写入,又要支持高效查询
- 数据清洗需求:原始数据往往需要清洗处理后再存储
- 资源限制:受限于服务器存储空间、内存和网络带宽
1.2 主流存储方案对比
根据项目需求不同,可以选择不同类型的存储方案。以下是主流存储方案的对比:
文件系统存储
格式 | 优势 | 劣势 | 适用场景 |
---|---|---|---|
CSV | 简单易用,兼容性好,适合表格数据 | 不支持复杂结构,大文件处理困难 | 结构简单的表格数据,小型爬虫项目 |
JSON | 支持嵌套结构,可读性好,语言无关 | 空间效率较低,查询性能一般 | 结构多变的数据,中小型项目 |
XML | 结构严格,支持命名空间和验证 | 冗余大,解析开销大 | 需要严格结构验证的场景 |
二进制 | 存储效率高,读写速度快 | 可读性差,通用性低 | 需要高性能的特定场景 |
数据库存储
类型 | 代表产品 | 优势 | 劣势 | 适用场景 |
---|---|---|---|---|
关系型数据库 | MySQL, PostgreSQL | 事务支持,强一致性,成熟稳定 | 扩展性受限,不适合非结构化数据 | 结构稳定的数据,需要复杂查询 |
文档型数据库 | MongoDB, CouchDB | 灵活的数据模型,易于扩展 | 事务支持有限,索引占空间 | 结构多变的数据,需要快速迭代 |
键值存储 | Redis, LevelDB | 极高的读写性能,易于扩展 | 查询能力有限,功能相对简单 | 缓存,队列,会话存储 |
列式数据库 | Cassandra, HBase | 优秀的写入性能,高可扩展性 | 实现复杂,学习曲线陡峭 | 超大规模数据,时间序列数据 |
1.3 存储方案选择策略
选择合适的存储方案应考虑以下因素:
-
数据规模:小型项目(<1GB)可考虑文件存储,中型项目(1-100GB)适合单机数据库,大型项目(>100GB)需考虑分布式解决方案
-
数据结构:
- 结构化数据(表格型):关系型数据库或CSV
- 半结构化数据(嵌套复杂):文档数据库或JSON
- 非结构化数据(图片、文本):专用存储系统或文件系统
-
查询需求:
- 复杂关联查询:关系型数据库
- 简单键值查询:键值存储
- 全文检索:搜索引擎(如Elasticsearch)
-
性能要求:
- 高并发写入:键值存储、列式数据库
- 快速读取:内存数据库、良好索引的关系型数据库
- 批量处理:支持批处理的数据库或文件系统
-
开发资源:
- 团队熟悉度
- 维护成本
- 社区支持
1.4 数据存储与系统架构的关系
存储方案的选择应与整个爬虫系统的架构协调一致:
┌─────────────────┐ ┌───────────────────┐ ┌─────────────────┐
│ │ │ │ │ │
│ 数据采集模块 │─────▶│ 数据存储系统 │─────▶│ 数据分析模块 │
│ │ │ │ │ │
└─────────────────┘ └───────────────────┘ └─────────────────┘
│
│
┌─────────▼──────────┐
│ │
│ 数据导出/备份 │
│ │
└────────────────────┘
在实际项目中,可能需要组合使用多种存储方案:
- 使用Redis作为URL队列和临时缓存
- 使用MongoDB存储原始爬取数据
- 使用MySQL存储清洗后的结构化数据
- 使用文件系统存储大型二进制数据(图片、文档等)
接下来,我们将依次介绍各种存储方案的具体实现方法。
二、文件系统存储实现
文件系统存储是最基础的数据持久化方式,适用于小型爬虫项目或原型开发。Go语言提供了丰富的文件操作API,可以轻松实现各种格式的文件存储。
2.1 CSV文件存储
CSV(逗号分隔值)是一种简单的表格数据格式,特别适合存储结构化、表格型数据。Go语言标准库提供了encoding/csv
包用于CSV文件的读写操作。
基本写入示例
package main
import (
"encoding/csv"
"log"
"os"
)
type Product struct {
ID string
Name string
Price string
Category string
URL string
}
func main() {
// 准备数据
products := []Product{
{ID: "1001", Name: "笔记本电脑", Price: "4999.00", Category: "电子产品", URL: "https://example.com/p/1001"},
{ID: "1002", Name: "机械键盘", Price: "399.00", Category: "电脑配件", URL: "https://example.com/p/1002"},
{ID: "1003", Name: "无线鼠标", Price: "129.00", Category: "电脑配件", URL: "https://example.com/p/1003"},
}
// 创建文件
file, err := os.Create("products.csv")
if err != nil {
log.Fatalf("无法创建文件: %v", err)
}
defer file.Close()
// 创建CSV写入器
writer := csv.NewWriter(file)
defer writer.Flush()
// 写入表头
header := []string{"ID", "Name", "Price", "Category", "URL"}
if err := writer.Write(header); err != nil {
log.Fatalf("写入表头失败: %v", err)
}
// 写入数据
for _, product := range products {
record := []string{
product.ID,
product.Name,
product.Price,
product.Category,
product.URL,
}
if err := writer.Write(record); err != nil {
log.Fatalf("写入记录失败: %v", err)
}
}
log.Println("数据已成功写入 products.csv")
}
CSV数据读取
func readCSVFile(filename string) ([]Product, error) {
// 打开文件
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
// 创建CSV读取器
reader := csv.NewReader(file)
// 读取表头(如果存在)
header, err := reader.Read()
if err != nil {
return nil, err
}
log.Printf("表头: %v", header)
// 读取所有记录
records, err := reader.ReadAll()
if err != nil {
return nil, err
}
// 转换为Product结构
products := make([]Product, 0, len(records))
for _, record := range records {
if len(record) == 5 { // 确保记录有足够的字段
product := Product{
ID: record[0],
Name: record[1],
Price: record[2],
Category: record[3],
URL: record[4],
}
products = append(products, product)
}
}
return products, nil
}
实用的CSV存储封装
为了更方便地使用CSV存储,我们可以创建一个简单的封装:
type CSVStorage struct {
Filename string
Delimiter rune
file *os.File
writer *csv.Writer
}
func NewCSVStorage(filename string) (*CSVStorage, error) {
file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return nil, err
}
writer := csv.NewWriter(file)
writer.Comma = ',' // 默认分隔符
return &CSVStorage{
Filename: filename,
Delimiter: ',',
file: file,
writer: writer,
}, nil
}
func (s *CSVStorage) SetDelimiter(delimiter rune) {
s.Delimiter = delimiter
s.writer.Comma = delimiter
}
func (s *CSVStorage) WriteHeader(header []string) error {
return s.writer.Write(header)
}
func (s *CSVStorage) WriteRecord(record []string) error {
return s.writer.Write(record)
}
func (s *CSVStorage) WriteRecords(records [][]string) error {
return s.writer.WriteAll(records)
}
func (s *CSVStorage) Flush() {
s.writer.Flush()
}
func (s *CSVStorage) Close() error {
s.writer.Flush()
return s.file.Close()
}
CSV存储的优缺点:
- 优点:简单易用,兼容性极好,可直接导入Excel等工具
- 缺点:不支持嵌套结构,不适合复杂数据类型,大文件处理性能较差
2.2 JSON文件存储
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,支持复杂的嵌套结构,非常适合存储爬虫采集的半结构化数据。Go语言标准库提供了encoding/json
包用于JSON处理。
基本写入示例
package main
import (
"encoding/json"
"io/ioutil"
"log"
"os"
)
type Review struct {
Content string
Rating int
Author string
Date string
}
type Product struct {
ID string
Name string
Price float64
Description string
Category string
URL string
Images []string
Specs map[string]string
Reviews []Review
}
func main() {
// 准备数据
products := []Product{
{
ID: "1001",
Name: "高性能笔记本电脑",
Price: 4999.99,
Description: "最新一代处理器,16GB内存,512GB固态硬盘",
Category: "电子产品",
URL: "https://example.com/p/1001",
Images: []string{"img1.jpg", "img2.jpg", "img3.jpg"},
Specs: map[string]string{
"处理器": "Intel Core i7",
"内存": "16GB DDR4",
"存储": "512GB SSD",
"显卡": "NVIDIA RTX 3060",
},
Reviews: []Review{
{Content: "非常好用的笔记本", Rating: 5, Author: "用户A", Date: "2023-01-15"},
{Content: "性价比很高", Rating: 4, Author: "用户B", Date: "2023-02-20"},
},
},
// 可以添加更多产品...
}
// 转换为JSON
jsonData, err := json.MarshalIndent(products, "", " ")
if err != nil {
log.Fatalf("JSON编码失败: %v", err)
}
// 写入文件
err = ioutil.WriteFile("products.json", jsonData, 0644)
if err != nil {
log.Fatalf("写入文件失败: %v", err)
}
log.Println("数据已成功写入 products.json")
}
JSON数据读取
func readJSONFile(filename string) ([]Product, error) {
// 读取文件
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
// 解析JSON
var products []Product
err = json.Unmarshal(data, &products)
if err != nil {
return nil, err
}
return products, nil
}
流式JSON处理
对于大型JSON文件,可以使用流式处理避免一次性加载整个文件到内存:
func streamWriteJSON(filename string, products []Product) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
// 创建JSON编码器
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
// 写入JSON数组开始标记
file.WriteString("[\n")
// 依次写入每个产品
for i, product := range products {
// 编码单个产品
productJSON, err := json.MarshalIndent(product, " ", " ")
if err != nil {
return err
}
// 写入产品JSON
file.Write(productJSON)
// 添加逗号,除非是最后一个元素
if i < len(products)-1 {
file.WriteString(",\n")
} else {
file.WriteString("\n")
}
}
// 写入JSON数组结束标记
file.WriteString("]\n")
return nil
}
func streamReadJSON(filename string, callback func(product Product) error) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
// 创建JSON解码器
decoder := json.NewDecoder(file)
// 读取开始的数组标记
_, err = decoder.Token()
if err != nil {
return err
}
// 循环解码每个产品
for decoder.More() {
var product Product
err := decoder.Decode(&product)
if err != nil {
return err
}
// 调用回调函数处理产品
err = callback(product)
if err != nil {
return err
}
}
// 读取结束的数组标记
_, err = decoder.Token()
if err != nil {
return err
}
return nil
}
JSON存储的优缺点:
- 优点:格式灵活,支持复杂数据结构,可读性好,语言无关
- 缺点:相比二进制格式占用空间大,解析性能一般,不支持局部更新
2.3 XML文件存储
XML(可扩展标记语言)是一种结构严格的标记语言,支持复杂的层次结构和数据验证。Go语言标准库提供了encoding/xml
包用于XML处理。
基本写入示例
package main
import (
"encoding/xml"
"io/ioutil"
"log"
"os"
)
// 定义XML结构
type Image struct {
URL string `xml:"url,attr"`
Type string `xml:"type,attr,omitempty"`
}
type Specification struct {
Name string `xml:"name,attr"`
Value string `xml:",chardata"`
}
type Review struct {
Author string `xml:"author"`
Rating int `xml:"rating"`
Content string `xml:"content"`
Date string `xml:"date"`
}
type Product struct {
XMLName xml.Name `xml:"product"`
ID string `xml:"id,attr"`
Name string `xml:"name"`
Price float64 `xml:"price"`
Description string `xml:"description"`
Category string `xml:"category"`
URL string `xml:"url"`
Images []Image `xml:"images>image"`
Specs []Specification `xml:"specifications>spec"`
Reviews []Review `xml:"reviews>review"`
}
type ProductCatalog struct {
XMLName xml.Name `xml:"catalog"`
Products []Product `xml:"product"`
}
func main() {
// 准备数据
catalog := ProductCatalog{
Products: []Product{
{
ID: "1001",
Name: "高性能笔记本电脑",
Price: 4999.99,
Description: "最新一代处理器,16GB内存,512GB固态硬盘",
Category: "电子产品",
URL: "https://example.com/p/1001",
Images: []Image{
{URL: "https://example.com/img/1001-1.jpg"},
{URL: "https://example.com/img/1001-2.jpg", Type: "detail"},
},
Specs: []Specification{
{Name: "处理器", Value: "Intel Core i7"},
{Name: "内存", Value: "16GB DDR4"},
{Name: "存储", Value: "512GB SSD"},
},
Reviews: []Review{
{Content: "非常好用的笔记本", Rating: 5, Author: "用户A", Date: "2023-01-15"},
{Content: "性价比很高", Rating: 4, Author: "用户B", Date: "2023-02-20"},
},
},
// 可以添加更多产品...
},
}
// 创建文件
file, err := os.Create("products.xml")
if err != nil {
log.Fatalf("无法创建文件: %v", err)
}
defer file.Close()
// 写入XML头
file.WriteString(xml.Header)
// 创建编码器
encoder := xml.NewEncoder(file)
encoder.Indent("", " ")
// 编码并写入
if err := encoder.Encode(catalog); err != nil {
log.Fatalf("XML编码失败: %v", err)
}
log.Println("数据已成功写入 products.xml")
}
XML数据读取
func readXMLFile(filename string) (*ProductCatalog, error) {
// 读取文件
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
// 解析XML
var catalog ProductCatalog
err = xml.Unmarshal(data, &catalog)
if err != nil {
return nil, err
}
return &catalog, nil
}
XML存储的优缺点:
- 优点:结构严格,支持命名空间和验证,适合EDI等正式场合
- 缺点:文件体积大,解析开销高,配置复杂,近年来逐渐被JSON取代
三、关系型数据库存储(MySQL)
关系型数据库是存储结构化数据的经典解决方案,提供了事务支持、参照完整性和复杂查询能力。在爬虫系统中,关系型数据库特别适合存储经过清洗和结构化的数据。
3.1 MySQL数据库设计原则
在爬虫项目中使用MySQL等关系型数据库时,应遵循以下设计原则:
-
合理设计表结构:
- 适当拆分表,避免过多无关字段
- 为大字段(如文章内容)单独建表
- 使用适当的字段类型和长度
-
建立合适的索引:
- 对频繁查询的字段建立索引
- 避免过度索引影响写入性能
- 考虑复合索引优化复杂查询
-
处理重复数据:
- 使用唯一键约束防止重复数据
- 实现"更新或插入"(UPSERT)逻辑
-
批量操作:
- 使用事务和批量插入提高性能
- 控制每批次的记录数量
3.2 连接MySQL数据库
Go语言中连接MySQL主要使用database/sql
包和MySQL驱动。最常用的MySQL驱动是go-sql-driver/mysql。
package main
import (
"database/sql"
"fmt"
"log"
"time"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// 配置DSN (Data Source Name)
// 格式: [username[:password]@][protocol[(address)]]/dbname[?param=value]
dsn := "root:password@tcp(127.0.0.1:3306)/crawler?charset=utf8mb4&parseTime=True&loc=Local"
// 打开数据库连接
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("数据库连接失败: %v", err)
}
defer db.Close()
// 设置连接池参数
db.SetMaxOpenConns(100) // 最大连接数
db.SetMaxIdleConns(20) // 最大空闲连接数
db.SetConnMaxLifetime(time.Hour) // 连接最大生命周期
// 测试连接
if err := db.Ping(); err != nil {
log.Fatalf("数据库Ping失败: %v", err)
}
fmt.Println("成功连接到MySQL数据库")
}
3.3 创建数据表
在存储爬虫数据前,需要先创建相应的数据表:
func setupDatabase(db *sql.DB) error {
// 创建产品表
productTable := `
CREATE TABLE IF NOT EXISTS products (
id VARCHAR(50) PRIMARY KEY,
name VARCHAR(200) NOT NULL,
price DECIMAL(10,2) NOT NULL,
category VARCHAR(100),
url VARCHAR(500) UNIQUE,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`
_, err := db.Exec(productTable)
if err != nil {
return err
}
// 创建产品图片表
imageTable := `
CREATE TABLE IF NOT EXISTS product_images (
id INT AUTO_INCREMENT PRIMARY KEY,
product_id VARCHAR(50),
image_url VARCHAR(500) NOT NULL,
image_type VARCHAR(50),
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
UNIQUE KEY unique_product_image (product_id, image_url)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`
_, err = db.Exec(imageTable)
if err != nil {
return err
}
// 创建产品规格表
specTable := `
CREATE TABLE IF NOT EXISTS product_specs (
id INT AUTO_INCREMENT PRIMARY KEY,
product_id VARCHAR(50),
spec_name VARCHAR(100) NOT NULL,
spec_value TEXT,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
UNIQUE KEY unique_product_spec (product_id, spec_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`
_, err = db.Exec(specTable)
if err != nil {
return err
}
// 创建产品评论表
reviewTable := `
CREATE TABLE IF NOT EXISTS product_reviews (
id INT AUTO_INCREMENT PRIMARY KEY,
product_id VARCHAR(50),
content TEXT,
rating INT,
author VARCHAR(100),
review_date VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`
_, err = db.Exec(reviewTable)
return err
}
3.4 数据插入与更新
在爬虫中,需要实现高效的数据插入与更新操作,同时处理重复数据问题。
基本插入
func insertProduct(db *sql.DB, product Product) error {
// 开始事务
tx, err := db.Begin()
if err != nil {
return err
}
// 声明延迟提交或回滚
defer func() {
if err != nil {
tx.Rollback()
return
}
err = tx.Commit()
}()
// 插入产品基本信息
_, err = tx.Exec(`
INSERT INTO products (id, name, price, category, url, description)
VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
price = VALUES(price),
category = VALUES(category),
description = VALUES(description)
`, product.ID, product.Name, product.Price, product.Category, product.URL, product.Description)
if err != nil {
return err
}
// 插入产品图片
for _, img := range product.Images {
_, err = tx.Exec(`
INSERT INTO product_images (product_id, image_url, image_type)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE
image_type = VALUES(image_type)
`, product.ID, img.URL, img.Type)
if err != nil {
return err
}
}
// 插入产品规格
for _, spec := range product.Specs {
_, err = tx.Exec(`
INSERT INTO product_specs (product_id, spec_name, spec_value)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE
spec_value = VALUES(spec_value)
`, product.ID, spec.Name, spec.Value)
if err != nil {
return err
}
}
// 插入产品评论
for _, review := range product.Reviews {
_, err = tx.Exec(`
INSERT INTO product_reviews (product_id, content, rating, author, review_date)
VALUES (?, ?, ?, ?, ?)
`, product.ID, review.Content, review.Rating, review.Author, review.Date)
if err != nil {
return err
}
}
return nil
}
批量插入优化
对于大量数据插入,可以使用批量插入提高性能:
func batchInsertProducts(db *sql.DB, products []Product, batchSize int) error {
// 开始事务
tx, err := db.Begin()
if err != nil {
return err
}
// 声明延迟提交或回滚
defer func() {
if err != nil {
tx.Rollback()
return
}
err = tx.Commit()
}()
// 准备批量插入产品的语句
stmt, err := tx.Prepare(`
INSERT INTO products (id, name, price, category, url, description)
VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
price = VALUES(price),
category = VALUES(category),
description = VALUES(description)
`)
if err != nil {
return err
}
defer stmt.Close()
// 批量插入产品
for i, product := range products {
_, err = stmt.Exec(
product.ID, product.Name, product.Price,
product.Category, product.URL, product.Description,
)
if err != nil {
return err
}
// 每到一批的大小就提交一次事务
if (i+1) % batchSize == 0 || i == len(products)-1 {
if err = tx.Commit(); err != nil {
return err
}
// 开始新事务
tx, err = db.Begin()
if err != nil {
return err
}
// 准备新的语句
stmt, err = tx.Prepare(`
INSERT INTO products (id, name, price, category, url, description)
VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
price = VALUES(price),
category = VALUES(category),
description = VALUES(description)
`)
if err != nil {
return err
}
}
}
// 最后一批不足batchSize的数据会在defer中的tx.Commit()提交
return nil
}
3.5 数据查询
爬虫系统经常需要查询已爬取的数据,以下是一些常用查询操作:
// 根据ID查询完整产品信息
func getProductByID(db *sql.DB, productID string) (*Product, error) {
// 查询产品基本信息
product := &Product{ID: productID}
err := db.QueryRow(`
SELECT name, price, category, url, description
FROM products
WHERE id = ?
`, productID).Scan(&product.Name, &product.Price, &product.Category, &product.URL, &product.Description)
if err != nil {
return nil, err
}
// 查询产品图片
rows, err := db.Query(`
SELECT image_url, image_type
FROM product_images
WHERE product_id = ?
`, productID)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var img Image
if err := rows.Scan(&img.URL, &img.Type); err != nil {
return nil, err
}
product.Images = append(product.Images, img)
}
// 查询产品规格
rows, err = db.Query(`
SELECT spec_name, spec_value
FROM product_specs
WHERE product_id = ?
`, productID)
if err != nil {
return nil, err
}
defer rows.Close()
product.Specs = make([]Specification, 0)
for rows.Next() {
var spec Specification
if err := rows.Scan(&spec.Name, &spec.Value); err != nil {
return nil, err
}
product.Specs = append(product.Specs, spec)
}
// 查询产品评论
rows, err = db.Query(`
SELECT content, rating, author, review_date
FROM product_reviews
WHERE product_id = ?
`, productID)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var review Review
if err := rows.Scan(&review.Content, &review.Rating, &review.Author, &review.Date); err != nil {
return nil, err
}
product.Reviews = append(product.Reviews, review)
}
return product, nil
}
// 查询产品列表
func listProducts(db *sql.DB, category string, limit, offset int) ([]Product, error) {
query := `
SELECT id, name, price, category, url
FROM products
WHERE 1=1
`
args := []interface{}{}
if category != "" {
query += "AND category = ? "
args = append(args, category)
}
query += "ORDER BY updated_at DESC LIMIT ? OFFSET ?"
args = append(args, limit, offset)
rows, err := db.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
products := []Product{}
for rows.Next() {
var p Product
if err := rows.Scan(&p.ID, &p.Name, &p.Price, &p.Category, &p.URL); err != nil {
return nil, err
}
products = append(products, p)
}
return products, nil
}
3.6 使用ORM简化数据库操作
对于复杂的数据库操作,使用ORM(对象关系映射)可以简化开发。Go语言中流行的ORM包括GORM、XORM等。以下是使用GORM的示例:
package main
import (
"log"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// 定义模型
type Product struct {
ID string `gorm:"primaryKey"`
Name string `gorm:"size:200;not null"`
Price float64
Category string
URL string `gorm:"size:500;uniqueIndex"`
Description string `gorm:"type:text"`
CreatedAt time.Time
UpdatedAt time.Time
Images []ProductImage `gorm:"foreignKey:ProductID"`
Specs []ProductSpec `gorm:"foreignKey:ProductID"`
Reviews []ProductReview `gorm:"foreignKey:ProductID"`
}
type ProductImage struct {
ID uint `gorm:"primaryKey"`
ProductID string
ImageURL string `gorm:"size:500;not null"`
ImageType string
}
type ProductSpec struct {
ID uint `gorm:"primaryKey"`
ProductID string
Name string `gorm:"size:100;not null"`
Value string `gorm:"type:text"`
}
type ProductReview struct {
ID uint `gorm:"primaryKey"`
ProductID string
Content string `gorm:"type:text"`
Rating int
Author string
ReviewDate string
CreatedAt time.Time
}
func main() {
// 连接数据库
dsn := "root:password@tcp(127.0.0.1:3306)/crawler?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
log.Fatalf("连接数据库失败: %v", err)
}
// 自动迁移schema
db.AutoMigrate(&Product{}, &ProductImage{}, &ProductSpec{}, &ProductReview{})
// 创建产品
product := Product{
ID: "1001",
Name: "高性能笔记本电脑",
Price: 4999.99,
Category: "电子产品",
URL: "https://example.com/p/1001",
Description: "最新一代处理器,16GB内存,512GB固态硬盘",
Images: []ProductImage{
{ImageURL: "https://example.com/img/1001-1.jpg"},
{ImageURL: "https://example.com/img/1001-2.jpg", ImageType: "detail"},
},
Specs: []ProductSpec{
{Name: "处理器", Value: "Intel Core i7"},
{Name: "内存", Value: "16GB DDR4"},
{Name: "存储", Value: "512GB SSD"},
},
Reviews: []ProductReview{
{Content: "非常好用的笔记本", Rating: 5, Author: "用户A", ReviewDate: "2023-01-15"},
},
}
// 保存产品(包括关联数据)
result := db.Create(&product)
if result.Error != nil {
log.Fatalf("创建产品失败: %v", result.Error)
}
log.Printf("成功创建产品, ID: %s, 影响行数: %d\n", product.ID, result.RowsAffected)
// 更新产品
db.Model(&product).Updates(map[string]interface{}{
"price": 4899.99,
"description": "最新一代处理器,16GB内存,512GB固态硬盘,限时优惠",
})
// 查询产品
var retrievedProduct Product
db.Preload("Images").Preload("Specs").Preload("Reviews").First(&retrievedProduct, "id = ?", "1001")
log.Printf("查询到产品: %s, 价格: %.2f, 图片数: %d, 规格数: %d, 评论数: %d\n",
retrievedProduct.Name,
retrievedProduct.Price,
len(retrievedProduct.Images),
len(retrievedProduct.Specs),
len(retrievedProduct.Reviews),
)
}
MySQL等关系型数据库的优缺点:
- 优点:强大的事务支持,数据一致性保证,丰富的查询语言,成熟稳定
- 缺点:处理海量数据时扩展性受限,不适合非结构化或频繁变化的数据结构
四、NoSQL数据库存储(MongoDB)
NoSQL数据库特别适合存储结构多变的爬虫原始数据。其中,MongoDB作为文档型数据库的代表,能够以JSON形式存储复杂的嵌套文档,非常适合爬虫数据存储。
4.1 MongoDB数据模型设计
在爬虫系统中使用MongoDB时,应考虑以下设计原则:
-
尽量使用嵌入式文档:
- 将相关数据嵌入单个文档,减少查询次数
- 对于一对多关系,根据数据量决定嵌入还是引用
-
合理设计字段:
- 使用有意义的字段名
- 添加元数据字段(如爬取时间、来源等)
- 设置TTL(生存时间)适时清理过期数据
-
为查询场景优化索引:
- 为常用查询字段创建索引
- 使用复合索引优化多字段查询
- 考虑文本索引支持全文搜索
4.2 连接MongoDB
Go语言中连接MongoDB主要使用官方驱动mongo-go-driver:
package main
import (
"context"
"fmt"
"log"
"time"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
)
func main() {
// 设置客户端选项
clientOptions := options.Client().
ApplyURI("mongodb://localhost:27017").
SetMaxPoolSize(20).
SetConnectTimeout(5 * time.Second)
// 连接MongoDB
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client, err := mongo.Connect(ctx, clientOptions)
if err != nil {
log.Fatalf("连接MongoDB失败: %v", err)
}
// 确保在函数退出时断开连接
defer func() {
if err = client.Disconnect(ctx); err != nil {
log.Fatalf("断开MongoDB连接失败: %v", err)
}
}()
// 检查连接
err = client.Ping(ctx, readpref.Primary())
if err != nil {
log.Fatalf("MongoDB Ping失败: %v", err)
}
fmt.Println("成功连接到MongoDB")
// 获取数据库和集合
db := client.Database("crawler")
collection := db.Collection("products")
fmt.Printf("准备好操作集合: %s\n", collection.Name())
}
4.3 数据存储与更新
MongoDB非常适合存储爬虫采集的原始数据,特别是结构复杂或不固定的数据。
基本插入与更新
// 定义产品文档结构
type ProductDocument struct {
ID string `bson:"_id"`
Name string `bson:"name"`
Price float64 `bson:"price"`
Category string `bson:"category"`
URL string `bson:"url"`
Description string `bson:"description"`
Images []ImageDocument `bson:"images"`
Specs []SpecDocument `bson:"specs"`
Reviews []ReviewDocument `bson:"reviews"`
MetaData MetaData `bson:"metadata"`
}
type ImageDocument struct {
URL string `bson:"url"`
Type string `bson:"type,omitempty"`
}
type SpecDocument struct {
Name string `bson:"name"`
Value string `bson:"value"`
}
type ReviewDocument struct {
Content string `bson:"content"`
Rating int `bson:"rating"`
Author string `bson:"author"`
ReviewDate string `bson:"review_date"`
CreatedAt time.Time `bson:"created_at"`
}
type MetaData struct {
CreatedAt time.Time `bson:"created_at"`
UpdatedAt time.Time `bson:"updated_at"`
Source string `bson:"source"`
CrawlerVersion string `bson:"crawler_version"`
}
// 插入产品数据
func insertProductDocument(collection *mongo.Collection, product ProductDocument) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 设置元数据
now := time.Now()
product.MetaData.CreatedAt = now
product.MetaData.UpdatedAt = now
// 插入文档
_, err := collection.InsertOne(ctx, product)
return err
}
// 更新产品数据
func updateProductDocument(collection *mongo.Collection, product ProductDocument) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 更新元数据
product.MetaData.UpdatedAt = time.Now()
// 构建更新操作
filter := bson.M{"_id": product.ID}
update := bson.M{"$set": product}
// 使用upsert选项
opts := options.Update().SetUpsert(true)
// 执行更新
_, err := collection.UpdateOne(ctx, filter, update, opts)
return err
}
批量插入
对于爬取大量数据的场景,批量插入可以显著提高性能:
func batchInsertProducts(collection *mongo.Collection, products []ProductDocument) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 准备批量插入的文档
docs := make([]interface{}, len(products))
now := time.Now()
for i, product := range products {
// 设置元数据
product.MetaData.CreatedAt = now
product.MetaData.UpdatedAt = now
docs[i] = product
}
// 执行批量插入
_, err := collection.InsertMany(ctx, docs)
return err
}
// 使用批量写操作处理混合的插入和更新
func bulkWriteProducts(collection *mongo.Collection, products []ProductDocument) error {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
// 准备批量写操作
var models []mongo.WriteModel
now := time.Now()
for _, product := range products {
// 设置元数据
product.MetaData.UpdatedAt = now
model := mongo.NewReplaceOneModel().
SetFilter(bson.M{"_id": product.ID}).
SetReplacement(product).
SetUpsert(true)
models = append(models, model)
}
// 执行批量写操作
opts := options.BulkWrite().SetOrdered(false)
_, err := collection.BulkWrite(ctx, models, opts)
return err
}
4.4 数据查询
MongoDB提供了灵活的查询语言,支持复杂查询条件、聚合管道等高级功能。
基本查询
// 根据ID查询产品
func getProductByID(collection *mongo.Collection, productID string) (*ProductDocument, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var product ProductDocument
err := collection.FindOne(ctx, bson.M{"_id": productID}).Decode(&product)
if err != nil {
return nil, err
}
return &product, nil
}
// 查询产品列表
func listProducts(collection *mongo.Collection, category string, limit, skip int64) ([]ProductDocument, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 构建查询条件
filter := bson.M{}
if category != "" {
filter["category"] = category
}
// 设置查询选项
opts := options.Find().
SetSort(bson.M{"metadata.updated_at": -1}).
SetLimit(limit).
SetSkip(skip)
// 执行查询
cursor, err := collection.Find(ctx, filter, opts)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
// 解码结果
var products []ProductDocument
if err = cursor.All(ctx, &products); err != nil {
return nil, err
}
return products, nil
}
高级查询与聚合
MongoDB强大的聚合管道功能适合数据分析和统计:
// 按分类统计产品数量和平均价格
func aggregateProductStats(collection *mongo.Collection) ([]bson.M, error) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
// 定义聚合管道
pipeline := mongo.Pipeline{
// 按分类分组
bson.D{{"$group", bson.D{
{"_id", "$category"},
{"count", bson.D{{"$sum", 1}}},
{"avgPrice", bson.D{{"$avg", "$price"}}},
{"minPrice", bson.D{{"$min", "$price"}}},
{"maxPrice", bson.D{{"$max", "$price"}}},
}}},
// 按计数降序排序
bson.D{{"$sort", bson.D{{"count", -1}}}},
}
// 执行聚合
cursor, err := collection.Aggregate(ctx, pipeline)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
// 解码结果
var results []bson.M
if err = cursor.All(ctx, &results); err != nil {
return nil, err
}
return results, nil
}
// 查找评分高的产品
func findHighRatedProducts(collection *mongo.Collection, minRating float64) ([]ProductDocument, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 定义聚合管道
pipeline := mongo.Pipeline{
// 展开评论数组
bson.D{{"$unwind", "$reviews"}},
// 按产品ID分组并计算平均评分
bson.D{{"$group", bson.D{
{"_id", "$_id"},
{"avgRating", bson.D{{"$avg", "$reviews.rating"}}},
{"product", bson.D{{"$first", "$$ROOT"}}},
}}},
// 筛选高评分产品
bson.D{{"$match", bson.D{{"avgRating", bson.D{{"$gte", minRating}}}}}},
// 按平均评分降序排序
bson.D{{"$sort", bson.D{{"avgRating", -1}}}},
// 提取产品信息
bson.D{{"$replaceRoot", bson.D{{"newRoot", "$product"}}}},
}
// 执行聚合
cursor, err := collection.Aggregate(ctx, pipeline)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
// 解码结果
var products []ProductDocument
if err = cursor.All(ctx, &products); err != nil {
return nil, err
}
return products, nil
}
MongoDB的优缺点:
- 优点:灵活的文档模型,优秀的水平扩展能力,适合频繁变化的数据结构,丰富的查询和聚合功能
- 缺点:事务支持有限(4.0以前),索引占用空间大,复杂查询性能不如关系型数据库
五、数据导出与格式转换
爬虫数据采集后,经常需要将数据导出为特定格式,用于分析、报告或其他系统集成。
5.1 常见数据导出需求
爬虫系统中的数据导出通常有以下几种场景:
- 数据报表导出:生成CSV/Excel报表供业务人员使用
- 数据迁移导出:从一个存储系统导出到另一个系统
- 数据共享导出:生成标准格式文件供第三方系统使用
- 数据备份导出:定期备份爬取的数据
5.2 从数据库导出到CSV
从MySQL导出到CSV
func exportProductsToCSV(db *sql.DB, filename string) error {
// 创建CSV文件
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
// 创建CSV写入器
writer := csv.NewWriter(file)
defer writer.Flush()
// 写入表头
header := []string{"ID", "Name", "Price", "Category", "URL", "Description"}
if err := writer.Write(header); err != nil {
return err
}
// 查询产品数据
rows, err := db.Query(`
SELECT id, name, price, category, url, description
FROM products
`)
if err != nil {
return err
}
defer rows.Close()
// 逐行写入CSV
for rows.Next() {
var id, name, category, url, description string
var price float64
if err := rows.Scan(&id, &name, &price, &category, &url, &description); err != nil {
return err
}
record := []string{
id,
name,
fmt.Sprintf("%.2f", price),
category,
url,
// 处理可能包含换行符的字段
strings.ReplaceAll(description, "\n", " "),
}
if err := writer.Write(record); err != nil {
return err
}
}
return rows.Err()
}
从MongoDB导出到CSV
func exportMongoProductsToCSV(collection *mongo.Collection, filename string) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 创建CSV文件
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
// 创建CSV写入器
writer := csv.NewWriter(file)
defer writer.Flush()
// 写入表头
header := []string{"ID", "Name", "Price", "Category", "URL", "Description", "Image Count", "Avg Rating"}
if err := writer.Write(header); err != nil {
return err
}
// 定义聚合管道,计算每个产品的平均评分
pipeline := mongo.Pipeline{
// 添加字段:图片数量和平均评分
bson.D{{"$addFields", bson.D{
{"imageCount", bson.D{{"$size", "$images"}}},
{"avgRating", bson.D{
{"$cond", bson.A{
bson.D{{"$gt", bson.A{bson.D{{"$size", "$reviews"}}, 0}}},
bson.D{{"$avg", "$reviews.rating"}},
0,
}},
}},
}}},
// 投影需要的字段
bson.D{{"$project", bson.D{
{"_id", 1},
{"name", 1},
{"price", 1},
{"category", 1},
{"url", 1},
{"description", 1},
{"imageCount", 1},
{"avgRating", 1},
}}},
}
// 执行聚合查询
cursor, err := collection.Aggregate(ctx, pipeline)
if err != nil {
return err
}
defer cursor.Close(ctx)
// 逐个文档处理
for cursor.Next(ctx) {
var result bson.M
if err := cursor.Decode(&result); err != nil {
return err
}
// 提取字段,处理可能的空值
id := getStringValue(result, "_id", "")
name := getStringValue(result, "name", "")
price := getFloatValue(result, "price", 0)
category := getStringValue(result, "category", "")
url := getStringValue(result, "url", "")
description := getStringValue(result, "description", "")
imageCount := getIntValue(result, "imageCount", 0)
avgRating := getFloatValue(result, "avgRating", 0)
// 写入CSV记录
record := []string{
id,
name,
fmt.Sprintf("%.2f", price),
category,
url,
strings.ReplaceAll(description, "\n", " "),
fmt.Sprintf("%d", imageCount),
fmt.Sprintf("%.1f", avgRating),
}
if err := writer.Write(record); err != nil {
return err
}
}
return cursor.Err()
}
// 辅助函数:安全地获取字符串字段
func getStringValue(m bson.M, key, defaultVal string) string {
if val, ok := m[key]; ok && val != nil {
if strVal, ok := val.(string); ok {
return strVal
}
}
return defaultVal
}
// 辅助函数:安全地获取浮点数字段
func getFloatValue(m bson.M, key string, defaultVal float64) float64 {
if val, ok := m[key]; ok && val != nil {
switch v := val.(type) {
case float64:
return v
case float32:
return float64(v)
case int:
return float64(v)
case int32:
return float64(v)
case int64:
return float64(v)
}
}
return defaultVal
}
// 辅助函数:安全地获取整数字段
func getIntValue(m bson.M, key string, defaultVal int) int {
if val, ok := m[key]; ok && val != nil {
switch v := val.(type) {
case int:
return v
case int32:
return int(v)
case int64:
return int(v)
case float64:
return int(v)
case float32:
return int(v)
}
}
return defaultVal
}
5.3 生成Excel报表
Excel文件比CSV更丰富,支持多个工作表、格式化、公式等。Go语言中可以使用excelize库生成Excel文件:
package main
import (
"database/sql"
"fmt"
"log"
"time"
"github.com/xuri/excelize/v2"
_ "github.com/go-sql-driver/mysql"
)
func exportProductsToExcel(db *sql.DB, filename string) error {
// 创建Excel文件
f := excelize.NewFile()
// 设置工作表名称
sheetName := "Products"
index, err := f.NewSheet(sheetName)
if err != nil {
return err
}
f.SetActiveSheet(index)
// 设置表头样式
headerStyle, err := f.NewStyle(&excelize.Style{
Font: &excelize.Font{
Bold: true,
Size: 12,
Color: "#FFFFFF",
},
Fill: excelize.Fill{
Type: "pattern",
Color: []string{"#4472C4"},
Pattern: 1,
},
Alignment: &excelize.Alignment{
Horizontal: "center",
Vertical: "center",
},
Border: []excelize.Border{
{Type: "top", Color: "#000000", Style: 1},
{Type: "bottom", Color: "#000000", Style: 1},
{Type: "left", Color: "#000000", Style: 1},
{Type: "right", Color: "#000000", Style: 1},
},
})
if err != nil {
return err
}
// 设置数据样式
dataStyle, err := f.NewStyle(&excelize.Style{
Border: []excelize.Border{
{Type: "top", Color: "#D9D9D9", Style: 1},
{Type: "bottom", Color: "#D9D9D9", Style: 1},
{Type: "left", Color: "#D9D9D9", Style: 1},
{Type: "right", Color: "#D9D9D9", Style: 1},
},
})
if err != nil {
return err
}
// 写入表头
headers := []string{"ID", "产品名称", "价格", "分类", "URL", "描述", "更新时间"}
for i, header := range headers {
cell := fmt.Sprintf("%c%d", 'A'+i, 1)
f.SetCellValue(sheetName, cell, header)
}
// 应用表头样式
f.SetCellStyle(sheetName, "A1", fmt.Sprintf("%c1", 'A'+len(headers)-1), headerStyle)
// 设置列宽
f.SetColWidth(sheetName, "A", "A", 15)
f.SetColWidth(sheetName, "B", "B", 40)
f.SetColWidth(sheetName, "C", "C", 12)
f.SetColWidth(sheetName, "D", "D", 15)
f.SetColWidth(sheetName, "E", "E", 50)
f.SetColWidth(sheetName, "F", "F", 60)
f.SetColWidth(sheetName, "G", "G", 20)
// 查询产品数据
rows, err := db.Query(`
SELECT id, name, price, category, url, description, updated_at
FROM products
ORDER BY updated_at DESC
`)
if err != nil {
return err
}
defer rows.Close()
// 逐行写入Excel
rowIndex := 2 // 从第2行开始写入数据
for rows.Next() {
var id, name, category, url, description string
var price float64
var updatedAt time.Time
if err := rows.Scan(&id, &name, &price, &category, &url, &description, &updatedAt); err != nil {
return err
}
// 写入单元格
f.SetCellValue(sheetName, fmt.Sprintf("A%d", rowIndex), id)
f.SetCellValue(sheetName, fmt.Sprintf("B%d", rowIndex), name)
f.SetCellValue(sheetName, fmt.Sprintf("C%d", rowIndex), price)
f.SetCellValue(sheetName, fmt.Sprintf("D%d", rowIndex), category)
f.SetCellValue(sheetName, fmt.Sprintf("E%d", rowIndex), url)
f.SetCellValue(sheetName, fmt.Sprintf("F%d", rowIndex), description)
f.SetCellValue(sheetName, fmt.Sprintf("G%d", rowIndex), updatedAt.Format("2006-01-02 15:04:05"))
// 应用数据样式
f.SetCellStyle(sheetName, fmt.Sprintf("A%d", rowIndex), fmt.Sprintf("%c%d", 'A'+len(headers)-1, rowIndex), dataStyle)
rowIndex++
}
// 保存文件
if err := f.SaveAs(filename); err != nil {
return err
}
return nil
}
5.4 数据格式转换
JSON到CSV转换
func convertJSONToCSV(jsonFilePath, csvFilePath string, keyMap map[string]string) error {
// 读取JSON文件
jsonData, err := ioutil.ReadFile(jsonFilePath)
if err != nil {
return err
}
// 解析JSON
var data []map[string]interface{}
if err := json.Unmarshal(jsonData, &data); err != nil {
return err
}
// 创建CSV文件
file, err := os.Create(csvFilePath)
if err != nil {
return err
}
defer file.Close()
// 创建CSV写入器
writer := csv.NewWriter(file)
defer writer.Flush()
// 提取表头
var headers []string
for _, v := range keyMap {
headers = append(headers, v)
}
// 写入表头
if err := writer.Write(headers); err != nil {
return err
}
// 写入数据行
for _, item := range data {
var row []string
for k, _ := range keyMap {
// 提取字段值,处理可能的嵌套
value := extractNestedValue(item, k)
row = append(row, fmt.Sprintf("%v", value))
}
if err := writer.Write(row); err != nil {
return err
}
}
return nil
}
// 辅助函数:从嵌套结构中提取值
func extractNestedValue(data map[string]interface{}, path string) interface{} {
parts := strings.Split(path, ".")
current := data
for i, part := range parts {
if i == len(parts)-1 {
return current[part]
}
if nextMap, ok := current[part].(map[string]interface{}); ok {
current = nextMap
} else {
return nil
}
}
return nil
}
CSV到JSON转换
func convertCSVToJSON(csvFilePath, jsonFilePath string) error {
// 打开CSV文件
file, err := os.Open(csvFilePath)
if err != nil {
return err
}
defer file.Close()
// 创建CSV读取器
reader := csv.NewReader(file)
// 读取表头
headers, err := reader.Read()
if err != nil {
return err
}
// 读取所有记录
records, err := reader.ReadAll()
if err != nil {
return err
}
// 转换为JSON格式
var jsonData []map[string]string
for _, record := range records {
item := make(map[string]string)
for i, value := range record {
if i < len(headers) {
item[headers[i]] = value
}
}
jsonData = append(jsonData, item)
}
// 转换为JSON字符串
jsonBytes, err := json.MarshalIndent(jsonData, "", " ")
if err != nil {
return err
}
// 写入文件
return ioutil.WriteFile(jsonFilePath, jsonBytes, 0644)
}
5.5 数据备份与恢复策略
爬虫系统需要定期备份数据,以防止数据丢失。以下是一些常用的备份策略:
MySQL数据库备份
func backupMySQLDatabase(dbName, backupPath string) error {
// 构建备份命令
backupFile := fmt.Sprintf("%s/%s_%s.sql", backupPath, dbName, time.Now().Format("20060102_150405"))
cmd := exec.Command("mysqldump",
"-u", "root",
"-p[password]", // 替换为实际密码
"--databases", dbName,
"--single-transaction",
"--quick",
"--lock-tables=false",
"-r", backupFile)
// 执行命令
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("备份失败: %v, 输出: %s", err, output)
}
log.Printf("数据库 %s 已备份到 %s", dbName, backupFile)
return nil
}
MongoDB数据库备份
func backupMongoDatabase(dbName, backupPath string) error {
// 构建备份命令
backupDir := fmt.Sprintf("%s/%s_%s", backupPath, dbName, time.Now().Format("20060102_150405"))
cmd := exec.Command("mongodump",
"--db", dbName,
"--out", backupDir)
// 执行命令
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("备份失败: %v, 输出: %s", err, output)
}
log.Printf("数据库 %s 已备份到 %s", dbName, backupDir)
return nil
}
自动化定期备份
在实际应用中,可以使用cron作业定期执行备份任务:
package main
import (
"log"
"time"
"github.com/robfig/cron/v3"
)
func main() {
c := cron.New()
// 每天凌晨3点执行MySQL备份
c.AddFunc("0 3 * * *", func() {
if err := backupMySQLDatabase("crawler", "/backup/mysql"); err != nil {
log.Printf("MySQL备份失败: %v", err)
}
})
// 每周日凌晨4点执行MongoDB备份
c.AddFunc("0 4 * * 0", func() {
if err := backupMongoDatabase("crawler", "/backup/mongo"); err != nil {
log.Printf("MongoDB备份失败: %v", err)
}
})
// 每月删除3个月前的备份
c.AddFunc("0 5 1 * *", func() {
cleanupOldBackups("/backup", 90*24*time.Hour)
})
c.Start()
// 让程序保持运行
select {}
}
// 清理旧备份
func cleanupOldBackups(backupDir string, maxAge time.Duration) error {
cutoffTime := time.Now().Add(-maxAge)
// 遍历备份目录
entries, err := ioutil.ReadDir(backupDir)
if err != nil {
return err
}
for _, entry := range entries {
if entry.ModTime().Before(cutoffTime) {
path := filepath.Join(backupDir, entry.Name())
log.Printf("删除过期备份: %s", path)
if err := os.RemoveAll(path); err != nil {
log.Printf("删除失败: %v", err)
}
}
}
return nil
}
📝 练习与思考
-
基础练习:创建一个爬虫程序,爬取一个新闻网站的文章列表,将数据分别存储到CSV文件和MySQL数据库中。
-
进阶练习:对已爬取的数据实现三种不同格式的导出功能(CSV、JSON、Excel),并设计直观的命令行参数控制导出选项。
-
思考题:
- 当爬取的数据量达到10GB级别时,哪种存储方案更合适?为什么?
- 如何设计数据模型以同时满足爬取性能和查询性能的需求?
- 在爬虫系统中,什么情况下应该使用NoSQL数据库而非关系型数据库?
💡 小结
在本文中,我们深入探讨了爬虫数据的存储与导出技术,主要内容包括:
-
数据存储方案比较:我们对比了文件系统、关系型数据库和NoSQL数据库等不同存储方案的优缺点,帮助读者根据项目需求选择最合适的方案。
-
文件系统存储实现:详细介绍了如何使用Go语言实现CSV、JSON和XML等不同格式的文件存储,适合小型爬虫项目或原型开发。
-
关系型数据库存储:重点讲解了MySQL数据库的设计原则、连接配置、表结构设计以及数据的插入和查询操作,适合存储结构化数据。
-
NoSQL数据库存储:通过MongoDB的实例,展示了如何存储结构复杂、多变的爬虫数据,以及如何利用文档数据库的灵活性和查询能力。
-
数据导出与格式转换:介绍了如何将爬取的数据导出为CSV、Excel等格式,以及不同格式间的转换方法,方便数据分享和分析。
-
数据备份与恢复:提供了数据库备份和自动化定期备份的实用技术,确保爬虫数据的安全。
通过掌握这些技术,您可以为爬虫系统构建高效、可靠的数据管理模块,根据实际需求选择最合适的存储方案,并实现数据的高效处理和利用。
下篇预告
在下一篇文章【反爬虫策略应对技术】中,我们将深入探讨如何应对各种网站的反爬虫措施,包括IP限制、User-Agent检测、验证码识别等关键技术,帮助您构建更稳定、更健壮的爬虫系统。敬请期待!
👨💻 关于作者与Gopher部落
"Gopher部落"专注于Go语言技术分享,提供从入门到精通的完整学习路线。
🌟 为什么关注我们?
- 系统化学习路径:本系列14篇文章循序渐进,带你完整掌握Go爬虫开发
- 实战驱动教学:理论结合实践,每篇文章都有可操作的代码示例
- 持续更新内容:定期分享最新Go生态技术动态与大厂实践经验
- 专业技术社区:加入我们的技术交流群,与众多Go开发者共同成长
📱 关注方式
- 微信公众号:搜索 “Gopher部落” 或 “GopherTribe”
- 优快云专栏:点击页面右上角"关注"按钮
💡 读者福利
关注公众号回复 “Go爬虫” 即可获取:
- 完整Go爬虫学习资料
- 本系列示例代码
- 项目实战源码
期待与您在Go语言的学习旅程中共同成长!