京东联盟定向计划商品数据导出

抓取京东联盟定向计划的商品数据并导出为Excel

// ==UserScript==
// @name         京东联盟定向计划商品数据导出
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @description  抓取京东联盟定向计划的商品数据并导出为Excel
// @author       Dustin
// @match        https://union.jd.com/proManager/planDetails*
// @grant        none
// @require      https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    console.log('京东联盟商品数据导出脚本');

    // 等待页面加载完成
    function waitForElements() {
        return new Promise((resolve) => {
            const checkElements = () => {
                // 检查商品数据是否已加载
                const links = document.querySelectorAll('a[href*="//item.jd.com/"]');
                console.log(`找到 ${links.length} 个商品链接`);
                
                if (links.length > 0) {
                    resolve(links);
                } else {
                    setTimeout(checkElements, 500);
                }
            };
            checkElements();
        });
    }

    // 提取商品数据
    function extractProductData() {
        const products = [];
        
        try {
            console.log('开始提取商品数据...');
            
            // 基于实际DOM结构:每个商品由 图片链接 + 佣金段落 + 标题段落 + 价格段落 + 标签文本 组成
            // 获取所有段落元素
            const allParagraphs = document.querySelectorAll('p');
            console.log(`总共找到 ${allParagraphs.length} 个段落`);
            
            // 用于跟踪已添加的SKU,避免重复
            const addedSkus = new Set();
            
            // 查找包含商品标题的段落(包含京东商品链接的段落)
            allParagraphs.forEach((titleParagraph, index) => {
                try {
                    // 查找段落中的商品链接
                    const productLink = titleParagraph.querySelector('a[href*="//item.jd.com/"]');
                    if (!productLink) return;
                    
                    const href = productLink.getAttribute('href');
                    const skuMatch = href.match(/\/(\d+)\.html/);
                    if (!skuMatch) return;
                    
                    const skuId = skuMatch[1];
                    // 避免重复添加同一个SKU
                    if (addedSkus.has(skuId)) return;
                    
                    const title = productLink.textContent.trim();
                    // 过滤掉空标题或太短的标题
                    if (!title || title.length < 10) return;
                    
                    // 查找佣金信息(前一个段落)
                    let commission = '';
                    let estimatedIncome = '';
                    let currentPrice = '';
                    let originalPrice = '';
                    
                    // 向前查找佣金段落
                    const commissionParagraph = titleParagraph.previousElementSibling;
                    console.log('佣金段落:', commissionParagraph);
                    if (commissionParagraph && commissionParagraph.tagName === 'P') {
                        const commissionText = commissionParagraph.textContent.trim();
                        console.log('佣金文本:', commissionText);
                        if (commissionText.includes('佣金比例:') && commissionText.includes('预估收益')) {
                            const commissionMatch = commissionText.match(/佣金比例:已为您匹配当前最高佣金比例(\d+)%/);
                            const incomeMatch = commissionText.match(/预估收益¥([\d.]+)/);
                            
                            console.log('佣金匹配:', commissionMatch);
                            console.log('收益匹配:', incomeMatch);
                            
                            if (commissionMatch) commission = commissionMatch[1];
                            if (incomeMatch) estimatedIncome = incomeMatch[1];
                        } else {
                            console.log('佣金文本格式不匹配,实际内容:', commissionText);
                        }
                    } else {
                        console.log('未找到佣金段落或段落类型错误');
                    }
                    
                    // 向后查找价格段落,使用HTML class提取价格
                    const priceParagraph = titleParagraph.nextElementSibling;
                    console.log('价格段落:', priceParagraph);
                    if (priceParagraph && priceParagraph.tagName === 'P') {
                        // 查找class="real-price"的元素作为到手价
                        const currentPriceElement = priceParagraph.querySelector('.real-price');
                        console.log('到手价元素:', currentPriceElement);
                        if (currentPriceElement) {
                            // 提取价格数字,去掉¥符号和“到手价”关键词
                            const priceText = currentPriceElement.textContent.trim().replace('¥', '').replace('到手价', '');
                            currentPrice = priceText;
                            console.log('提取的到手价:', currentPrice);
                        } else {
                            // 如果没找到.real-price class,使用正则表达式作为备用方案
                            const priceText = priceParagraph.textContent.trim();
                            console.log('备用方案:价格文本:', priceText);
                            if (priceText.includes('到手价¥')) {
                                const priceMatch = priceText.match(/到手价¥([\d.]+)/);
                                if (priceMatch) {
                                    currentPrice = priceMatch[1];
                                    console.log('备用方案提取的到手价:', currentPrice);
                                }
                            }
                        }
                        
                        // 查找有text-decoration: line-through样式的元素作为划线价
                        const allSpans = priceParagraph.querySelectorAll('span');
                        console.log('找到span元素个数:', allSpans.length);
                        allSpans.forEach((span, idx) => {
                            const style = window.getComputedStyle(span);
                            console.log(`span${idx}样式:`, style.textDecoration, '内容:', span.textContent);
                            if (style.textDecoration.includes('line-through')) {
                                const lineThroughPrice = span.textContent.trim().replace('¥', '').replace('划线价', '').replace('原价', '');
                                if (lineThroughPrice) {
                                    originalPrice = lineThroughPrice;
                                    console.log('提取的划线价:', originalPrice);
                                }
                            }
                        });
                        
                        // 如果没找到划线价,使用正则表达式作为备用方案
                        if (!originalPrice) {
                            const priceText = priceParagraph.textContent.trim();
                            const originalMatch = priceText.match(/到手价¥[\d.]+¥([\d.]+)/);
                            if (originalMatch) {
                                originalPrice = originalMatch[1];
                                console.log('备用方案提取的划线价:', originalPrice);
                            }
                        }
                    } else {
                        console.log('未找到价格段落或段落类型错误');
                    }
                    
                    // 添加商品数据
                    addedSkus.add(skuId);
                    products.push({
                        '序号': products.length + 1,
                        'SKU ID': skuId,
                        '商品标题': title,
                        '佣金比例': commission ? commission + '%' : '',
                        '预估收益': estimatedIncome ? estimatedIncome : '',
                        '到手价': currentPrice ? currentPrice : '',
                        '划线价': originalPrice ? originalPrice : '',
                        '商品链接': 'https:' + href
                    });
                    
                    console.log(`提取商品 ${products.length}: ${skuId} - ${title.substring(0, 30)}...`);
                    
                } catch (error) {
                    console.error(`处理第${index}个段落时出错:`, error);
                }
            });
            
            console.log(`成功提取 ${products.length} 个商品数据`);
            return products;
            
        } catch (error) {
            console.error('提取商品数据时出错:', error);
            return [];
        }
    }

    // 导出到Excel
    function exportToExcel(data) {
        if (data.length === 0) {
            alert('没有找到商品数据,请确保页面已完全加载');
            return;
        }
        
        console.log('开始导出Excel,数据条数:', data.length);
        
        try {
            const ws = XLSX.utils.json_to_sheet(data);
            const wb = XLSX.utils.book_new();
            XLSX.utils.book_append_sheet(wb, ws, "商品数据");
            
            // 设置列宽
            const colWidths = [
                {wch: 5},   // 序号
                {wch: 15},  // SKU ID
                {wch: 50},  // 商品标题
                {wch: 10},  // 佣金比例
                {wch: 12},  // 预估收益
                {wch: 12},  // 到手价
                {wch: 12},  // 划线价
                {wch: 30}   // 商品链接
            ];
            ws['!cols'] = colWidths;
            
            const planId = new URLSearchParams(window.location.search).get('planId') || 'unknown';
            const fileName = `京东联盟定向计划_${planId}_商品数据_${new Date().toISOString().slice(0, 10)}.xlsx`;
            
            XLSX.writeFile(wb, fileName);
            console.log(`导出完成,共导出 ${data.length} 条商品数据`);
            alert(`导出完成!共导出 ${data.length} 条商品数据\n\n导出的数据包括:\n- SKU ID\n- 商品标题\n- ${data.filter(d => d['佣金比例']).length} 条有佣金数据\n- ${data.filter(d => d['到手价']).length} 条有价格数据\n- ${data.filter(d => d['划线价']).length} 条有划线价数据`);
        } catch (error) {
            console.error('导出Excel时出错:', error);
            alert('导出失败: ' + error.message);
        }
    }

    // 创建导出按钮
    function createExportButton() {
        // 移除已存在的按钮
        const existingButton = document.getElementById('jd-export-button');
        if (existingButton) {
            existingButton.remove();
        }
        
        const button = document.createElement('button');
        button.id = 'jd-export-button';
        button.innerHTML = '📥 导出商品数据';
        button.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            z-index: 10000;
            background: #ff6600;
            color: white;
            border: none;
            padding: 10px 15px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.3);
        `;
        
        button.addEventListener('click', async () => {
            const originalText = button.innerHTML;
            button.innerHTML = '⏳ 正在导出...';
            button.disabled = true;
            
            try {
                console.log('点击导出按钮,开始导出流程');
                await waitForElements();
                const data = extractProductData();
                console.log('数据提取完成,条数:', data.length);
                
                if (data.length > 0) {
                    exportToExcel(data);
                } else {
                    console.log('未找到商品数据,显示调试信息');
                    const allLinks = document.querySelectorAll('a[href*="//item.jd.com/"]');
                    const allParagraphs = document.querySelectorAll('p');
                    alert(`未找到有效商品数据\n\n调试信息:\n- 找到 ${allLinks.length} 个商品链接\n- 找到 ${allParagraphs.length} 个段落\n\n请检查页面是否完全加载`);
                }
            } catch (error) {
                console.error('导出失败:', error);
                alert('导出失败: ' + error.message);
            } finally {
                button.innerHTML = originalText;
                button.disabled = false;
            }
        });
        
        document.body.appendChild(button);
        console.log('导出按钮已创建');
    }

    // 页面加载后创建按钮
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            setTimeout(createExportButton, 1000);
        });
    } else {
        setTimeout(createExportButton, 1000);
    }

    // 监听路由变化
    let lastUrl = location.href;
    const observer = new MutationObserver(() => {
        const url = location.href;
        if (url !== lastUrl) {
            lastUrl = url;
            if (url.includes('planDetails')) {
                setTimeout(createExportButton, 2000);
            }
        }
    });
    
    observer.observe(document, {subtree: true, childList: true});

})();