淘宝百亿补贴赛道采集

做电商的朋友应该都有体感:现在想冲销售额,‘百亿补贴’几乎是必选项,尤其是对我们这种非旗舰店、多品牌的C店来说,不上百补真的很难熬。

但问题来了,你点开百补赛道一看,足足500页!靠人工一个个翻?根本不现实。虽然能搜品牌名,但如果我想发掘竞品、找新机会怎么办?

为了解决这个痛点,我写了个采集脚本,亲测有效,一次性就能把数据全部拉下来。

⚠️ 这里必须强调一下哈: 这个脚本纯粹是技术交流和自用分享,不提供任何售后支持。大家用的时候务必注意合规,千万别对平台造成负担或损害,相关风险一定要自负哦。

/**
 * ╔══════════════════════════════════════════════════════════════╗
 * ║   百亿补贴全量赛道数据抓取工具 - 控制台版                    ║
 * ║                                                            ║
 * ║   使用方法:                                                ║
 * ║   1. 在千牛百亿补贴页面按 F12 打开开发者工具                  ║
 * ║   2. 切换到 Console(控制台) 标签                             ║
// ║   3. 粘贴以下全部代码,按回车执行                            ║
 * ║   4. 等待自动完成(约2-3分钟),CSV文件会自动下载              ║
 * ╚══════════════════════════════════════════════════════════════╝
 */
(async function() {
  'use strict';

  const startTime = Date.now();
  console.log('%c🚀 百亿补贴全量赛道抓取工具启动...', 'color:#1F4E79;font-size:14px;font-weight:bold');

  // ========== 配置 ==========
  const API_URL = 'https://sale.taobao.com/list/raceList/queryListData.do';
  const SITE_ID = '127';
  const BIZ_CODE = 'bybt_bidding';
  const CONCURRENCY = 15;        // 并发请求数
  const DELAY_MS = 60;            // 请求间隔(ms)

  // ========== 获取Token ==========
  const tokenMatch = document.cookie.match(/_tb_token_=([^;]+)/);
  if (!tokenMatch) {
    console.error('❌ 未找到 _tb_token_,请确认已登录千牛');
    return;
  }
  const TOKEN = tokenMatch[1];
  console.log(`%c✅ Token获取成功`, 'color:green');

  // ========== 请求函数 ==========
  async function fetchPage(pageNo) {
    const params = new URLSearchParams({
      '_tb_token_': TOKEN,
      'page': String(pageNo),
      'siteId': SITE_ID,
      'bizCode': BIZ_CODE,
      'isYpys': 'false',
      'raceTypes': '0'
    });

    const resp = await fetch(`${API_URL}?${params.toString()}`, {
      method: 'GET',
      credentials: 'include',
      headers: { 'X-Requested-With': 'XMLHttpRequest' }
    });
    return resp.json();
  }

  // ========== 第1页:获取总数 ==========
  console.log('%c📡 正在连接API,获取数据总量...', 'color:#E67E22');
  let firstPage;
  try {
    firstPage = await fetchPage(1);
  } catch(e) {
    console.error('❌ API连接失败:', e.message);
    return;
  }

  const firstList = firstPage?.data?.list || [];
  const totalCount = firstPage?.data?.totalItem || 5000;
  const actualPageSize = firstList.length || 10;
  const totalPages = Math.ceil(totalCount / actualPageSize);

  if (firstList.length === 0) {
    console.error('❌ API未返回数据,响应内容:', JSON.stringify(firstPage).substring(0, 300));
    return;
  }

  console.log(`%c📊 数据概览:`, 'color:#2980B9;font-weight:bold');
  console.log(`   总记录数: ${totalCount}`);
  console.log(`   每页条数: ${actualPageSize}`);
  console.log(`   总页数: ${totalPages}`);
  console.log(`   字段示例: ${Object.keys(firstList[0]).join(', ')}`);

  // ========== 批量抓取所有页面 ==========
  const allData = [...firstList];
  const errors = [];

  console.log(`\n%c⏳ 开始全量抓取 (并发${CONCURRENCY})...`, 'color:#8E44AD;font-weight:bold');

  for (let batchStart = 2; batchStart <= totalPages; batchStart += CONCURRENCY) {
    const batchEnd = Math.min(batchStart + CONCURRENCY - 1, totalPages);

    const promises = [];
    for (let p = batchStart; p <= batchEnd; p++) {
      promises.push(
        fetchPage(p)
          .then(json => json?.data?.list || [])
          .catch(e => {
            errors.push({ page: p, error: e.message });
            return [];
          })
      );
    }

    const results = await Promise.all(promises);
    for (const list of results) allData.push(...list);

    // 进度显示
    const pct = ((batchEnd / totalPages) * 100).toFixed(1);
    const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
    const eta = ((Date.now() - startTime) / batchEnd * (totalPages - batchEnd) / 1000).toFixed(0);

    if (batchEnd % 50 === 0 || batchEnd >= totalPages) {
      console.log(`   📥 进度: ${batchEnd}/${totalPages}页 (${pct}%) | 已获${allData.length}条 | ⏱️${elapsed}s | 预计剩余${eta}s`);
    }

    await new Promise(r => setTimeout(r, DELAY_MS));
    if (allData.length >= totalCount) break;
  }

  // ========== 完成 ==========
  const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);

  console.log('\n' + '═'.repeat(50));
  console.log('%c🎉 抓取完成!', 'color:#27AE60;font-size:16px;font-weight:bold');
  console.log(`   📊 总记录数: ${allData.length}`);
  console.log(`   ⏱️  耗时: ${totalTime}秒`);
  console.log(`   ❌ 失败: ${errors.length}个请求`);
  if (errors.length > 0) {
    console.log(`   🔍 失败详情:`, errors.slice(0, 5));
  }

  // ========== 导出CSV ==========
  // 提取所有字段
  const fieldSet = new Set();
  allData.forEach(item => Object.keys(item).forEach(k => fieldSet.add(k)));
  const fields = [...fieldSet].sort();

  // 特殊字段处理
  function formatValue(val, field) {
    if (val === null || val === undefined) return '';
    if (field === 'raceLabels' && Array.isArray(val)) {
      return val.map(l => l.raceLabelName || '').join('; ');
    }
    if (typeof val === 'boolean') return val ? '是' : '否';
    return String(val);
  }

  // 构建CSV(带BOM头支持中文)
  const csvRows = [fields.join(',')];
  allData.forEach(item => {
    csvRows.push(fields.map(f => {
      let v = formatValue(item[f], f);
      if (v.includes(',') || v.includes('"') || v.includes('\n')) {
        v = '"' + v.replace(/"/g, '""') + '"';
      }
      return v;
    }).join(','));
  });

  const csvContent = '\uFEFF' + csvRows.join('\n');
  const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
  const link = document.createElement('a');
  link.href = URL.createObjectURL(blob);
  link.download = `百亿补贴全量赛道_${totalCount}条_${new Date().toISOString().slice(0,10)}.csv`;
  link.click();

  console.log(`\n%c📥 CSV文件已下载!`, 'color:#27AE60;font-weight:bold');
  console.log(`   文件名: 百亿补贴全量赛道_${totalCount}条_${new Date().toISOString().slice(0,10)}.csv`);

  // 同时保存到全局变量供查看
  window.__BYBT_ALL_TRACKS__ = allData;
  console.log(`\n💡 完整数据已保存到全局变量: window.__BYBT_ALL_TRACKS__`);
  console.log(`   可在控制台输入此变量名查看或进一步操作`);

  // ========== 数据预览统计 ==========
  console.log('\n' + '─'.repeat(50));
  console.log('%c📈 数据预览:', 'color:#2980B9;font-weight:bold');

  // 品牌分布
  const brandCount = {};
  allData.forEach(d => { const b = d.raceBrandName || '未知'; brandCount[b] = (brandCount[b]||0)+1; });
  const topBrands = Object.entries(brandCount).sort((a,b)=>b[1]-a[1]).slice(0,10);
  console.log(`   Top10品牌:`);
  topBrands.forEach(([b,c]) => console.log(`     ${b}: ${c}条`));

  // 类目分布
  const cateCount = {};
  allData.forEach(d => { const c = d.raceCateName || '未知'; cateCount[c] = (cateCount[c]||0)+1; });
  const topCates = Object.entries(cateCount).sort((a,b)=>b[1]-a[1]).slice(0,10);
  console.log(`   Top10类目:`);
  topCates.forEach(([c,n]) => console.log(`     ${c}: ${n}条`));

  // 营销玩法分布
  const sceneCount = {};
  allData.forEach(d => { const s = d.sceneName || '未知'; sceneCount[s] = (sceneCount[s]||0)+1; });
  console.log(`   营销玩法分布:`);
  Object.entries(sceneCount).sort((a,b)=>b[1]-a[1]).forEach(([s,n]) => console.log(`     ${s}: ${n}条`));

  console.log('\n' + '═'.repeat(50));
})();