在电商数据分析、价格监控等场景中,高效采集商品数据是一项常见需求。本文将介绍如何使用 Go 语言的并发特性,结合淘宝 API,实现高性能的商品数据采集系统。
为什么选择 Go 语言?
Go 语言(Golang)在并发处理方面具有天然优势:
- 轻量级的 goroutine 比传统线程更节省资源
- 内置的 channel 机制简化了并发通信
- 优秀的标准库提供了丰富的网络操作和并发控制工具
- 原生支持的 JSON 处理非常适合 API 数据交互
淘宝平台 API 准备
在开始开发前,需要先完成以下准备工作:
- 注册开发者账号
- 获取 Api Key 和 Api Secret
- 申请所需的商品数据 API 权限(如 item_get 接口)
- 了解 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
}
}
}
}
代码解析
核心功能模块
-
配置管理 :
Config
结构体存储 API 密钥、请求 URL 等信息,NewConfig
函数用于初始化配置。 -
签名生成 :
GenerateSign
方法按照淘宝 API 要求,对请求参数进行排序、拼接和加密,生成合法的签名。 -
API 请求 :
FetchProduct
方法负责构建请求、发送 HTTP 请求并解析 JSON 响应,返回商品数据。 -
并发控制:
BatchFetchProducts
函数作为工作函数,处理分配的商品 ID 列表SplitTaskList
函数将任务均匀分配给不同的 goroutine- 使用带缓冲的 channel 收集结果和错误信息
- 通过 WaitGroup 等待所有 goroutine 完成
-
结果处理 :
SaveResults
函数将采集到的商品数据保存为 JSON 文件。
使用方法
- 准备一个包含商品 ID 的文本文件(每行一个 ID)
- 执行命令:
bash
go
go run taobao_crawler.go -appkey 你的AppKey -secret 你的AppSecret -input item_ids.txt -output products.json -conns 10
性能优化建议
- 动态调整并发数:根据 API 的限流策略和响应速度,动态调整并发数
- 实现重试机制:对失败的请求进行有限次数的重试
- 添加缓存:缓存已获取的商品数据,避免重复请求
- 使用连接池:复用 HTTP 连接,减少握手开销
- 监控系统状态:实时监控成功率、响应时间等指标
注意事项
- 遵守平台的使用规范,不要进行过度频繁的请求
- 注意 API 的调用限额,避免超出配额导致账号受限
- 商业使用时请确保符合相关法律法规和平台规定
- 定期检查 API 版本更新,及时调整代码以适应接口变化
通过 Go 语言的并发特性和合理的系统设计,我们可以高效、稳定地采集淘宝商品数据,为后续的数据分析和应用开发提供支持。