Python/Node.js 调用淘宝关键词搜索 API:商品数据采集开发实战

62 阅读7分钟

 在电商数据分析、价格监控和市场调研等场景中,获取淘宝平台的商品数据至关重要。本文将分别使用 Python 和 Node.js 两种编程语言,详细介绍如何调用淘宝关键词搜索 API,实现商品数据的采集,并提供完整的代码实现和实践指南。

淘宝 API 接入准备

在接入淘宝 API 之前,需要完成以下准备工作:

  1. 注册账号:注册账号并完成实名认证。
  2. 创建应用:获取 ApiKey 和 ApiSecret。
  3. 申请 API 权限:在应用管理中申请taobao.tbk.dg.material.optional接口的访问权限。
  4. 了解 API 参数:熟悉 API 的请求参数和返回格式,例如关键词搜索、排序方式、分页等。

Python 实现方案

下面是使用 Python 调用淘宝关键词搜索 API 的完整代码实现:

import requests
import json
import time
import hmac
import hashlib
from urllib.parse import urlencode
import os
from dotenv import load_dotenv

class TaobaoAPI:
    def __init__(self, app_key, app_secret, server_url='https://gw.api.taobao.com/router/rest'):
        """初始化淘宝API客户端"""
        self.app_key = app_key
        self.app_secret = app_secret
        self.server_url = server_url
    
    def generate_sign(self, params):
        """生成API请求签名"""
        # 对参数进行排序
        sorted_params = sorted(params.items(), key=lambda x: x[0])
        # 拼接参数字符串
        sign_str = self.app_secret
        for k, v in sorted_params:
            sign_str += f"{k}{v}"
        sign_str += self.app_secret
        
        # 使用HMAC-SHA1算法生成签名
        sign = hmac.new(
            self.app_secret.encode('utf-8'),
            sign_str.encode('utf-8'),
            hashlib.sha1
        ).hexdigest().upper()
        
        return sign
    
    def call(self, method, params):
        """调用淘宝API接口"""
        # 公共参数
        public_params = {
            'app_key': self.app_key,
            'method': method,
            'format': 'json',
            'v': '2.0',
            'sign_method': 'hmac',
            'timestamp': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
        }
        
        # 合并公共参数和业务参数
        all_params = {**public_params, **params}
        
        # 生成签名
        sign = self.generate_sign(all_params)
        all_params['sign'] = sign
        
        # 发送请求
        try:
            response = requests.post(self.server_url, data=all_params)
            response.raise_for_status()  # 检查请求是否成功
            result = response.json()
            return result
        except requests.exceptions.RequestException as e:
            print(f"请求出错: {e}")
            return None
        except json.JSONDecodeError as e:
            print(f"JSON解析出错: {e}")
            return None

def search_taobao_items(app_key, app_secret, keyword, page_no=1, page_size=20, sort='default'):
    """搜索淘宝商品"""
    api = TaobaoAPI(app_key, app_secret)
    
    # 业务参数
    params = {
        'q': keyword,
        'page_no': page_no,
        'page_size': page_size,
        'sort': sort,  # 排序方式:default(默认),price-asc(价格从低到高),price-desc(价格从高到低),sale-desc(销量从高到低)
        'has_coupon': 'true',  # 只搜索有优惠券的商品
        'need_free_shipment': 'true',  # 只搜索包邮商品
    }
    
    # 调用淘宝客商品搜索API
    result = api.call('taobao.tbk.dg.material.optional', params)
    
    if result:
        # 处理返回结果
        if 'tbk_dg_material_optional_response' in result:
            response = result['tbk_dg_material_optional_response']
            if 'result_list' in response and 'map_data' in response['result_list']:
                items = response['result_list']['map_data']
                print(f"第 {page_no} 页搜索结果: 找到 {len(items)} 个商品")
                return items
            else:
                print(f"第 {page_no} 页未找到商品数据")
        else:
            print(f"API调用失败: {result}")
    return []

def search_all_pages(app_key, app_secret, keyword, max_pages=10, sort='default'):
    """搜索多页商品数据"""
    all_items = []
    for page in range(1, max_pages + 1):
        print(f"正在搜索第 {page} 页...")
        items = search_taobao_items(app_key, app_secret, keyword, page, sort=sort)
        if not items:
            break  # 没有更多数据,退出循环
        all_items.extend(items)
        # 控制请求频率,避免被限流
        time.sleep(1)
    
    print(f"总共搜索到 {len(all_items)} 个商品")
    return all_items

def save_to_csv(items, filename='taobao_items.csv'):
    """将商品数据保存到CSV文件"""
    import csv
    if not items:
        print("没有数据可保存")
        return
    
    # 定义CSV文件的列名
    fieldnames = ['title', 'price', 'original_price', 'sales_volume', 'coupon_amount', 'shop_title', 'item_url']
    
    with open(filename, 'w', newline='', encoding='utf-8-sig') as csvfile:
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
        
        # 写入表头
        writer.writeheader()
        
        # 写入数据
        for item in items:
            # 提取需要的字段
            row = {
                'title': item.get('title', ''),
                'price': item.get('zk_final_price', ''),
                'original_price': item.get('reserve_price', ''),
                'sales_volume': item.get('volume', ''),
                'coupon_amount': item.get('coupon_amount', ''),
                'shop_title': item.get('shop_title', ''),
                'item_url': item.get('item_url', '')
            }
            writer.writerow(row)
    
    print(f"数据已保存到 {filename}")

if __name__ == "__main__":
    # 加载环境变量
    load_dotenv()
    
    # 获取AppKey和AppSecret
    app_key = os.getenv("TAOBAO_APP_KEY")
    app_secret = os.getenv("TAOBAO_APP_SECRET")
    
    if not app_key or not app_secret:
        print("请设置TAOBAO_APP_KEY和TAOBAO_APP_SECRET环境变量")
        exit(1)
    
    # 搜索关键词
    keyword = input("请输入搜索关键词: ")
    
    # 搜索商品
    items = search_all_pages(app_key, app_secret, keyword, max_pages=5, sort='price-asc')
    
    # 保存数据
    if items:
        save_to_csv(items, f"{keyword}_商品数据.csv")    

 

Node.js 实现方案

下面是使用 Node.js 调用淘宝关键词搜索 API 的完整代码实现:

const https = require('https');
const crypto = require('crypto');
const querystring = require('querystring');
const fs = require('fs');
const path = require('path');
require('dotenv').config();

class TaobaoAPI {
    constructor(appKey, appSecret, serverUrl = 'https://gw.api.taobao.com/router/rest') {
        this.appKey = appKey;
        this.appSecret = appSecret;
        this.serverUrl = serverUrl;
    }

    /**
     * 生成API请求签名
     * @param {Object} params 请求参数
     * @returns {string} 签名结果
     */
    generateSign(params) {
        // 对参数进行排序
        const sortedParams = Object.keys(params).sort().reduce((acc, key) => {
            acc[key] = params[key];
            return acc;
        }, {});

        // 拼接参数字符串
        let signStr = this.appSecret;
        for (const key in sortedParams) {
            signStr += `${key}${sortedParams[key]}`;
        }
        signStr += this.appSecret;

        // 使用HMAC-SHA1算法生成签名
        const sign = crypto
            .createHmac('sha1', this.appSecret)
            .update(signStr)
            .digest('hex')
            .toUpperCase();

        return sign;
    }

    /**
     * 调用淘宝API接口
     * @param {string} method API方法名
     * @param {Object} params 业务参数
     * @returns {Promise<Object>} API响应结果
     */
    call(method, params) {
        return new Promise((resolve, reject) => {
            // 公共参数
            const publicParams = {
                app_key: this.appKey,
                method: method,
                format: 'json',
                v: '2.0',
                sign_method: 'hmac',
                timestamp: new Date().toISOString().replace('T', ' ').substring(0, 19)
            };

            // 合并公共参数和业务参数
            const allParams = { ...publicParams, ...params };

            // 生成签名
            const sign = this.generateSign(allParams);
            allParams.sign = sign;

            // 解析服务器URL
            const url = new URL(this.serverUrl);

            // 准备请求参数
            const postData = querystring.stringify(allParams);

            // 准备HTTP请求选项
            const options = {
                hostname: url.hostname,
                port: url.port || 443,
                path: url.pathname,
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'Content-Length': Buffer.byteLength(postData)
                }
            };

            // 发送HTTP请求
            const req = https.request(options, (res) => {
                let data = '';

                res.on('data', (chunk) => {
                    data += chunk;
                });

                res.on('end', () => {
                    try {
                        const result = JSON.parse(data);
                        resolve(result);
                    } catch (error) {
                        reject(new Error(`JSON解析错误: ${error.message}`));
                    }
                });
            });

            req.on('error', (error) => {
                reject(new Error(`请求错误: ${error.message}`));
            });

            req.write(postData);
            req.end();
        });
    }
}

/**
 * 搜索淘宝商品
 * @param {string} appKey 应用Key
 * @param {string} appSecret 应用Secret
 * @param {string} keyword 搜索关键词
 * @param {number} pageNo 页码
 * @param {number} pageSize 每页数量
 * @param {string} sort 排序方式
 * @returns {Promise<Object[]>} 商品列表
 */
async function searchTaobaoItems(appKey, appSecret, keyword, pageNo = 1, pageSize = 20, sort = 'default') {
    const api = new TaobaoAPI(appKey, appSecret);

    // 业务参数
    const params = {
        q: keyword,
        page_no: pageNo,
        page_size: pageSize,
        sort: sort, // 排序方式:default(默认),price-asc(价格从低到高),price-desc(价格从高到低),sale-desc(销量从高到低)
        has_coupon: 'true', // 只搜索有优惠券的商品
        need_free_shipment: 'true' // 只搜索包邮商品
    };

    try {
        // 调用淘宝客商品搜索API
        const result = await api.call('taobao.tbk.dg.material.optional', params);

        if (result) {
            // 处理返回结果
            if (result.tbk_dg_material_optional_response) {
                const response = result.tbk_dg_material_optional_response;
                if (response.result_list && response.result_list.map_data) {
                    const items = response.result_list.map_data;
                    console.log(`第 ${pageNo} 页搜索结果: 找到 ${items.length} 个商品`);
                    return items;
                } else {
                    console.log(`第 ${pageNo} 页未找到商品数据`);
                    return [];
                }
            } else {
                console.log(`API调用失败:`, result);
                return [];
            }
        } else {
            console.log('API调用无响应');
            return [];
        }
    } catch (error) {
        console.error(`搜索出错: ${error.message}`);
        return [];
    }
}

/**
 * 搜索多页商品数据
 * @param {string} appKey 应用Key
 * @param {string} appSecret 应用Secret
 * @param {string} keyword 搜索关键词
 * @param {number} maxPages 最大搜索页数
 * @param {string} sort 排序方式
 * @returns {Promise<Object[]>} 所有商品列表
 */
async function searchAllPages(appKey, appSecret, keyword, maxPages = 10, sort = 'default') {
    let allItems = [];
    for (let page = 1; page <= maxPages; page++) {
        console.log(`正在搜索第 ${page} 页...`);
        const items = await searchTaobaoItems(appKey, appSecret, keyword, page, 20, sort);
        if (items.length === 0) {
            break; // 没有更多数据,退出循环
        }
        allItems = allItems.concat(items);
        
        // 控制请求频率,避免被限流
        await new Promise(resolve => setTimeout(resolve, 1000));
    }

    console.log(`总共搜索到 ${allItems.length} 个商品`);
    return allItems;
}

/**
 * 将商品数据保存到CSV文件
 * @param {Object[]} items 商品列表
 * @param {string} filename 文件名
 */
function saveToCSV(items, filename = 'taobao_items.csv') {
    if (items.length === 0) {
        console.log("没有数据可保存");
        return;
    }

    // 创建CSV内容
    let csvContent = '标题,价格,原价,销量,优惠券金额,店铺名称,商品链接\n';
    
    items.forEach(item => {
        const row = [
            item.title || '',
            item.zk_final_price || '',
            item.reserve_price || '',
            item.volume || '',
            item.coupon_amount || '',
            item.shop_title || '',
            item.item_url || ''
        ];
        
        // 转义CSV中的特殊字符
        const escapedRow = row.map(value => {
            if (typeof value === 'string') {
                return `"${value.replace(/"/g, '""')}"`;
            }
            return value;
        });
        
        csvContent += escapedRow.join(',') + '\n';
    });

    // 写入文件
    fs.writeFile(filename, csvContent, 'utf8', (err) => {
        if (err) {
            console.error(`保存CSV文件出错: ${err.message}`);
        } else {
            console.log(`数据已保存到 ${filename}`);
        }
    });
}

async function main() {
    // 获取AppKey和AppSecret
    const appKey = process.env.TAOBAO_APP_KEY;
    const appSecret = process.env.TAOBAO_APP_SECRET;

    if (!appKey || !appSecret) {
        console.error("请设置TAOBAO_APP_KEY和TAOBAO_APP_SECRET环境变量");
        return;
    }

    // 搜索关键词
    const readline = require('readline').createInterface({
        input: process.stdin,
        output: process.stdout
    });

    readline.question('请输入搜索关键词: ', async (keyword) => {
        readline.close();

        try {
            // 搜索商品
            const items = await searchAllPages(appKey, appSecret, keyword, 5, 'price-asc');
            
            // 保存数据
            if (items.length > 0) {
                saveToCSV(items, `${keyword}_商品数据.csv`);
            }
        } catch (error) {
            console.error(`程序执行出错: ${error.message}`);
        }
    });
}

// 执行主函数
main();    

 

开发注意事项与避坑指南

在接入淘宝 API 时,需要注意以下几点:

  1. API 权限问题

    • 需要申请taobao.tbk.dg.material.optional接口的访问权限
    • 部分高级功能需要成为淘宝联盟会员并绑定推广位
  2. 签名生成规则

    • 参数必须按字典序排序
    • 签名过程中不要对参数名和参数值进行 URL 编码
    • 签名结果需要转换为大写
  3. 频率限制

    • 免费应用有 QPS 限制,建议每次请求间隔 1 秒以上
    • 超出频率限制会返回错误码isv.access-control-exceed-limit
  4. 数据解析注意事项

    • 检查返回结果中是否包含预期的字段
    • 处理可能的空值或异常数据格式
    • 商品图片 URL 可能需要添加协议前缀
  5. 合规使用

    • 采集的数据仅用于个人学习或合法商业用途
    • 避免过度频繁请求,以免影响 API 服务稳定性
    • 遵守协议

扩展功能建议

  1. 数据可视化:使用 Matplotlib (Python) 或 Chart.js ( Node.js ) 将价格分布等数据可视化
  2. 定时任务:使用 APScheduler (Python) 或 node-cron ( Node.js ) 实现定时采集数据
  3. 数据库存储:将数据存入 MySQL 或 MongoDB 等数据库进行长期分析
  4. 异常处理增强:添加重试机制和更完善的错误处理

通过以上步骤,你可以使用 Python 或 Node.js 快速接入淘宝关键词搜索 API,实现海量商品数据的采集和分析。根据实际需求,你还可以进一步扩展功能,构建更强大的电商数据分析系统。