您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
从淘宝搜索页面按照销量排序抓取商品销量数据并导出Excel
// ==UserScript== // @name 淘宝商品销量抓取工具 // @namespace http://tampermonkey.net/ // @license MIT // @version 0.2 // @description 从淘宝搜索页面按照销量排序抓取商品销量数据并导出Excel // @author GitHub Copilot // @match https://*.s.taobao.com/search* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant unsafeWindow // @connect taobao.com // @connect tmall.com // @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js // ==/UserScript== (function() { 'use strict'; // 禁用淘宝的一些屏蔽措施 function disableAntiCrawl() { try { // 尝试禁用一些可能的拦截器 if (unsafeWindow.XMLHttpRequest.prototype._originalOpen === undefined) { unsafeWindow.XMLHttpRequest.prototype._originalOpen = unsafeWindow.XMLHttpRequest.prototype.open; unsafeWindow.XMLHttpRequest.prototype.open = function() { // 避免请求被中断 this.addEventListener('error', function(e) { console.log('XHR 错误被捕获', e); }); this.addEventListener('abort', function(e) { console.log('XHR 中断被捕获', e); }); return this._originalOpen.apply(this, arguments); }; } // // 防止页面跳转打断脚本执行 // window.onbeforeunload = function(e) { // // 如果正在爬取数据,阻止页面跳转 // if (GM_getValue('isScrapingActive', false)) { // e = e || window.event; // // 对于现代浏览器 // e.returnValue = '正在抓取数据,确定要离开吗?'; // // 对于旧浏览器 // return '正在抓取数据,确定要离开吗?'; // } // }; // 禁用可能的反爬虫JavaScript const scriptTags = document.querySelectorAll('script'); scriptTags.forEach(script => { if (script.innerHTML.includes('crawler') || script.innerHTML.includes('spider') || script.innerHTML.includes('bot')) { script.innerHTML = ''; } }); } catch (e) { console.error('禁用反爬虫措施失败:', e); } } // 创建浮动工具面板 function createToolPanel() { // 删除可能已存在的面板 const existingPanel = document.getElementById('sales-crawler-panel'); if (existingPanel) { existingPanel.remove(); } const panel = document.createElement('div'); panel.id = 'sales-crawler-panel'; panel.style.cssText = ` position: fixed; top: 100px; right: 20px; width: 320px; background: #fff; border: none; border-radius: 8px; box-shadow: 0 3px 15px rgba(0,0,0,0.15); z-index: 9999; padding: 0; font-family: "PingFang SC", "Microsoft YaHei", sans-serif; transition: all 0.3s ease; overflow: hidden; `; // 面板内容 panel.innerHTML = ` <div class="panel-header" style=" padding: 12px 15px; background: linear-gradient(135deg, #ff6a00, #ff8533); color: white; font-weight: bold; border-radius: 8px 8px 0 0; display: flex; justify-content: space-between; align-items: center; user-select: none; "> <div style="display: flex; align-items: center;"> <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 8px;"> <polyline points="23 4 23 10 17 10"></polyline> <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path> </svg> <span>淘宝商品销量抓取</span> </div> <div style="display: flex; align-items: center;"> <button id="minimize-btn" style=" background: none; border: none; color: white; cursor: pointer; font-size: 18px; padding: 0 8px; ">−</button> </div> </div> <div id="panel-content" style="padding: 15px;"> <div class="input-group" style="margin-bottom: 12px;"> <label style="display: block; font-size: 13px; color: #666; margin-bottom: 5px;">搜索关键词</label> <input id="keyword-input" type="text" placeholder="请输入要搜索的商品关键词" style=" width: 100%; padding: 10px; box-sizing: border-box; border: 1px solid #e0e0e0; border-radius: 6px; font-size: 14px; transition: border 0.2s; " /> </div> <div style="display: flex; margin-bottom: 12px; gap: 10px;"> <div class="input-group" style="flex: 1;"> <label style="display: block; font-size: 13px; color: #666; margin-bottom: 5px;">最大页数</label> <input id="max-pages" type="number" value="3" min="1" max="100" style=" width: 100%; padding: 10px; box-sizing: border-box; border: 1px solid #e0e0e0; border-radius: 6px; font-size: 14px; " /> </div> <div class="input-group" style="flex: 1;"> <label style="display: block; font-size: 13px; color: #666; margin-bottom: 5px;">排序方式</label> <select id="sort-type" style=" width: 100%; padding: 10px; box-sizing: border-box; border: 1px solid #e0e0e0; border-radius: 6px; font-size: 14px; background-color: white; "> <option value="sale-desc" selected>销量降序</option> <option value="price-asc">价格升序</option> <option value="price-desc">价格降序</option> <option value="renqi-desc">人气降序</option> </select> </div> </div> <div class="btn-group" style="display: flex; justify-content: space-between; margin-bottom: 15px; gap: 10px;"> <button id="search-btn" style=" flex: 1; padding: 10px 15px; background: #ff6a00; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: bold; transition: background 0.2s; ">开始抓取</button> <button id="download-btn" style=" flex: 1; padding: 10px 15px; background: #2ecc71; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: bold; transition: background 0.2s; " disabled>下载Excel</button> </div> <div id="status" style=" font-size: 13px; color: #666; text-align: center; margin: 10px 0; padding: 8px; border-radius: 4px; background-color: #f9f9f9; min-height: 18px; ">准备就绪</div> <div class="progress-container" style=" margin-bottom: 15px; background-color: #f0f0f0; border-radius: 4px; height: 8px; overflow: hidden; display: none; "> <div id="progress-bar" style=" height: 100%; width: 0%; background-color: #4CAF50; transition: width 0.3s; "></div> </div> <div id="results" style=" margin-top: 10px; max-height: 350px; overflow-y: auto; border-top: 1px solid #eee; padding-top: 10px; "></div> </div> <div id="panel-footer" style=" padding: 10px 15px; border-top: 1px solid #eee; background-color: #f9f9f9; font-size: 12px; color: #888; text-align: center; "> 版本 0.2 · <a href="#" id="help-btn" style="color: #ff6a00; text-decoration: none;">使用帮助</a> </div> `; document.body.appendChild(panel); // 添加事件监听器 setupPanelEventListeners(panel); // 添加拖拽功能 makeDraggable(panel); return panel; } // 设置面板事件监听器 function setupPanelEventListeners(panel) { // 最小化/最大化按钮功能 const minimizeBtn = panel.querySelector('#minimize-btn'); const panelContent = panel.querySelector('#panel-content'); const panelFooter = panel.querySelector('#panel-footer'); minimizeBtn.addEventListener('click', () => { if (panelContent.style.display === 'none') { // 展开面板 panelContent.style.display = 'block'; panelFooter.style.display = 'block'; minimizeBtn.textContent = '−'; } else { // 折叠面板 panelContent.style.display = 'none'; panelFooter.style.display = 'none'; minimizeBtn.textContent = '+'; } }); // 输入框焦点效果 const inputs = panel.querySelectorAll('input[type="text"], input[type="number"]'); inputs.forEach(input => { input.addEventListener('focus', () => { input.style.border = '1px solid #ff6a00'; }); input.addEventListener('blur', () => { input.style.border = '1px solid #e0e0e0'; }); }); // 按钮悬停效果 const searchBtn = panel.querySelector('#search-btn'); searchBtn.addEventListener('mouseover', () => { searchBtn.style.background = '#ff8533'; }); searchBtn.addEventListener('mouseout', () => { searchBtn.style.background = '#ff6a00'; }); const downloadBtn = panel.querySelector('#download-btn'); downloadBtn.addEventListener('mouseover', () => { if (!downloadBtn.disabled) { downloadBtn.style.background = '#27ae60'; } }); downloadBtn.addEventListener('mouseout', () => { downloadBtn.style.background = '#2ecc71'; }); // 帮助按钮功能 const helpBtn = panel.querySelector('#help-btn'); helpBtn.addEventListener('click', (e) => { e.preventDefault(); showHelp(); }); // 添加回车键搜索功能 const keywordInput = panel.querySelector('#keyword-input'); keywordInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !searchBtn.disabled) { searchBtn.click(); } }); } // 显示帮助信息 function showHelp() { // 创建帮助弹窗 const helpModal = document.createElement('div'); helpModal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; z-index: 10000; `; helpModal.innerHTML = ` <div style=" width: 500px; background-color: white; border-radius: 8px; padding: 20px; box-shadow: 0 5px 20px rgba(0,0,0,0.2); "> <h2 style="margin-top: 0; color: #ff6a00;">淘宝商品销量抓取工具使用帮助</h2> <div style="margin-bottom: 20px;"> <h3>功能简介</h3> <p>本工具可以帮助您抓取淘宝搜索结果中的商品销量数据,并支持导出为Excel表格。</p> <h3>使用方法</h3> <ol> <li>在"搜索关键词"输入框中输入您想要搜索的商品关键词</li> <li>设置要抓取的最大页数(每页约44个商品)</li> <li>选择排序方式(默认为销量降序)</li> <li>点击"开始抓取"按钮开始抓取数据</li> <li>抓取完成后,可以点击"下载Excel"按钮导出数据</li> </ol> <h3>注意事项</h3> <ul> <li>抓取过程中请不要关闭或刷新页面</li> <li>如需中途停止,可点击"停止抓取"按钮</li> <li>抓取大量数据时可能会被淘宝识别为异常行为,建议设置合理的页数</li> <li>本工具仅供学习研究使用,请勿用于商业用途</li> </ul> </div> <div style="text-align: center;"> <button id="close-help" style=" padding: 8px 15px; background: #ff6a00; color: white; border: none; border-radius: 4px; cursor: pointer; ">关闭</button> </div> </div> `; document.body.appendChild(helpModal); // 点击关闭按钮或背景关闭帮助 helpModal.querySelector('#close-help').addEventListener('click', () => { helpModal.remove(); }); helpModal.addEventListener('click', (e) => { if (e.target === helpModal) { helpModal.remove(); } }); } // 更新进度条 function updateProgress(current, max) { const progressContainer = document.querySelector('.progress-container'); const progressBar = document.getElementById('progress-bar'); if (progressContainer && progressBar) { // 显示进度条 progressContainer.style.display = 'block'; // 计算进度百分比 const percentage = Math.round((current / max) * 100); // 更新进度条宽度 progressBar.style.width = `${percentage}%`; // 根据进度改变颜色 if (percentage < 30) { progressBar.style.backgroundColor = '#ff6a00'; } else if (percentage < 70) { progressBar.style.backgroundColor = '#ffaa33'; } else { progressBar.style.backgroundColor = '#4CAF50'; } } } // 添加面板拖动功能 function makeDraggable(element) { // 存储初始位置和鼠标位置 let startX, startY, startLeft, startTop; let isDragging = false; // 获取面板头部元素作为拖拽手柄 const header = element.querySelector('.panel-header'); if (!header) return; // 确保面板有正确的定位样式 element.style.position = 'fixed'; element.style.margin = '0'; // 添加拖拽指示样式 header.style.cursor = 'move'; header.style.userSelect = 'none'; // 鼠标按下事件 header.addEventListener('mousedown', startDrag); function startDrag(e) { // 防止文本选择 e.preventDefault(); // 记录初始位置 startX = e.clientX; startY = e.clientY; // 获取面板当前位置 const rect = element.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; // 标记开始拖拽 isDragging = true; // 添加全局拖拽事件监听 document.addEventListener('mousemove', drag); document.addEventListener('mouseup', stopDrag); // 添加拖拽时的视觉反馈 element.style.transition = 'none'; // 拖拽时禁用过渡效果 element.style.opacity = '0.9'; element.style.boxShadow = '0 5px 20px rgba(0,0,0,0.2)'; // 记录在日志中 console.log('开始拖拽,初始位置:', {x: startX, y: startY, left: startLeft, top: startTop}); } function drag(e) { if (!isDragging) return; // 计算位移 const dx = e.clientX - startX; const dy = e.clientY - startY; // 应用新位置 const newLeft = startLeft + dx; const newTop = startTop + dy; // 确保不超出屏幕 const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const panelWidth = element.offsetWidth; const panelHeight = element.offsetHeight; // 边界检查,防止面板拖出屏幕 const boundedLeft = Math.max(0, Math.min(newLeft, viewportWidth - panelWidth / 3)); const boundedTop = Math.max(0, Math.min(newTop, viewportHeight - 50)); // 留出至少50px高度 // 设置新位置 element.style.left = `${boundedLeft}px`; element.style.top = `${boundedTop}px`; element.style.right = 'auto'; // 确保right样式不会干扰 // 记录日志 (但不要太频繁) if (Math.random() < 0.05) { console.log('拖拽中:', { dx, dy, newLeft, newTop, boundedLeft, boundedTop }); } } function stopDrag() { if (!isDragging) return; // 标记结束拖拽 isDragging = false; // 移除全局事件监听 document.removeEventListener('mousemove', drag); document.removeEventListener('mouseup', stopDrag); // 恢复视觉样式 element.style.transition = 'box-shadow 0.3s, opacity 0.3s'; element.style.opacity = '1'; element.style.boxShadow = '0 3px 15px rgba(0,0,0,0.15)'; // 记录最终位置 console.log('结束拖拽,最终位置:', { left: element.style.left, top: element.style.top }); } // 允许通过触摸拖拽(移动设备支持) header.addEventListener('touchstart', function(e) { const touch = e.touches[0]; const mouseEvent = new MouseEvent('mousedown', { clientX: touch.clientX, clientY: touch.clientY }); startDrag(mouseEvent); e.preventDefault(); // 防止滚动 }, { passive: false }); document.addEventListener('touchmove', function(e) { if (!isDragging) return; const touch = e.touches[0]; const mouseEvent = new MouseEvent('mousemove', { clientX: touch.clientX, clientY: touch.clientY }); drag(mouseEvent); e.preventDefault(); // 防止滚动 }, { passive: false }); document.addEventListener('touchend', function() { stopDrag(); }); // 记录拖拽初始化完成 console.log('拖拽功能已初始化'); } // 延迟函数 const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); // 手动搜索并抓取 async function manualSearch(keyword) { const statusElement = document.getElementById('status'); statusElement.textContent = '准备搜索,请耐心等待...'; // 构建搜索URL(按销量排序) // s.taobao.com/search?q=%E7%BB%BF%E8%8C%B6&sort=sale-desc&tab=all // https://s.taobao.com/search?page=2&q=%E7%BB%BF%E8%8C%B6&sort=sale-desc&tab=all const searchUrl = `https://s.taobao.com/search?q=${encodeURIComponent(keyword)}&sort=sale-desc&tab=all`; // 检查当前是否在搜索页面 if (!window.location.href.includes('s.taobao.com/search')) { window.location.href = searchUrl; return null; // 返回null表示需要重新加载页面 } // 检查URL参数 const currentKeyword = getParameterByName('q', window.location.href); const currentSort = getParameterByName('sort', window.location.href); const currentPage = getParameterByName('page', window.location.href) || '1'; // 如果是首页但关键词或排序不匹配,则跳转 if (currentKeyword !== keyword || currentSort !== 'sale-desc') { window.location.href = searchUrl; return null; } // 更新状态显示当前页码 statusElement.textContent = `正在加载第 ${currentPage} 页数据...`; // 等待页面元素加载完成 await waitForPageLoad(); // 提取当前页面的商品数据 return extractItemsFromCurrentPage(); } // 等待页面加载完成 function waitForPageLoad() { return new Promise(resolve => { // 定义可能的淘宝商品列表选择器 const selectors = [ '.m-itemlist .items .item', // 经典淘宝列表 '.doubleCard--gO3Bz6bu', // 新版淘宝卡片 '.items .item', // 通用项目选择器 '.J_MouserOnverReq', // 淘宝鼠标悬停元素 '.grid .item', // 网格布局商品 'div[data-index]', // 带索引的商品项 '.contentHolder--gO3Bz6bu', // 新版淘宝内容容器 '.commonCard--gO3Bz6bu' // 新版淘宝通用卡片 ]; // 首先检查页面是否已经加载了商品元素 for (const selector of selectors) { const elements = document.querySelectorAll(selector); if (elements && elements.length > 0) { console.log(`页面已加载,找到 ${elements.length} 个商品,选择器: ${selector}`); return setTimeout(resolve, 800); // 短暂延迟确保完全加载 } } // 设置最大尝试次数和计数器 let attempts = 0; const maxAttempts = 30; // 最多等待30次,约15秒 // 定时检查元素是否出现 const checkInterval = setInterval(() => { attempts++; // 检查所有可能的选择器 for (const selector of selectors) { const elements = document.querySelectorAll(selector); if (elements && elements.length > 0) { clearInterval(checkInterval); if (observer) observer.disconnect(); console.log(`第 ${attempts} 次检查: 找到 ${elements.length} 个商品,选择器: ${selector}`); return setTimeout(resolve, 800); // 短暂延迟确保完全加载 } } // 超时处理 if (attempts >= maxAttempts) { clearInterval(checkInterval); if (observer) observer.disconnect(); console.log(`等待页面加载超时(${maxAttempts * 500}ms),继续执行`); return resolve(); } console.log(`等待页面加载中...(${attempts}/${maxAttempts})`); }, 500); // 使用MutationObserver监听DOM变化,更快地检测元素出现 let observer = null; try { observer = new MutationObserver(mutations => { for (const selector of selectors) { const elements = document.querySelectorAll(selector); if (elements && elements.length > 0) { clearInterval(checkInterval); observer.disconnect(); console.log(`观察器检测到商品元素,选择器: ${selector}`); return setTimeout(resolve, 800); // 短暂延迟确保完全加载 } } }); // 观察整个文档的变化 observer.observe(document.body, { childList: true, subtree: true, attributes: false, characterData: false }); } catch (e) { console.error('创建MutationObserver失败:', e); } // 设置最终超时保护,确保不会永远等待 setTimeout(() => { clearInterval(checkInterval); if (observer) observer.disconnect(); console.log('最终超时保护触发,强制继续执行'); resolve(); }, maxAttempts * 500 + 2000); }); } // 获取URL参数 function getParameterByName(name, url = window.location.href) { name = name.replace(/[\[\]]/g, '\\$&'); const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'); const results = regex.exec(url); if (!results) return null; if (!results[2]) return ''; return decodeURIComponent(results[2].replace(/\+/g, ' ')); } // 从当前页面提取商品数据 function extractItemsFromCurrentPage() { const items = []; let productItems = []; // 尝试多种可能的选择器 const selectors = [ '.m-itemlist .items .item', '.doubleCard--gO3Bz6bu', '.items .item', '.J_MouserOnverReq', '[data-index]' ]; // 找到有效的选择器 for (const selector of selectors) { const elements = document.querySelectorAll(selector); if (elements && elements.length > 0) { productItems = elements; console.log(`使用选择器 ${selector} 找到 ${elements.length} 个商品`); break; } } if (productItems.length === 0) { console.warn('未找到商品元素,请检查页面结构'); return items; } // 处理每个商品项 productItems.forEach((item, index) => { try { // 获取商品标题 let title = ''; const titleSelectors = [ '.title a', '.title--qJ7Xg_90', '.ctx-box .title a', '.title', 'a.J_ClickStat' ]; for (const selector of titleSelectors) { const titleElement = item.querySelector(selector); if (titleElement) { title = titleElement.textContent.trim(); break; } } // 获取商品链接 let link = ''; const linkSelectors = [ '.title a', 'a.J_ClickStat', '.pic a', 'a[data-nid]' ]; for (const selector of linkSelectors) { const linkElement = item.querySelector(selector); if (linkElement && linkElement.href) { link = linkElement.href; break; } } // 获取销量数据 let sales = '0'; const salesSelectors = [ '.deal-cnt', '.realSales--XZJiepmt', '.sale-num', '[data-field="itemList"] [data-field="deal"]', '.sale em' ]; for (const selector of salesSelectors) { const salesElement = item.querySelector(selector); if (salesElement) { sales = salesElement.textContent.trim(); // 处理销量数据 sales = sales.replace(/人收货|人付款|笔|付款|\+|收货/g, ''); break; } } // 获取店铺名称 let shop = ''; const shopSelectors = [ '.shop .J_ShopInfo', '.shopNameText--DmtlsDKm', '.shop a', '.shopname' ]; for (const selector of shopSelectors) { const shopElement = item.querySelector(selector); if (shopElement) { shop = shopElement.textContent.trim(); break; } } // 获取价格 let price = ''; const priceSelectors = [ '.price strong', '.priceInt--yqqZMJ5a', '.price', '.price em' ]; for (const selector of priceSelectors) { const priceElement = item.querySelector(selector); if (priceElement) { price = priceElement.textContent.trim().replace(/[^\d.]/g, ''); break; } } // 添加商品数据 items.push({ 序号: index + 1, 商品名称: title, 店铺: shop, 价格: price, 销量: sales, 链接: link }); } catch (error) { console.error('提取商品数据出错:', error); } }); return items; } // 执行爬取 async function performScraping(keyword, maxPages) { // 设置正在抓取的标志 GM_setValue('isScrapingActive', true); // 更新状态 const statusElement = document.getElementById('status'); const resultsElement = document.getElementById('results'); statusElement.textContent = '开始搜索...'; try { // 从缓存恢复已抓取数据 let allItems = []; const cachedData = GM_getValue('currentItems'); if (cachedData) { try { allItems = JSON.parse(cachedData); console.log(`恢复了${allItems.length}条缓存数据`); // 显示恢复的数据 if (allItems.length > 0) { displayResults(allItems, resultsElement); } } catch (e) { console.error('解析缓存数据出错:', e); // 如果解析出错,重置数据 allItems = []; GM_setValue('currentItems', JSON.stringify([])); } } // 检查我们当前在哪一页 let currentPage = getPageFromUrl(window.location.href); console.log(`当前页码: ${currentPage}, 最大页数: ${maxPages}`); // 确保我们有正确的数据 if (currentPage > 1 && allItems.length === 0) { // 如果我们在非第一页但没有之前的数据,回到第一页 console.log('在非第一页但没有历史数据,跳回第一页'); await navigateToPage(1, keyword); return; } // 执行搜索,可能会导致页面内容更新 const pageItems = await manualSearch(keyword); if (pageItems === null) { // 页面将更新,中断当前执行 console.log('搜索操作会导致页面刷新,暂停执行'); return; } console.log(`从当前页面提取了 ${pageItems.length} 条商品数据`); // 将当前页数据添加到总数据集 if (currentPage === 1) { // 第一页直接替换数据 allItems = pageItems; console.log(`第1页:设置 ${pageItems.length} 条数据作为起始数据`); } else { // 对于后续页面,附加新数据 console.log(`第${currentPage}页:合并数据,当前总数据量: ${allItems.length},新数据量: ${pageItems.length}`); // 改进的重复检测逻辑:同时考虑商品名称和店铺名称 const uniqueItems = []; const existingItemKeys = new Set(); // 先为现有数据创建唯一键集合 allItems.forEach(item => { // 创建唯一键:商品名称 + 店铺名称,这样可以更精确地识别重复商品 const uniqueKey = `${item.商品名称}|${item.店铺}`; existingItemKeys.add(uniqueKey); }); // 筛选新数据中不重复的项目 pageItems.forEach(item => { const uniqueKey = `${item.商品名称}|${item.店铺}`; // 如果该商品不存在于现有数据中,添加它 if (!existingItemKeys.has(uniqueKey)) { // 设置正确的序号 item.序号 = allItems.length + uniqueItems.length + 1; uniqueItems.push(item); // 添加到已存在键集合中,避免当前页内的重复 existingItemKeys.add(uniqueKey); } }); console.log(`过滤后添加 ${uniqueItems.length} 条新数据,丢弃 ${pageItems.length - uniqueItems.length} 条重复数据`); // 合并数据 allItems = allItems.concat(uniqueItems); } // 按销量排序数据 - 修改为按照销量排序 allItems.sort((a, b) => { // 销量数据预处理:移除逗号、点和其他非数字字符,然后转为整数 const salesA = parseInt(a.销量.replace(/[,\.万+]/g, '')) || 0; const salesB = parseInt(b.销量.replace(/[,\.万+]/g, '')) || 0; return salesB - salesA; // 降序排序 }); // 重新标记序号 allItems.forEach((item, index) => { item.序号 = index + 1; }); // 保存当前数据 GM_setValue('currentItems', JSON.stringify(allItems)); console.log(`已保存 ${allItems.length} 条数据到缓存`); // 显示抓取结果 statusElement.textContent = `已抓取 ${allItems.length} 条数据,当前第 ${currentPage}/${maxPages} 页`; // 显示数据 displayResults(allItems, resultsElement); // 决定是否继续抓取下一页 if (currentPage < maxPages) { statusElement.textContent = `已抓取第 ${currentPage}/${maxPages} 页,总计 ${allItems.length} 条数据,准备下一页...`; // 保存当前数据 GM_setValue('currentItems', JSON.stringify(allItems)); GM_setValue('currentPage', currentPage + 1); GM_setValue('maxPages', maxPages); GM_setValue('keyword', keyword); // 添加停止按钮 - 新增功能 addStopButton(); // 延迟一下以便用户查看 await delay(1000); // 使用新函数导航到下一页,优先使用页面内按钮点击 await navigateToPage(currentPage + 1, keyword); } else { // 全部抓取完成 finishScraping(allItems); } } catch (error) { console.error('抓取过程中出错:', error); statusElement.textContent = '抓取过程中出错,请重试: ' + error.message; GM_setValue('isScrapingActive', false); } } // 显示结果 function displayResults(items, resultsElement) { // 限制显示数量,避免浏览器卡顿 const maxDisplayItems = 100; const displayItems = items.slice(0, maxDisplayItems); let html = ` <div style="margin-top:10px;font-size:12px;"> <p>已抓取 ${items.length} 条数据${items.length > maxDisplayItems ? `(显示前 ${maxDisplayItems} 条)` : ''}</p> <table style="width:100%;border-collapse:collapse;font-size:12px;"> <thead> <tr> <th style="padding:5px;border:1px solid #ddd;background:#f5f5f5;">序号</th> <th style="padding:5px;border:1px solid #ddd;background:#f5f5f5;">商品名称</th> <th style="padding:5px;border:1px solid #ddd;background:#f5f5f5;">价格</th> <th style="padding:5px;border:1px solid #ddd;background:#f5f5f5;">销量</th> <th style="padding:5px;border:1px solid #ddd;background:#f5f5f5;">店铺</th> </tr> </thead> <tbody> ${displayItems.map(item => ` <tr> <td style="padding:5px;border:1px solid #ddd;text-align:center;">${item.序号}</td> <td style="padding:5px;border:1px solid #ddd;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${item.商品名称}">${item.商品名称 || '未知'}</td> <td style="padding:5px;border:1px solid #ddd;text-align:center;">${item.价格 || '0'}</td> <td style="padding:5px;border:1px solid #ddd;text-align:center;">${item.销量 || '0'}</td> <td style="padding:5px;border:1px solid #ddd;">${item.店铺 || '未知'}</td> </tr> `).join('')} </tbody> </table> </div> `; resultsElement.innerHTML = html; } // 继续抓取(从页面刷新后恢复) async function continueScraping() { if (!GM_getValue('isScrapingActive')) return; const keyword = GM_getValue('keyword'); const maxPages = GM_getValue('maxPages', 3); // 当前页面URL中的页码参数 const pageParam = getParameterByName('page') || '1'; const currentPage = parseInt(pageParam); // 更新UI状态 if (document.getElementById('keyword-input')) { document.getElementById('keyword-input').value = keyword; } if (document.getElementById('max-pages')) { document.getElementById('max-pages').value = maxPages; } // 禁用搜索按钮 if (document.getElementById('search-btn')) { document.getElementById('search-btn').disabled = true; } // 继续抓取流程 await performScraping(keyword, maxPages); } // 完成抓取 function finishScraping(allItems) { const statusElement = document.getElementById('status'); statusElement.textContent = `抓取完成,共获取 ${allItems.length} 条数据`; // 保存最终数据 GM_setValue('finalScrapedData', JSON.stringify(allItems)); // 清理中间状态 GM_deleteValue('currentItems'); GM_deleteValue('currentPage'); GM_deleteValue('isScrapingActive'); // 启用下载按钮 document.getElementById('download-btn').disabled = false; } // 导出Excel function exportToExcel() { try { // 获取最终数据 const data = JSON.parse(GM_getValue('finalScrapedData') || '[]'); if (!data || data.length === 0) { alert('没有可导出的数据'); return; } // 准备表头和数据 const header = ["序号", "商品名称", "店铺", "价格", "销量", "链接"]; const rows = data.map(item => [ item.序号, item.商品名称 || '', item.店铺 || '', item.价格 || '', item.销量 || '0', item.链接 || '' ]); // 创建工作表 const worksheet = XLSX.utils.aoa_to_sheet([header, ...rows]); // 设置列宽 const wscols = [ {wch: 6}, // 序号 {wch: 40}, // 商品名称 {wch: 20}, // 店铺 {wch: 10}, // 价格 {wch: 10}, // 销量 {wch: 60} // 链接 ]; worksheet['!cols'] = wscols; // 创建工作簿并添加工作表 const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, "销量数据"); // 获取文件名 const keyword = GM_getValue('keyword') || '淘宝商品'; const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19); const fileName = `${keyword}_销量数据_${timestamp}.xlsx`; // 导出文件 XLSX.writeFile(workbook, fileName); console.log('Excel导出成功:', fileName); } catch (error) { console.error('导出Excel出错:', error); alert('导出Excel失败: ' + error.message); } } // 添加URL变更监听 function setupUrlChangeListener() { // 存储当前URL以便检测变化 let lastUrl = window.location.href; // 创建一个观察器来监视URL变化 const observer = new MutationObserver(() => { if (lastUrl !== window.location.href) { console.log(`URL已变更: ${lastUrl} -> ${window.location.href}`); const oldUrl = lastUrl; lastUrl = window.location.href; // 处理URL变更 handleUrlChange(oldUrl, lastUrl); } }); // 开始观察document变化 observer.observe(document, { subtree: true, childList: true }); // 自定义事件监听 window.addEventListener('urlchanged', function() { if (lastUrl !== window.location.href) { console.log(`通过自定义事件检测到URL变更: ${lastUrl} -> ${window.location.href}`); const oldUrl = lastUrl; lastUrl = window.location.href; handleUrlChange(oldUrl, lastUrl); } }); // 通过监听popstate事件捕获浏览器历史导航 window.addEventListener('popstate', function() { if (lastUrl !== window.location.href) { console.log(`通过popstate检测到URL变更: ${lastUrl} -> ${window.location.href}`); const oldUrl = lastUrl; lastUrl = window.location.href; handleUrlChange(oldUrl, lastUrl); } }); // 通过监听hashchange事件捕获hash变更 window.addEventListener('hashchange', function() { if (lastUrl !== window.location.href) { console.log(`通过hashchange检测到URL变更: ${lastUrl} -> ${window.location.href}`); const oldUrl = lastUrl; lastUrl = window.location.href; handleUrlChange(oldUrl, lastUrl); } }); // 重写history方法以捕获pushState和replaceState const originalPushState = history.pushState; history.pushState = function() { originalPushState.apply(this, arguments); if (lastUrl !== window.location.href) { console.log(`通过pushState检测到URL变更: ${lastUrl} -> ${window.location.href}`); const oldUrl = lastUrl; lastUrl = window.location.href; handleUrlChange(oldUrl, lastUrl); } }; const originalReplaceState = history.replaceState; history.replaceState = function() { originalReplaceState.apply(this, arguments); if (lastUrl !== window.location.href) { console.log(`通过replaceState检测到URL变更: ${lastUrl} -> ${window.location.href}`); const oldUrl = lastUrl; lastUrl = window.location.href; handleUrlChange(oldUrl, lastUrl); } }; // 定期检查URL变化(针对某些特殊情况) setInterval(() => { if (lastUrl !== window.location.href) { console.log(`通过轮询检测到URL变更: ${lastUrl} -> ${window.location.href}`); const oldUrl = lastUrl; lastUrl = window.location.href; handleUrlChange(oldUrl, lastUrl); } }, 1000); } // 处理URL变更 async function handleUrlChange(oldUrl, newUrl) { try { // 检查页面是否改变 const oldPage = getPageFromUrl(oldUrl); const newPage = getPageFromUrl(newUrl); console.log(`URL变更: ${oldUrl} -> ${newUrl}`); console.log(`页码变更: ${oldPage} -> ${newPage}`); // 检查是否是淘宝搜索页面 const isSearchPage = newUrl.includes('s.taobao.com/search'); // 如果不是搜索页面,则不处理 if (!isSearchPage) { console.log('不是搜索页面,不处理URL变更'); return; } // 检查是否正在抓取 const isActive = GM_getValue('isScrapingActive', false); if (!isActive) { console.log('没有正在进行的抓取任务,不处理URL变更'); return; } // 分析URL变更是否是分页变更 if (oldPage !== newPage) { console.log(`检测到页码从 ${oldPage} 变更为 ${newPage}`); // 更新当前页码 GM_setValue('currentPage', newPage); // 等待新页面加载 await waitForPageLoad(); // 继续抓取 const keyword = GM_getValue('keyword'); const maxPages = GM_getValue('maxPages', 3); await performScraping(keyword, maxPages); } else { console.log('页码未变更,不处理'); } } catch (error) { console.error('处理URL变更时出错:', error); } } // 从URL中提取页码 function getPageFromUrl(url) { // 尝试获取page参数 const pageParam = getParameterByName('page', url); if (pageParam) { return parseInt(pageParam); } // 尝试获取s参数(s=44对应第2页,s=88对应第3页,以此类推) const sParam = getParameterByName('s', url); if (sParam) { return Math.floor(parseInt(sParam) / 44) + 1; } // 默认为第1页 return 1; } // 导航到特定页码 async function navigateToPage(pageNum, keyword) { console.log(`正在导航到第 ${pageNum} 页...`); // 更新状态显示 const statusElement = document.getElementById('status'); if (statusElement) { statusElement.textContent = `正在跳转到第 ${pageNum} 页...`; } // 尝试点击分页按钮 const result = await handlePagination(pageNum, keyword); // 如果点击按钮失败,尝试回退到其他方式 if (!result) { console.log('点击分页按钮失败,尝试备用方案...'); // 检查是否已经在正确的页面 const currentPage = getPageFromUrl(window.location.href); if (currentPage === pageNum) { console.log(`已经在第 ${pageNum} 页,无需导航`); return true; } // 尝试点击"下一页"按钮(如果需要前进一页) if (pageNum === currentPage + 1) { const nextBtn = document.querySelector('.J_Ajax.next, .ui-page-s-next, .ui-page-next, a.next-page'); if (nextBtn) { console.log('使用"下一页"按钮导航...'); nextBtn.scrollIntoView({ behavior: 'smooth', block: 'center' }); await delay(500); nextBtn.click(); return true; } } // 最后的备选方案:直接修改URL(避免此方式但保留作为最后手段) console.log('无法通过点击按钮导航,尝试直接修改URL(备选方案)'); const baseUrl = `https://s.taobao.com/search`; // 注意:此处修复了URL构建的问题,添加了缺失的&符号 let targetUrl = `${baseUrl}?page=${pageNum}&q=${encodeURIComponent(keyword)}&sort=sale-desc&tab=all`; // const targetUrl = `${baseUrl}?q=${encodeURIComponent(keyword)}&sort=sale-desc&page=${pageNum}`; // 使用pushState而不是直接修改location if (window.history && window.history.pushState) { window.history.pushState({}, '', targetUrl); // 触发一个自定义事件,便于我们的URL监听器捕获 window.dispatchEvent(new Event('urlchanged')); return true; } else { // 最后手段 window.location.href = targetUrl; return true; } } return true; } // 处理分页点击 async function handlePagination(pageNum, keyword) { try { console.log(`尝试点击第 ${pageNum} 页按钮...`); // 新增针对淘宝新版分页的选择器 const nextPaginationSelectors = [ // 新版淘宝分页按钮 '.next-pagination-item', '.next-btn.next-medium.next-btn-normal.next-pagination-item', '.next-pagination-list button', // 老版淘宝分页按钮 '.J_Ajax.num', '.ui-page-s-next', 'li.item', 'a[data-value]', '.pagination a', '.page-item', '.page-number', '.pagination-item', '.item.J_Ajax' ]; // 先尝试精确匹配目标页码的按钮 let targetBtn = null; let pageClicked = false; // 尝试方法1: 使用aria-label属性查找(新版淘宝最准确的方式) const ariaLabelButtons = Array.from(document.querySelectorAll('button[aria-label*="页"]')); for (const btn of ariaLabelButtons) { const ariaLabel = btn.getAttribute('aria-label') || ''; if (ariaLabel.includes(`第${pageNum}页`)) { console.log(`通过aria-label找到第${pageNum}页按钮: ${ariaLabel}`); targetBtn = btn; break; } } // 尝试方法2: 使用按钮内容文本匹配 if (!targetBtn) { // 遍历所有可能的分页按钮 for (const selector of nextPaginationSelectors) { const buttons = document.querySelectorAll(selector); for (const btn of buttons) { // 检查按钮文本是否与目标页码匹配 const btnText = btn.textContent.trim(); if (btnText === String(pageNum)) { console.log(`通过文本内容找到第${pageNum}页按钮: ${btnText}`); targetBtn = btn; break; } // 检查按钮中的span元素 const helperSpan = btn.querySelector('.next-btn-helper'); if (helperSpan && helperSpan.textContent.trim() === String(pageNum)) { console.log(`通过.next-btn-helper找到第${pageNum}页按钮: ${helperSpan.textContent}`); targetBtn = btn; break; } } if (targetBtn) break; } } // 如果找到了目标按钮,执行点击 if (targetBtn) { console.log(`找到第${pageNum}页按钮,准备点击...`); // 确保按钮可见 targetBtn.scrollIntoView({ behavior: 'smooth', block: 'center' }); await delay(500); // 尝试多种点击方式 try { // 1. 常规点击 targetBtn.click(); console.log(`点击第${pageNum}页按钮成功`); pageClicked = true; } catch (e) { console.log(`常规点击失败,尝试模拟鼠标事件: ${e.message}`); // 2. 使用MouseEvent模拟点击 try { const clickEvent = new MouseEvent('click', { view: window, bubbles: true, cancelable: true }); targetBtn.dispatchEvent(clickEvent); console.log(`使用MouseEvent点击第${pageNum}页按钮成功`); pageClicked = true; } catch (e2) { console.error(`点击事件模拟失败: ${e2.message}`); // 3. 触发按钮的onmousedown和onmouseup事件 try { const mouseDown = new MouseEvent('mousedown', { view: window, bubbles: true, cancelable: true }); const mouseUp = new MouseEvent('mouseup', { view: window, bubbles: true, cancelable: true }); targetBtn.dispatchEvent(mouseDown); await delay(10); targetBtn.dispatchEvent(mouseUp); console.log(`使用mousedown/up事件点击第${pageNum}页按钮`); pageClicked = true; } catch (e3) { console.error(`所有点击方法均失败: ${e3.message}`); } } } } // 方法3: 如果目标页码按钮无法找到或点击失败,尝试使用"下一页"按钮 if (!pageClicked && pageNum > 1) { // 先查找下一页按钮 let nextPageBtn = null; // 针对新版淘宝下一页按钮 const nextBtnCandidates = [ document.querySelector('.next-btn.next-medium.next-btn-normal.next-pagination-item.next-next'), document.querySelector('button[aria-label*="下一页"]'), document.querySelector('.next-pagination-item.next-next'), document.querySelector('.J_Ajax.next'), document.querySelector('.ui-page-s-next'), document.querySelector('button.next-btn:has(span.next-btn-helper:contains("下一页"))'), document.querySelector('a.next-page') ]; for (const btn of nextBtnCandidates) { if (btn) { nextPageBtn = btn; break; } } // 如果找到下一页按钮,尝试点击 if (nextPageBtn) { console.log('使用"下一页"按钮导航...'); nextPageBtn.scrollIntoView({ behavior: 'smooth', block: 'center' }); await delay(500); try { nextPageBtn.click(); console.log('"下一页"按钮点击成功'); pageClicked = true; } catch (e) { console.error(`点击"下一页"按钮失败: ${e.message}`); try { const clickEvent = new MouseEvent('click', { view: window, bubbles: true, cancelable: true }); nextPageBtn.dispatchEvent(clickEvent); console.log(`使用MouseEvent点击"下一页"按钮成功`); pageClicked = true; } catch (e2) { console.error(`所有点击"下一页"方式均失败: ${e2.message}`); } } } } // 方法4: 尝试使用跳转输入框(淘宝新版分页特有) if (!pageClicked) { const jumpInput = document.querySelector('.next-pagination-jump-input input'); const jumpBtn = document.querySelector('.next-pagination-jump-go'); if (jumpInput && jumpBtn) { console.log(`尝试使用跳转输入框跳转到第${pageNum}页...`); // 设置输入框值 jumpInput.value = String(pageNum); // 触发输入框change事件 jumpInput.dispatchEvent(new Event('change', { bubbles: true })); jumpInput.dispatchEvent(new Event('input', { bubbles: true })); await delay(300); // 点击确定按钮 try { jumpBtn.click(); console.log('跳转按钮点击成功'); pageClicked = true; } catch (e) { console.log(`点击跳转按钮失败: ${e.message}`); try { const clickEvent = new MouseEvent('click', { view: window, bubbles: true, cancelable: true }); jumpBtn.dispatchEvent(clickEvent); console.log(`使用MouseEvent点击跳转按钮成功`); pageClicked = true; } catch (e2) { console.error(`所有点击跳转按钮方式均失败: ${e2.message}`); } } } } // 如果所有点击方法都失败,记录详细的分页元素信息 if (!pageClicked) { console.log('所有点击方法都失败,记录分页元素信息:'); // 打印所有可能的分页元素的HTML const paginationContainer = document.querySelector('.next-pagination') || document.querySelector('.pgWrap--RTFKoWa6') || document.querySelector('.pagination'); if (paginationContainer) { console.log('分页容器HTML:', paginationContainer.outerHTML); // 尝试使用URL方式进行导航 // 构建请求URL const baseUrl = `https://s.taobao.com/search`; const targetUrl = `${baseUrl}?page=${pageNum}&q=${encodeURIComponent(keyword)}&sort=sale-desc&tab=all`; // 使用pushState而不是直接修改location if (window.history && window.history.pushState) { window.history.pushState({}, '', targetUrl); window.dispatchEvent(new Event('urlchanged')); console.log(`尝试使用pushState跳转到: ${targetUrl}`); pageClicked = true; } } else { console.log('未找到分页容器'); } } // 等待URL变化确认分页是否成功 if (pageClicked) { console.log('分页操作已执行,等待URL变化...'); const originalUrl = window.location.href; // 等待URL变化或超时 const urlChangePromise = new Promise((resolve) => { let checkCount = 0; const maxChecks = 20; // 10秒超时 (500ms * 20) const checkInterval = setInterval(() => { checkCount++; if (window.location.href !== originalUrl) { clearInterval(checkInterval); console.log(`URL已变化: ${originalUrl} -> ${window.location.href}`); resolve(true); } else if (checkCount >= maxChecks) { clearInterval(checkInterval); console.log(`URL未变化,可能页码未更新,已等待${maxChecks * 500 / 1000}秒`); resolve(false); } }, 500); }); const urlChanged = await urlChangePromise; return urlChanged || pageClicked; // 如果URL变化或按钮点击成功,视为成功 } return pageClicked; } catch (error) { console.error('分页处理出错:', error); return false; } } // 添加停止按钮 function addStopButton() { if (document.getElementById('stop-btn')) return; const btnContainer = document.querySelector('#search-btn').parentNode; if (!btnContainer) return; const stopBtn = document.createElement('button'); stopBtn.id = 'stop-btn'; stopBtn.textContent = '停止抓取'; stopBtn.style.cssText = ` padding: 8px 15px; background: #e74c3c; color: white; border: none; border-radius: 4px; cursor: pointer; margin-top: 10px; width: 100%; `; stopBtn.addEventListener('click', () => { // 停止抓取过程 GM_setValue('isScrapingActive', false); // 更新UI document.getElementById('status').textContent = '已手动停止抓取'; document.getElementById('search-btn').disabled = false; // 如果已有数据,启用下载按钮 const items = JSON.parse(GM_getValue('currentItems') || '[]'); if (items.length > 0) { GM_setValue('finalScrapedData', JSON.stringify(items)); document.getElementById('download-btn').disabled = false; } // 移除停止按钮 stopBtn.remove(); }); btnContainer.appendChild(stopBtn); } // 主函数 function init() { console.log('淘宝商品销量抓取工具初始化...'); // 禁用反爬措施 disableAntiCrawl(); // 设置URL变更监听 setupUrlChangeListener(); // 创建工具面板 createToolPanel(); // 绑定开始抓取按钮事件 document.getElementById('search-btn').addEventListener('click', async () => { const keyword = document.getElementById('keyword-input').value; const maxPages = parseInt(document.getElementById('max-pages').value) || 3; if (!keyword) { alert('请输入搜索关键词'); return; } // 清理所有之前的数据 GM_deleteValue('currentItems'); GM_deleteValue('currentPage'); GM_deleteValue('finalScrapedData'); GM_setValue('isScrapingActive', true); GM_setValue('keyword', keyword); GM_setValue('maxPages', maxPages); // 重置UI状态 document.getElementById('results').innerHTML = ''; document.getElementById('status').textContent = '准备开始新的抓取...'; // 禁用按钮防止重复点击 document.getElementById('search-btn').disabled = true; document.getElementById('download-btn').disabled = true; // 执行搜索并获取数据 await performScraping(keyword, maxPages); }); // 绑定下载按钮事件 document.getElementById('download-btn').addEventListener('click', exportToExcel); // 重要修改点:不再自动继续抓取,而是立即停止任务并恢复UI状态 if (GM_getValue('isScrapingActive')) { // 检查是否是页面刷新而非正常导航 const isPageRefresh = performance.navigation && (performance.navigation.type === 1 || // 标准刷新类型 performance.getEntriesByType('navigation')[0]?.type === 'reload'); // 新API if (isPageRefresh) { console.log('检测到页面刷新,停止抓取任务'); // 尝试保存已有数据为最终数据 const cachedItems = GM_getValue('currentItems'); if (cachedItems) { GM_setValue('finalScrapedData', cachedItems); try { const data = JSON.parse(cachedItems); const statusElement = document.getElementById('status'); if (statusElement && data && data.length > 0) { statusElement.textContent = `抓取已停止,已保存 ${data.length} 条数据`; // 显示数据 const resultsElement = document.getElementById('results'); if (resultsElement) { displayResults(data, resultsElement); } // 启用下载按钮 document.getElementById('download-btn').disabled = false; document.getElementById('search-btn').disabled = false; // 显示通知 showNotification(`页面已刷新,抓取已停止,已保存 ${data.length} 条数据`, 'warning'); } } catch (e) { console.error('解析已保存数据出错:', e); } } // 关闭抓取活动状态 GM_setValue('isScrapingActive', false); // 恢复关键词和页数设置 document.getElementById('keyword-input').value = GM_getValue('keyword', ''); document.getElementById('max-pages').value = GM_getValue('maxPages', 3); } else { console.log('正常导航,不再继续抓取任务'); // 关闭抓取活动状态 GM_setValue('isScrapingActive', false); } } else { // 检查是否有最终数据 const finalData = GM_getValue('finalScrapedData'); if (finalData) { try { const data = JSON.parse(finalData); if (data && data.length > 0) { // 恢复UI状态 document.getElementById('keyword-input').value = GM_getValue('keyword', ''); document.getElementById('max-pages').value = GM_getValue('maxPages', 3); // 启用下载按钮 document.getElementById('download-btn').disabled = false; // 更新状态 const statusElement = document.getElementById('status'); statusElement.textContent = `已加载之前抓取的 ${data.length} 条数据`; // 显示数据 const resultsElement = document.getElementById('results'); displayResults(data, resultsElement); } } catch (e) { console.error('解析已保存数据出错:', e); } } } } // 显示通知提示 function showNotification(message, type = 'info') { try { // 先检查是否已有通知,如果有则移除 const existingNotification = document.getElementById('crawler-notification'); if (existingNotification) { existingNotification.remove(); } // 创建通知元素 const notification = document.createElement('div'); notification.id = 'crawler-notification'; // 设置不同类型的样式 let backgroundColor = '#3498db'; // 默认信息色 let icon = 'ℹ️'; if (type === 'success') { backgroundColor = '#2ecc71'; icon = '✅'; } else if (type === 'warning') { backgroundColor = '#f39c12'; icon = '⚠️'; } else if (type === 'error') { backgroundColor = '#e74c3c'; icon = '❌'; } notification.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); padding: 12px 20px; background-color: ${backgroundColor}; color: white; border-radius: 4px; box-shadow: 0 4px 10px rgba(0,0,0,0.2); font-size: 14px; font-family: "PingFang SC", "Microsoft YaHei", sans-serif; z-index: 10000; display: flex; align-items: center; animation: fadeIn 0.3s ease; `; notification.innerHTML = ` <span style="margin-right: 8px; font-size: 16px;">${icon}</span> <span>${message}</span> `; // 添加到文档 document.body.appendChild(notification); // 添加CSS动画 const style = document.createElement('style'); style.textContent = ` @keyframes fadeIn { from { opacity: 0; transform: translate(-50%, -10px); } to { opacity: 1; transform: translate(-50%, 0); } } `; document.head.appendChild(style); // 设置自动消失 setTimeout(() => { notification.style.opacity = '0'; notification.style.transform = 'translate(-50%, -10px)'; notification.style.transition = 'all 0.3s ease'; setTimeout(() => { notification.remove(); style.remove(); }, 300); }, 3000); } catch (error) { console.error('显示通知出错:', error); } } // 页面加载完成后初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { setTimeout(init, 1000); // 延迟初始化以确保页面加载完成 } // 添加调试信息 console.log('淘宝商品销量抓取工具已加载'); })();