Go 语言并发采集淘宝商品数据:利用 API 实现高性能抓取

在电商数据分析、价格监控等场景中,高效采集商品数据是一项常见需求。本文将介绍如何使用 Go 语言的并发特性,结合淘宝 API,实现高性能的商品数据采集系统。

为什么选择 Go 语言?

Go 语言(Golang)在并发处理方面具有天然优势:

  • 轻量级的 goroutine 比传统线程更节省资源
  • 内置的 channel 机制简化了并发通信
  • 优秀的标准库提供了丰富的网络操作和并发控制工具
  • 原生支持的 JSON 处理非常适合 API 数据交互

淘宝平台 API 准备

在开始开发前,需要先完成以下准备工作:

  1. 注册开发者账号
  2. 获取 Api Key 和 Api Secret
  3. 申请所需的商品数据 API 权限(如 item_get 接口)
  4. 了解 API 调用规范和限流策略

实现方案设计

我们的采集系统将包含以下核心组件:

  • 配置管理:处理 API 密钥、请求参数等
  • 签名工具:生成符合淘宝 API 要求的签名
  • 并发控制器:控制 goroutine 数量,避免触发限流
  • 数据处理器:解析、存储采集到的商品数据

代码实现

下面是完整的实现代码,包含了并发控制、错误处理和数据采集功能:

go 复制代码
package main

import (
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base64"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"
)

// 配置信息
type Config struct {
	AppKey    string
	AppSecret string
	APIUrl    string
	MaxConns  int           // 最大并发数
	Timeout   time.Duration // 请求超时时间
}

// 商品信息结构体
type Product struct {
	ItemId    string `json:"item_id"`
	Title     string `json:"title"`
	Price     string `json:"price"`
	Sales     string `json:"sales"`
	ShopName  string `json:"shop_name"`
	CreatedAt string `json:"created_at"`
}

// API响应结构体
type ApiResponse struct {
	Code      int         `json:"code"`
	Msg       string      `json:"msg"`
	Data      Product     `json:"data"`
	RequestId string      `json:"request_id"`
}

// 初始化配置
func NewConfig(appKey, appSecret string) *Config {
	return &Config{
		AppKey:    appKey,
		AppSecret: appSecret,
		APIUrl:    "http://gw.api.taobao.com/router/rest",
		MaxConns:  10,         // 默认最大并发数
		Timeout:   30 * time.Second, // 默认超时时间
	}
}

// 生成签名
func (c *Config) GenerateSign(params map[string]string) string {
	// 1. 按键名排序
	keys := make([]string, 0, len(params))
	for k := range params {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	// 2. 拼接参数
	var paramStr strings.Builder
	for _, k := range keys {
		paramStr.WriteString(k)
		paramStr.WriteString(params[k])
	}

	// 3. 拼接AppSecret
	signStr := c.AppSecret + paramStr.String() + c.AppSecret

	// 4. 计算HMAC-SHA1
	mac := hmac.New(sha1.New, []byte(c.AppSecret))
	mac.Write([]byte(signStr))
	signBytes := mac.Sum(nil)

	// 5. Base64编码并转为大写
	return strings.ToUpper(base64.StdEncoding.EncodeToString(signBytes))
}

// 构建API请求参数
func (c *Config) buildParams(itemId string) map[string]string {
	params := make(map[string]string)
	params["app_key"] = c.AppKey
	params["format"] = "json"
	params["method"] = "taobao.item.get"
	params["timestamp"] = time.Now().Format("2006-01-02 15:04:05")
	params["v"] = "2.0"
	params["fields"] = "item_id,title,price,sales,shop_name,created"
	params["num_iid"] = itemId
	
	// 生成签名
	params["sign"] = c.GenerateSign(params)
	
	return params
}

// 发送API请求
func (c *Config) FetchProduct(itemId string) (*Product, error) {
	params := c.buildParams(itemId)
	
	// 构建请求URL
	urlValues := url.Values{}
	for k, v := range params {
		urlValues.Set(k, v)
	}
	
	fullUrl := fmt.Sprintf("%s?%s", c.APIUrl, urlValues.Encode())
	
	// 创建HTTP客户端
	client := &http.Client{
		Timeout: c.Timeout,
	}
	
	// 发送请求
	resp, err := client.Get(fullUrl)
	if err != nil {
		return nil, fmt.Errorf("请求失败: %v", err)
	}
	defer resp.Body.Close()
	
	// 读取响应内容
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("读取响应失败: %v", err)
	}
	
	// 解析JSON
	var apiResp ApiResponse
	if err := json.Unmarshal(body, &apiResp); err != nil {
		return nil, fmt.Errorf("解析JSON失败: %v, 响应内容: %s", err, string(body))
	}
	
	// 检查API返回状态
	if apiResp.Code != 0 {
		return nil, fmt.Errorf("API错误: %s (代码: %d)", apiResp.Msg, apiResp.Code)
	}
	
	return &apiResp.Data, nil
}

// 并发采集商品数据
func BatchFetchProducts(config *Config, itemIds []string, resultChan chan<- *Product, errChan chan<- error, wg *sync.WaitGroup) {
	defer wg.Done()
	
	for _, itemId := range itemIds {
		product, err := config.FetchProduct(itemId)
		if err != nil {
			errChan <- fmt.Errorf("获取商品 %s 失败: %v", itemId, err)
			continue
		}
		
		resultChan <- product
		fmt.Printf("成功获取商品: %s - %s\n", product.ItemId, product.Title)
		
		// 简单限流,根据API实际限额调整
		time.Sleep(100 * time.Millisecond)
	}
}

// 将结果保存到文件
func SaveResults(products []*Product, filename string) error {
	if len(products) == 0 {
		return errors.New("没有数据可保存")
	}
	
	data, err := json.MarshalIndent(products, "", "  ")
	if err != nil {
		return fmt.Errorf("序列化数据失败: %v", err)
	}
	
	return ioutil.WriteFile(filename, data, 0644)
}

// 分割任务列表
func SplitTaskList(list []string, num int) [][]string {
	var result [][]string
	length := len(list)
	if length == 0 {
		return result
	}
	
	// 计算每个分片的大小
	chunkSize := (length + num - 1) / num
	
	for i := 0; i < length; i += chunkSize {
		end := i + chunkSize
		if end > length {
			end = length
		}
		result = append(result, list[i:end])
	}
	
	return result
}

func main() {
	// 解析命令行参数
	appKey := flag.String("appkey", "", "淘宝开放平台AppKey")
	appSecret := flag.String("secret", "", "淘宝开放平台AppSecret")
	inputFile := flag.String("input", "item_ids.txt", "包含商品ID的文件路径")
	outputFile := flag.String("output", "products.json", "输出结果文件路径")
	maxConns := flag.Int("conns", 10, "最大并发数")
	flag.Parse()
	
	// 验证必要参数
	if *appKey == "" || *appSecret == "" {
		fmt.Println("请提供AppKey和AppSecret")
		flag.Usage()
		os.Exit(1)
	}
	
	// 读取商品ID列表
	content, err := ioutil.ReadFile(*inputFile)
	if err != nil {
		fmt.Printf("读取商品ID文件失败: %v\n", err)
		os.Exit(1)
	}
	
	itemIds := strings.Fields(string(content))
	if len(itemIds) == 0 {
		fmt.Println("没有找到商品ID")
		os.Exit(1)
	}
	
	fmt.Printf("共发现 %d 个商品ID,准备开始采集...\n", len(itemIds))
	
	// 初始化配置
	config := NewConfig(*appKey, *appSecret)
	config.MaxConns = *maxConns
	
	// 分割任务
	taskChunks := SplitTaskList(itemIds, config.MaxConns)
	
	// 创建通道和等待组
	resultChan := make(chan *Product, len(itemIds))
	errChan := make(chan error, len(itemIds))
	var wg sync.WaitGroup
	
	// 启动工作goroutine
	startTime := time.Now()
	wg.Add(len(taskChunks))
	for _, chunk := range taskChunks {
		go BatchFetchProducts(config, chunk, resultChan, errChan, &wg)
	}
	
	// 等待所有工作完成并关闭通道
	go func() {
		wg.Wait()
		close(resultChan)
		close(errChan)
	}()
	
	// 收集结果
	var products []*Product
	var errors []error
	
	for product := range resultChan {
		products = append(products, product)
	}
	
	for err := range errChan {
		errors = append(errors, err)
	}
	
	// 输出统计信息
	duration := time.Since(startTime)
	fmt.Printf("\n采集完成!耗时: %v\n", duration)
	fmt.Printf("成功采集: %d 个商品\n", len(products))
	fmt.Printf("采集失败: %d 个商品\n", len(errors))
	
	// 保存成功的结果
	if err := SaveResults(products, *outputFile); err != nil {
		fmt.Printf("保存结果失败: %v\n", err)
	} else {
		fmt.Printf("结果已保存到 %s\n", *outputFile)
	}
	
	// 输出错误信息
	if len(errors) > 0 {
		fmt.Println("\n错误信息:")
		for i, err := range errors {
			if i < 10 { // 只显示前10个错误
				fmt.Printf("- %v\n", err)
			} else {
				fmt.Printf("- 还有 %d 个错误未显示\n", len(errors)-i)
				break
			}
		}
	}
}

代码解析

核心功能模块

  1. 配置管理Config结构体存储 API 密钥、请求 URL 等信息,NewConfig函数用于初始化配置。

  2. 签名生成GenerateSign方法按照淘宝 API 要求,对请求参数进行排序、拼接和加密,生成合法的签名。

  3. API 请求FetchProduct方法负责构建请求、发送 HTTP 请求并解析 JSON 响应,返回商品数据。

  4. 并发控制

    • BatchFetchProducts函数作为工作函数,处理分配的商品 ID 列表
    • SplitTaskList函数将任务均匀分配给不同的 goroutine
    • 使用带缓冲的 channel 收集结果和错误信息
    • 通过 WaitGroup 等待所有 goroutine 完成
  5. 结果处理SaveResults函数将采集到的商品数据保存为 JSON 文件。

使用方法

  1. 准备一个包含商品 ID 的文本文件(每行一个 ID)
  2. 执行命令:

bash

go 复制代码
go run taobao_crawler.go -appkey 你的AppKey -secret 你的AppSecret -input item_ids.txt -output products.json -conns 10

性能优化建议

  1. 动态调整并发数:根据 API 的限流策略和响应速度,动态调整并发数
  2. 实现重试机制:对失败的请求进行有限次数的重试
  3. 添加缓存:缓存已获取的商品数据,避免重复请求
  4. 使用连接池:复用 HTTP 连接,减少握手开销
  5. 监控系统状态:实时监控成功率、响应时间等指标

注意事项

  1. 遵守平台的使用规范,不要进行过度频繁的请求
  2. 注意 API 的调用限额,避免超出配额导致账号受限
  3. 商业使用时请确保符合相关法律法规和平台规定
  4. 定期检查 API 版本更新,及时调整代码以适应接口变化

通过 Go 语言的并发特性和合理的系统设计,我们可以高效、稳定地采集淘宝商品数据,为后续的数据分析和应用开发提供支持。

相关推荐
Java 码农23 分钟前
nodejs koa留言板案例开发
前端·javascript·npm·node.js
ZhuAiQuan1 小时前
[electron]开发环境驱动识别失败
前端·javascript·electron
nyf_unknown1 小时前
(vue)将dify和ragflow页面嵌入到vue3项目
前端·javascript·vue.js
胡gh1 小时前
浏览器:我要用缓存!服务器:你缓存过期了!怎么把数据挽留住,这是个问题。
前端·面试·node.js
你挚爱的强哥1 小时前
SCSS上传图片占位区域样式
前端·css·scss
奶球不是球1 小时前
css新特性
前端·css
Nicholas681 小时前
flutter滚动视图之Viewport、RenderViewport源码解析(六)
前端
无羡仙2 小时前
React 状态更新:如何避免为嵌套数据写一长串 ...?
前端·react.js
TimelessHaze2 小时前
🔥 一文掌握 JavaScript 数组方法(2025 全面指南):分类解析 × 业务场景 × 易错点
前端·javascript·trae
jvxiao3 小时前
搭建个人博客系列--(4) 利用Github Actions自动构建博客
前端