【Go语言爬虫系列04】数据存储与导出

AWVS V24.8深度解析:从漏洞扫描原理到DevSecOps实战部署 漏洞扫描是网络安全防御体系中的关键技术,其核心原理在于通过自动化工具模拟攻击者行为,对Web应用、API接口等进行主动安全测试,以发现潜在的安全漏洞。这项技术的价值在于将传统低效、易遗漏的人工检查转变为系统化、可重复的检测流程,极大地提升了安全评估的覆盖率和效率。在应用场景上,它不仅是安全团队进行渗透测试的利器,更是融入DevSecOps实践、实现安全左移的关键环节。本文以业界广泛使用的专业漏洞扫描器Acunetix Premium(AWVS)的最新V24.8版本为例,深入剖析其增强的API安全测试能力和J 阅读详情

📚 原创系列: “Go语言爬虫系列”

🔄 转载说明: 本文最初发布于"Gopher部落"微信公众号,经原作者授权转载。

🔗 关注原创: 欢迎扫描文末二维码,关注"Gopher部落"微信公众号获取第一手Go技术文章。

📑 Go语言爬虫系列导航

本文是【Go语言爬虫系列】的第4篇,点击下方链接查看更多文章

🚀 Go爬虫系列:共14篇
  1. 爬虫入门与Colly框架基础
  2. HTML解析与Goquery技术详解
  3. 并发控制与错误处理
  4. 数据存储与导出 👈 当前位置
  5. 反爬虫策略应对技术
  6. 模拟登录与会话维持
  7. 分布式爬虫架构
  8. JavaScript渲染页面抓取
  9. 移动应用数据抓取
  10. 爬虫性能优化技术
  11. 爬虫数据分析与应用
  12. 爬虫系统安全与伦理
  13. 爬虫系统监控与运维
  14. 综合项目实战:新闻聚合系统 ⏳ 开发中 - 关注公众号获取发布通知!

📢 特别提示:《综合项目实战:新闻聚合系统》正在精心制作中!这将是一个完整的实战项目,带您从零构建一个多站点新闻聚合系统。扫描文末二维码关注公众号并回复「新闻聚合」,获取项目发布通知和源码下载链接!

📖 文章导读

在前三篇文章中,我们学习了爬虫的基础知识、HTML解析技术以及并发控制与错误处理。获取数据后,如何高效存储和管理这些数据成为关键问题。本文作为系列的第四篇,将深入探讨爬虫数据的存储与导出技术,主要内容包括:

  1. 爬虫数据存储方案的比较与选择
  2. 文件系统存储实现(CSV、JSON、XML)
  3. 关系型数据库存储(MySQL)实战
  4. NoSQL数据库(MongoDB)在爬虫中的应用
  5. 数据导出与格式转换的有效方法
  6. 爬虫数据的备份与恢复策略

通过本文的学习,您将能够为爬虫系统选择最适合的数据存储方案,并掌握数据管理的最佳实践,构建完整的爬虫数据处理流程。

一、爬虫数据存储方案比较

1.1 数据存储的重要性与挑战

在爬虫系统中,数据存储是连接数据采集和数据分析的关键环节。一个优秀的存储方案应当能够:

  • 高效处理大量数据:爬虫可能在短时间内采集海量信息
  • 支持多样化数据结构:不同网站的数据结构差异很大
  • 便于数据查询与分析:支持灵活的检索和统计功能
  • 确保数据一致性:防止重复或丢失数据
  • 具备良好扩展性:能够随着数据量增长进行扩展

然而,爬虫数据存储也面临着特殊挑战:

  1. 数据量大且增长快:某些爬虫项目可能每天产生GB甚至TB级数据
  2. 数据结构多变:网站结构可能随时变化,存储方案需要适应这种变化
  3. 读写性能平衡:既要满足高速写入,又要支持高效查询
  4. 数据清洗需求:原始数据往往需要清洗处理后再存储
  5. 资源限制:受限于服务器存储空间、内存和网络带宽

1.2 主流存储方案对比

根据项目需求不同,可以选择不同类型的存储方案。以下是主流存储方案的对比:

文件系统存储
格式优势劣势适用场景
CSV简单易用,兼容性好,适合表格数据不支持复杂结构,大文件处理困难结构简单的表格数据,小型爬虫项目
JSON支持嵌套结构,可读性好,语言无关空间效率较低,查询性能一般结构多变的数据,中小型项目
XML结构严格,支持命名空间和验证冗余大,解析开销大需要严格结构验证的场景
二进制存储效率高,读写速度快可读性差,通用性低需要高性能的特定场景
数据库存储
类型代表产品优势劣势适用场景
关系型数据库MySQL, PostgreSQL事务支持,强一致性,成熟稳定扩展性受限,不适合非结构化数据结构稳定的数据,需要复杂查询
文档型数据库MongoDB, CouchDB灵活的数据模型,易于扩展事务支持有限,索引占空间结构多变的数据,需要快速迭代
键值存储Redis, LevelDB极高的读写性能,易于扩展查询能力有限,功能相对简单缓存,队列,会话存储
列式数据库Cassandra, HBase优秀的写入性能,高可扩展性实现复杂,学习曲线陡峭超大规模数据,时间序列数据

1.3 存储方案选择策略

选择合适的存储方案应考虑以下因素:

  1. 数据规模:小型项目(<1GB)可考虑文件存储,中型项目(1-100GB)适合单机数据库,大型项目(>100GB)需考虑分布式解决方案

  2. 数据结构:

    • 结构化数据(表格型):关系型数据库或CSV
    • 半结构化数据(嵌套复杂):文档数据库或JSON
    • 非结构化数据(图片、文本):专用存储系统或文件系统
  3. 查询需求:

    • 复杂关联查询:关系型数据库
    • 简单键值查询:键值存储
    • 全文检索:搜索引擎(如Elasticsearch)
  4. 性能要求:

    • 高并发写入:键值存储、列式数据库
    • 快速读取:内存数据库、良好索引的关系型数据库
    • 批量处理:支持批处理的数据库或文件系统
  5. 开发资源:

    • 团队熟悉度
    • 维护成本
    • 社区支持

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等关系型数据库时,应遵循以下设计原则:

  1. 合理设计表结构:

    • 适当拆分表,避免过多无关字段
    • 为大字段(如文章内容)单独建表
    • 使用适当的字段类型和长度
  2. 建立合适的索引:

    • 对频繁查询的字段建立索引
    • 避免过度索引影响写入性能
    • 考虑复合索引优化复杂查询
  3. 处理重复数据:

    • 使用唯一键约束防止重复数据
    • 实现"更新或插入"(UPSERT)逻辑
  4. 批量操作:

    • 使用事务和批量插入提高性能
    • 控制每批次的记录数量

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时,应考虑以下设计原则:

  1. 尽量使用嵌入式文档:

    • 将相关数据嵌入单个文档,减少查询次数
    • 对于一对多关系,根据数据量决定嵌入还是引用
  2. 合理设计字段:

    • 使用有意义的字段名
    • 添加元数据字段(如爬取时间、来源等)
    • 设置TTL(生存时间)适时清理过期数据
  3. 为查询场景优化索引:

    • 为常用查询字段创建索引
    • 使用复合索引优化多字段查询
    • 考虑文本索引支持全文搜索

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 常见数据导出需求

爬虫系统中的数据导出通常有以下几种场景:

  1. 数据报表导出:生成CSV/Excel报表供业务人员使用
  2. 数据迁移导出:从一个存储系统导出到另一个系统
  3. 数据共享导出:生成标准格式文件供第三方系统使用
  4. 数据备份导出:定期备份爬取的数据

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
}

📝 练习与思考

  1. 基础练习:创建一个爬虫程序,爬取一个新闻网站的文章列表,将数据分别存储到CSV文件和MySQL数据库中。

  2. 进阶练习:对已爬取的数据实现三种不同格式的导出功能(CSV、JSON、Excel),并设计直观的命令行参数控制导出选项。

  3. 思考题:

    • 当爬取的数据量达到10GB级别时,哪种存储方案更合适?为什么?
    • 如何设计数据模型以同时满足爬取性能和查询性能的需求?
    • 在爬虫系统中,什么情况下应该使用NoSQL数据库而非关系型数据库?

💡 小结

在本文中,我们深入探讨了爬虫数据的存储与导出技术,主要内容包括:

  1. 数据存储方案比较:我们对比了文件系统、关系型数据库和NoSQL数据库等不同存储方案的优缺点,帮助读者根据项目需求选择最合适的方案。

  2. 文件系统存储实现:详细介绍了如何使用Go语言实现CSV、JSON和XML等不同格式的文件存储,适合小型爬虫项目或原型开发。

  3. 关系型数据库存储:重点讲解了MySQL数据库的设计原则、连接配置、表结构设计以及数据的插入和查询操作,适合存储结构化数据。

  4. NoSQL数据库存储:通过MongoDB的实例,展示了如何存储结构复杂、多变的爬虫数据,以及如何利用文档数据库的灵活性和查询能力。

  5. 数据导出与格式转换:介绍了如何将爬取的数据导出为CSV、Excel等格式,以及不同格式间的转换方法,方便数据分享和分析。

  6. 数据备份与恢复:提供了数据库备份和自动化定期备份的实用技术,确保爬虫数据的安全。

通过掌握这些技术,您可以为爬虫系统构建高效、可靠的数据管理模块,根据实际需求选择最合适的存储方案,并实现数据的高效处理和利用。

下篇预告

在下一篇文章【反爬虫策略应对技术】中,我们将深入探讨如何应对各种网站的反爬虫措施,包括IP限制、User-Agent检测、验证码识别等关键技术,帮助您构建更稳定、更健壮的爬虫系统。敬请期待!

👨‍💻 关于作者与Gopher部落

"Gopher部落"专注于Go语言技术分享,提供从入门到精通的完整学习路线。

🌟 为什么关注我们?

  1. 系统化学习路径:本系列14篇文章循序渐进,带你完整掌握Go爬虫开发
  2. 实战驱动教学:理论结合实践,每篇文章都有可操作的代码示例
  3. 持续更新内容:定期分享最新Go生态技术动态与大厂实践经验
  4. 专业技术社区:加入我们的技术交流群,与众多Go开发者共同成长

📱 关注方式

  1. 微信公众号:搜索 “Gopher部落” 或 “GopherTribe”
  2. 优快云专栏:点击页面右上角"关注"按钮

💡 读者福利

关注公众号回复 “Go爬虫” 即可获取:

  • 完整Go爬虫学习资料
  • 本系列示例代码
  • 项目实战源码

期待与您在Go语言的学习旅程中共同成长!

Agent Memory 工程实践 :用 Go + PostgreSQL 构建可证明、可撤销的长期记忆 状态对外含义必需证据revoked删除事务已提交;新权威复核排除旧代主库 Epoch 与删除请求在线存储和派生载体完成清理每个 carrier 的 purge receipt备份、日志、导出和模型服务等已到期或完成适用清理保留策略与外部系统记录“24 小时”只能是经验证的产品目标,不能泛化成法律或物理擦除保证。API 应返回请求 ID、当前状态、已覆盖与未覆盖范围,而非一个含混的success。删除事件到达后,标记相关工作集失效。 阅读详情

相关推荐

【信息科学与工程学】计算机科学与自动化——第八十四篇 C++分布式软件高并发/高可用算法01

4. 提交:协调者选择提交时间戳(>所有读时间戳),两阶段提交:a) 写意图预提交到所有参与者,b) 参与者持久化后确认,c) 协调者决定提交,异步清理意图。:客户端请求发送到协调节点,协调节点根据路由转发到主分片所在节点。Span有父子关系。1. 索引文档:客户端发送PUT请求到协调节点,协调节点路由到主分片节点,主分片写入本地,然后并行复制到副本分片,等待确认后响应客户端。3. 搜索:客户端发送搜索请求到协调节点,协调节点广播到所有相关分片,每个分片执行查询,返回结果,协调节点合并、排序、分页后返回。

weixin_49199313的博客 1077

getAwayBSG部署教程:Linux环境下的快速安装与运行步骤

getAwayBSG(又称“逃离北上广”)是一款专注于数据抓取与分析的工具,支持链家二手房、租房及智联招聘等数据的采集与处理。本教程将详细介绍如何在Linux环境下快速部署并运行该项目,帮助用户轻松上手数据抓取功能。 ## 1. 准备工作:环境与依赖检查 在开始部署前,请确保您的Linux系统满足以下要求: - **操作系统**:Ubuntu 18.04+/CentOS 7+ 或其他基于Lin

gitblog_00664的博客 513

Sigmastar平台WDR调试实战:从参数解析到鬼影消除的完整指南

本文提供了Sigmastar平台宽动态(WDR)技术的实战调试指南。针对逆光、运动场景下的核心痛点,系统性地解析了从多曝光采集、HDR融合到色调映射的完整管线,并重点阐述了如何通过调整曝光比、运动阈值与调整曲线等关键参数,有效消除运动鬼影、平衡暗部噪声与细节,最终实现清晰稳定的图像效果。

Jerry的专栏 926

资深python全能工程师

Python 数据结构算法和设计模式。数据分析Python必备工具模块实战。Python数据分析与机器学习实战。知识+技能+实战训练+项目演练。浏览器调试、数据分析与性能调优。Python 语言结构与语法。Web开发必备-HTTP协议。Django-Web后端框架。深度学习算法及其框架应用实战。Flask-Web后端框架。React框架一前端高级。数据挖掘项目解决方案实战。知识图谱及其项目应用实战。推荐系统及其项目应用实战。python学习路线图。Python 面向对象。Python 系统编程。

niwoxiangyu的博客 1803

Keil 5安装教程(C51与MDK共存)

目录前言MDK的安装C51的安装安装完成后的验证新建C51工程添加C51程序文件编译测试总结 前言 本篇文章将介绍嵌入式开发工具Keil 5的安装方法,并使MDK(Microcontroller Development Kit,开发stm32时会用到)和C51(51单片机C语言软件开发系统)共存。今后使用Keil 5可同时开发51单片机程序和ARM单片机程序,便于学习与从51到32的过渡。 注: 为防止商业纠纷,本篇文章不涉及文中软件的注册破解,请支持正版。 同时为了便于今后更深入的学习,本文不会介绍软件的

xushu_me的博客 6万+

STM32 超声波模块输入捕获测距(标准库)

通道2设置为下降沿触发,触发瞬间会将CNT的记录到CCR2中,由于上升沿触发时已经将CNT置零所以此时的CNT的值就是高电平的持续时间。通道1设置为上升沿触发,触发的瞬间会将CNT的值记录到CCR1里面,同时设置通道1触发带有从模式,并把从模式设置为置零CNT。超声波传感器模块上面通常有两个超声波元器件,一个用于发射,一个用于接收。电路板上有四个引脚:VCC、GND、Trig(触发)、Echo(回应)。输入捕获原理可以看看这位大佬,感觉写的很清楚,工作电压与电流:5V,15mA。感测角度:不小于15度。

qq_62425900的博客 2082

UNIX下C语言编程与实践14-UNIX 文件系统格式化:磁盘分区与文件系统创建原理

基于Kafka+ElasticSearch+MongoDB+Redis+XXL-Job日志分析系统(学习)配置即权限:从传统开源 RBAC 框架到 SPARK 的六层数据护盾,告别改权限就要改代码的魔咒。c++&MFC 用boost.asio库写的串口功能,包含发送、异步接收、打开、重连、关闭功能。Windows下NVM保姆级指南:安装、切换版本、指定路径+淘宝镜像配置,一次搞定!第2集:技术选型的智慧:Flask vs FastAPI,GLM-4 vs GPT。

AA_vvt1s的博客 414

2024最新版Anaconda Navigator安装使用指南

本文简要介绍如何下载安装Anaconda Navigator最新版,以及如何使用Python内核搭载来完成应用程序开发。

jackson_lingua的博客 1万+

开源监控工具部署与评估指南:从环境配置到API集成

监控与数据采集是自动化运维和数据分析的基础技术,其核心原理在于通过程序化方式定期或实时地从指定数据源获取信息,并进行状态检查、异常检测与结果聚合。这项技术的价值在于将人工巡检转变为自动化、可扩展的任务流,显著提升运维效率和系统可观测性。在工程实践中,此类工具广泛应用于服务状态监控、数据聚合追踪、资讯内容监听等场景,通常需要关注其部署便捷性、资源占用以及API集成能力。本文以典型开源监控项目为例,深入探讨其环境准备、功能验证及批量任务管理等关键实践,其中Docker容器化部署和RESTful API调用是提升

weixin_30321709的博客 428

SAP 生产订单工序删除状态撤回简介

2、我们在前台对于生产订单的状态一个是在生产订单抬头中的状态,另外一个是针对上面一直在测试的是工序的状态,这个两个状态是前台可见的状态,实际生产订单还有一个组件物料的状态,这个状态是不可见的一个状态。生产订单中的0010工序中对应的组件的栏位被标识,表示有物料分配到了0010的工序中,正常情况下0010的工序被分配了工序是不应该被删除的。将已领料的工序删除掉,导致异常的发生。当物料已领料会,用户仍然把已分配的物料的工序删除掉,导致后续业务操作过程中产生异常,如下图中所示,0010的工序被打上了删除标识。

我小时候很黑的博客 1939

Vue+OpenLayers6一本通,OpenLayers6完整教程大全,OpenLayers6全知全解,OpenLayers6入门、实战、扩展进阶三合一专栏目录汇总

Gis开发入门,OpenLayers、Leaflet、Maplibre-gl和Cesiumjs地图引擎介绍以及几种地图服务vms、vmts介绍OpenLayers介绍,相比较其他地图引擎有什么优点OpenLayers7.x版本和6.x版本有什么区别,是否需要升级到OpenLayers7版本?OpenLayers6实战:OpenLayers创建自定义控件,以创建一个地图复位控件为例OpenLayers6实战,OpenLayers创建自定义鹰眼控件,自定义鹰眼控件样式,调整鹰眼控件位置、大小、文字和按钮等样式。

GIS技术方向,GIS地图技术分享(GIS入门、Openlayers教程、Leaflet教程),做最好的GIS地图开发教程。关注博主公众号:eguid 2199

【Elasticsearch】集群配置深度解析与实践

基于Kafka+ElasticSearch+MongoDB+Redis+XXL-Job日志分析系统(学习)配置即权限:从传统开源 RBAC 框架到 SPARK 的六层数据护盾,告别改权限就要改代码的魔咒。c++&MFC 用boost.asio库写的串口功能,包含发送、异步接收、打开、重连、关闭功能。Windows下NVM保姆级指南:安装、切换版本、指定路径+淘宝镜像配置,一次搞定!第2集:技术选型的智慧:Flask vs FastAPI,GLM-4 vs GPT。

guojuan0525的博客 416

【学前须知】推荐阅读顺序

AUTOSAR学习笔记专栏文章推荐阅读顺序

weixin_47571898的博客 743

Spring Boot 集成 Redis 全方位详解

基于Kafka+ElasticSearch+MongoDB+Redis+XXL-Job日志分析系统(学习)配置即权限:从传统开源 RBAC 框架到 SPARK 的六层数据护盾,告别改权限就要改代码的魔咒。c++&MFC 用boost.asio库写的串口功能,包含发送、异步接收、打开、重连、关闭功能。Windows下NVM保姆级指南:安装、切换版本、指定路径+淘宝镜像配置,一次搞定!第2集:技术选型的智慧:Flask vs FastAPI,GLM-4 vs GPT。

slngX_357zp的博客 338

点云处理指南介绍

指南目录,Open3D与PCL内容介绍

NNNNNathan的博客 1万+

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

Spring Boot 缓存技术

基于Kafka+ElasticSearch+MongoDB+Redis+XXL-Job日志分析系统(学习)配置即权限:从传统开源 RBAC 框架到 SPARK 的六层数据护盾,告别改权限就要改代码的魔咒。c++&MFC 用boost.asio库写的串口功能,包含发送、异步接收、打开、重连、关闭功能。Windows下NVM保姆级指南:安装、切换版本、指定路径+淘宝镜像配置,一次搞定!第2集:技术选型的智慧:Flask vs FastAPI,GLM-4 vs GPT。

AA_ptpt5的博客 237

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

技术资源聚合站部署与自动化集成实战指南

在信息爆炸的时代,技术资源聚合站通过结构化方式整合开发工具、学习资料和开源项目,成为提升信息获取效率的关键。其核心原理在于将分散的资源进行统一分类、索引和检索,实现从海量信息到精准内容的转化。这类平台的技术价值在于提供了可编程的数据接口和本地化部署能力,使得开发者能够将静态资源列表转化为可自动化处理的动态资产。典型的应用场景包括构建个人知识库、团队内部工具导航以及技术资讯的自动化聚合。通过Docker容器化部署和API接口调用,可以实现资源的批量管理与集成,这正是评估和运用类似“粉丝空间站”项目的核心实践路

weixin_30691871的博客 396

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

SQLite 简介

基于Kafka+ElasticSearch+MongoDB+Redis+XXL-Job日志分析系统(学习)配置即权限:从传统开源 RBAC 框架到 SPARK 的六层数据护盾,告别改权限就要改代码的魔咒。c++&MFC 用boost.asio库写的串口功能,包含发送、异步接收、打开、重连、关闭功能。Windows下NVM保姆级指南:安装、切换版本、指定路径+淘宝镜像配置,一次搞定!第2集:技术选型的智慧:Flask vs FastAPI,GLM-4 vs GPT。

slngX_1wv5a的博客 290

打造个人写作主权空间:Hugo静态博客搭建实践

静态博客是一种将内容与呈现分离的轻量级技术写作范式,其核心在于通过纯文本(Markdown)+ 开源生成器(如Hugo)+ Git版本控制,构建可迁移、可离线、高可控的数字写作空间。相比依赖平台算法的内容分发模式,静态博客强调内容主权、长期存档与工程化写作流程,天然适配技术文档、嵌入式开发笔记、Linux内核调试记录等需要精准表达与版本追溯的场景。它不追求流量爆发,而聚焦于真实知识沉淀与可持续输出——whthomas.dev正是这一理念的典型落地,从本地VS Code写作、Hugo一键构建,到Cloudfl

SailingAptech的专栏 455

云原生技术问题

整体流程涉及DNS、ARP、TCP、IP、HTTP核心协议,完整过程如下:首先进行DNS解析,将域名解析为服务器IP,依次查询浏览器缓存、系统hosts、本地DNS缓存,未命中则向DNS服务器迭代查询。核心区别:TCP是面向连接的可靠传输协议,需三次握手建连,具备重传、校验、流量控制、拥塞控制机制,报文开销大、传输延迟高;traceroute利用ICMP超时报文:通过逐级递增数据包TTL值,每经过一台路由器TTL减1,TTL归零后路由器返回ICMP超时报文,以此获取链路所有路由节点IP,定位故障链路。

2303_81849819的博客 464

Redis 从 0 到 1(二十七):JSON 体积是 Protobuf 的 3.7 倍?3 种序列化的 Go 基准测试

缓存对象直接用 json.Marshal,1 亿条 key 多花 20G 内存和 3 倍带宽。本文对 JSON / MessagePack / Protobuf 做 Go 基准测试(体积、编解码耗时、CPU),给出不同场景的选型表和统一序列化封装。

2501_92769340的博客 174

cpuprof.go开关、缓冲区与记账

本文详解Go语言CPU剖析的全链路机制,核心为定时信号触发栈采样、环形缓冲存储与异步写入。通过实验验证了64帧栈深度限制、1MiB缓冲区容量及样本丢失计数机制,揭示了SetCPUProfileRate控制采样频率、add函数处理信号中断、addExtra上报丢失样本等关键流程,并解析了跨平台差异与废弃特性。

不愿言弃-念何的博客 352

联犀 Go SIMD 实战:视频转码从 16.5ms 到 5.46ms

联犀Go视频转码实战:在352×288测试流中单帧单核CPU耗时从16.5ms到5.46ms,区分SIMD、代码及编码策略的综合收益。保留测试条件、压测结果与适用边界。

QQ603785348的博客 332

纯 Go 写的 JS 引擎,跑分超过了 C 语言的 QuickJS

moejs 是一个纯 Go 编写的高性能 JavaScript 运行时,专为 Go 程序中的插件沙箱场景设计。发布于 2026 年 11 月,由 new-api 维护者 Calcium-Ion 开发,相较 C 版 QuickJS 快 35%,内存占用仅 81 KiB,单次调用仅 6.9 微秒。支持快速复用运行时、零拷贝解析、安全沙箱与 TypeScript 直接编译,通过 test262 80971 个用例零失败。适合高并发插件系统,如 AI 网关钩子处理,已在实际场景中实现请求吞吐量提升 8.6 倍。仓库

前端之虎陈随易 194

Go AI Agent 实战 30|Human-in-the-loop:关键操作前的人工确认

本课聚焦高风险操作的审批机制,强调仅靠Prompt无法保障安全,需在Go后端实现严格的HITL流程。通过定义waiting_approval状态、绑定具体操作与版本校验,确保审批可追溯、防并发漂移。示例展示任务状态管理、审批请求持久化及批准后重新验证逻辑,实现安全可靠的高风险操作执行。

2302_80713883的博客 248

运行go web服务,检测mysql服务和端口

如果你的窗口里显示的是别的报错文字(比如 ‘xxx’ 不是内部或外部命令、系统找不到指定的路径、或 Go 编译报错),把那几行原文发我,我再精确修——不过现在这版 bat 已经把最常见的三个坑(数据库、端口、闪退看不到错误)都堵上了。启动 MySQL:用干净的 my.ini 直接起 mysqld.exe(绕开 phpstudy 面板配置),验证连通:oa_admin_rule 554 行、oa_admin 2 行,数据完好。服务本身还能起来,但登录、菜单全依赖数据库,所以你看到的是红色警告 + 登录失效。

xinhuanjieyi的博客 159
上一篇: 【Gin框架入门到精通系列】全系列导航手册
下一篇: 【Go语言爬虫系列05】反爬虫策略应对技术
Gopher部落
博客等级 码龄2年 784粉丝 92原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Gopher部落

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值