京东主图以及评论组图下载

京东主图以及评论组图下载

// ==UserScript==
// @name         京东商品提取器 (主图/媒体图单选切换+比例区分)
// @namespace    http://tampermonkey.net/
// @version      3.0
// @description  主图/媒体图单选切换,主图1:1比例,媒体图3:4比例,互斥显示不同时展示
// @author       You
// @match        *://item.jd.com/*.html
// @grant        GM_registerMenuCommand
// @require      https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js
// ==/UserScript==

(function() {
    'use strict';

    // ========== 配置区 ==========
    const CONFIG = {
        waitParseTime: 1500, // 页面加载后自动提取主图的等待时间
        panelPosition: 'left', // 弹屏位置 left/right
        panelWidth: '460px', // 弹屏宽度
        autoRun: true, // 打开页面是否自动提取主图
        defaultMode: 'main' // 默认选中模式:main=主图模式,media=媒体图模式
    };

    // ========== 全局变量 ==========
    let globalProductInfo = {}; // 商品基础信息
    let globalMainImageList = []; // 主图(原轮播图)列表
    let globalMediaImageList = []; // 媒体层图片列表
    let currentMode = CONFIG.defaultMode; // 当前选中的模式
    let panelDom = null; // 弹屏DOM

    // ========== 1. 商品基础信息提取(不变) ==========
    function extractProductInfo() {
        let sku = null;
        let productTitle = "未知商品";
        const fullHtml = document.documentElement.innerHTML;

        // 优先从chat.jd.com链接提取pid作为SKU
        const chatLinkRegex = /<a[^>]+href="([^"]*chat\.jd\.com[^"]*)"[^>]*>/g;
        let linkMatch;
        while ((linkMatch = chatLinkRegex.exec(fullHtml)) !== null) {
            const pidMatch = linkMatch[1].match(/[?&]pid=(\d+)/);
            if (pidMatch && pidMatch[1]) {
                sku = pidMatch[1];
                console.log(`✅ 从客服链接提取到SKU(pid): ${sku}`);
                break;
            }
        }

        // 兜底:从URL提取SKU
        if (!sku) {
            const urlMatch = window.location.pathname.match(/\/(\d+)\.html/);
            sku = urlMatch ? urlMatch[1] : "unknown_sku";
            console.log(`⚠️ 未找到pid,从URL提取SKU: ${sku}`);
        }

        // 提取商品名
        const titleEl1 = document.querySelector(".sku-title-name");
        const titleEl2 = document.querySelector(".sku-name");
        if (titleEl1) productTitle = titleEl1.innerText.trim();
        else if (titleEl2) productTitle = titleEl2.innerText.trim();
        else productTitle = document.title.replace(/【.*?】|京东|,.*/g, "").trim();

        // 清洗非法字符,生成规范文件夹名
        const safeTitle = productTitle.replace(/[\\/:*?"<>|\r\n]/g, "").replace(/\s+/g, " ");
        const folderName = `${sku}----${safeTitle}`;

        globalProductInfo = { sku, title: safeTitle, folderName };
        return globalProductInfo;
    }

    // ========== 2. 主图提取(原轮播图逻辑不变) ==========
    function extractMainImages() {
        const imageUrlSet = new Set();
        const carouselContainer = document.querySelector('.image-carousel-content');

        if (carouselContainer) {
            const imgs = carouselContainer.querySelectorAll('img');
            imgs.forEach(img => {
                let src = img.src || img.getAttribute('data-lazy-img') || img.getAttribute('src');
                if (src) {
                    if (src.startsWith('//')) src = 'https:' + src;
                    let cleanUrl = src.replace(/\/s\d+x\d+_/, '/'); // 删尺寸前缀
                    cleanUrl = cleanUrl.replace('.avif', ''); // 删.avif后缀
                    imageUrlSet.add(cleanUrl);
                }
            });
        } else {
            // 兜底全局提取
            const fullHtml = document.documentElement.innerHTML;
            const imgRegex = /(https?:)?\/\/img\d+\.360buyimg\.com\/[^"'\s]+\.(avif|jpg|png)/g;
            let m;
            while((m = imgRegex.exec(fullHtml)) !== null) {
                let u = m[0].startsWith('//') ? 'https:' + m[0] : m[0];
                u = u.replace(/\/s\d+x\d+_/, '/').replace('.avif', '');
                imageUrlSet.add(u);
            }
        }

        globalMainImageList = Array.from(imageUrlSet);
        console.log(`✅ 提取到主图共 ${globalMainImageList.length} 张`, globalMainImageList);
        return globalMainImageList;
    }

    // ========== 3. 媒体层图片提取(逻辑不变,仅适配切换) ==========
    function extractMediaImages() {
        const imageUrlSet = new Set();
        const mediaContainer = document.querySelector('.jdc-pc-media-preview-list');

        // 核心校验:容器不存在,提示用户先打开对应区域
        if (!mediaContainer) {
            alert("⚠️ 未找到媒体层容器\n请先在页面中打开/展开【商品详情/媒体预览】区域,让 .jdc-pc-media-preview-list 层加载完成后,再重试");
            return [];
        }

        // 提取容器内所有图片,兼容懒加载
        const imgs = mediaContainer.querySelectorAll('img');
        imgs.forEach(img => {
            let src = img.src
                || img.getAttribute('data-lazy-img')
                || img.getAttribute('data-src')
                || img.getAttribute('origin-src')
                || img.getAttribute('src');
            if (src) {
                if (src.startsWith('//')) src = 'https:' + src;
                let cleanUrl = src.replace(/\/s\d+x\d+_/, '/');
                cleanUrl = cleanUrl.replace('.avif', '');
                imageUrlSet.add(cleanUrl);
            }
        });

        globalMediaImageList = Array.from(imageUrlSet);
        console.log(`✅ 提取到媒体层图片共 ${globalMediaImageList.length} 张`, globalMediaImageList);

        // 提取后重新渲染面板,刷新预览
        renderInfoPanel();

        if (globalMediaImageList.length === 0) {
            alert("⚠️ 容器内未提取到有效图片\n请确保图片已加载完成(滚动到对应区域让图片渲染),再重试");
        } else {
            alert(`✅ 媒体图提取成功!共获取到 ${globalMediaImageList.length} 张`);
        }

        return globalMediaImageList;
    }

    // ========== 4. 模式切换核心函数 ==========
    function switchMode(targetMode) {
        if (targetMode === currentMode) return; // 相同模式不重复渲染
        currentMode = targetMode;
        renderInfoPanel(); // 切换模式后重新渲染面板
        console.log(`✅ 已切换到${currentMode === 'main' ? '主图' : '媒体图'}模式`);
    }

    // ========== 5. 弹屏渲染(核心修改:单选互斥显示+比例区分) ==========
    function renderInfoPanel() {
        // 已存在面板先移除,避免重复渲染
        if (panelDom) document.body.removeChild(panelDom);

        // 创建面板DOM
        panelDom = document.createElement('div');
        panelDom.id = 'jd-extract-panel';
        panelDom.style.cssText = `
            position: fixed;
            ${CONFIG.panelPosition}: 0;
            top: 50%;
            transform: translateY(-50%);
            width: ${CONFIG.panelWidth};
            max-height: 90vh;
            background: #fff;
            box-shadow: 0 0 15px rgba(0,0,0,0.2);
            z-index: 99999999;
            border-radius: 0 8px 8px 0;
            padding: 16px;
            box-sizing: border-box;
            overflow-y: auto;
            font-family: Microsoft YaHei, sans-serif;
            font-size: 14px;
            color: #333;
        `;

        const { sku, title, folderName } = globalProductInfo;
        const isMainMode = currentMode === 'main';
        const currentImageList = isMainMode ? globalMainImageList : globalMediaImageList;
        const imageCount = currentImageList.length;

        // 面板HTML内容
        panelDom.innerHTML = `
            <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #eee;">
                <h3 style="margin: 0; font-size: 16px; font-weight: bold; color: #e4393c;">京东商品提取器</h3>
                <button id="panel-close-btn" style="border: none; background: #f5f5f5; border-radius: 4px; padding: 4px 8px; cursor: pointer;">关闭</button>
            </div>

            <!-- 商品基础信息 -->
            <div style="margin-bottom: 12px;">
                <p style="margin: 6px 0; line-height: 1.5;"><strong>商品SKU:</strong><span style="color: #e4393c; font-weight: bold;">${sku}</span></p>
                <p style="margin: 6px 0; line-height: 1.5;"><strong>商品名称:</strong><span style="word-break: break-all; font-size: 12px;">${title}</span></p>
                <p style="margin: 6px 0; line-height: 1.5;"><strong>文件夹名:</strong><span style="word-break: break-all; font-size: 12px; color: #666;">${folderName}</span></p>
                <p style="margin: 6px 0; line-height: 1.5;">
                    <strong>当前模式:</strong>
                    <span style="color: ${isMainMode ? '#0066cc' : '#ff5722'}; font-weight: bold;">
                        ${isMainMode ? '主图模式' : '媒体图模式'}
                    </span>
                    <span style="margin: 0 10px;">|</span>
                    <strong>图片数量:</strong>
                    <span style="font-weight: bold;">${imageCount} 张</span>
                </p>
            </div>

            <!-- 【核心新增】模式切换单选器 -->
            <div style="display: flex; gap: 2px; margin-bottom: 16px; background: #f5f5f5; border-radius: 6px; padding: 3px;">
                <button id="mode-main-btn" style="flex: 1; border: none; border-radius: 4px; padding: 8px 0; cursor: pointer; font-weight: bold; transition: all 0.2s; ${isMainMode ? 'background: #e4393c; color: #fff;' : 'background: transparent; color: #666;'}">
                    主图模式
                </button>
                <button id="mode-media-btn" style="flex: 1; border: none; border-radius: 4px; padding: 8px 0; cursor: pointer; font-weight: bold; transition: all 0.2s; ${!isMainMode ? 'background: #ff5722; color: #fff;' : 'background: transparent; color: #666;'}">
                    媒体图模式
                </button>
            </div>

            <!-- 核心操作按钮:一行3个,根据模式动态切换 -->
            <div style="display: flex; gap: 8px; margin-bottom: 16px;">
                ${isMainMode
                    // 主图模式按钮:下载主图、复制SKU、重新提取
                    ? `
                        <button id="main-download-btn" style="flex: 1; border: none; background: #e4393c; color: #fff; border-radius: 4px; padding: 8px 0; cursor: pointer; font-weight: bold;">下载主图</button>
                        <button id="copy-btn" style="flex: 1; border: none; background: #28a745; color: #fff; border-radius: 4px; padding: 8px 0; cursor: pointer; font-weight: bold;">复制SKU</button>
                        <button id="refresh-btn" style="flex: 1; border: none; background: #0066cc; color: #fff; border-radius: 4px; padding: 8px 0; cursor: pointer; font-weight: bold;">重新提取</button>
                    `
                    // 媒体图模式按钮:提取媒体图、下载媒体图、复制SKU
                    : `
                        <button id="media-extract-btn" style="flex: 1; border: none; background: #ff5722; color: #fff; border-radius: 4px; padding: 8px 0; cursor: pointer; font-weight: bold;">提取媒体图</button>
                        <button id="media-download-btn" style="flex: 1; border: none; background: #f57c00; color: #fff; border-radius: 4px; padding: 8px 0; cursor: pointer; font-weight: bold;" ${imageCount === 0 ? 'disabled' : ''}>下载媒体图</button>
                        <button id="copy-btn" style="flex: 1; border: none; background: #28a745; color: #fff; border-radius: 4px; padding: 8px 0; cursor: pointer; font-weight: bold;">复制SKU</button>
                    `
                }
            </div>

            <!-- 【核心修改】唯一预览区:根据模式切换比例,绝不两个都显示 -->
            <div style="margin-bottom: 12px;">
                <div style="margin-bottom: 8px; font-weight: bold; color: ${isMainMode ? '#0066cc' : '#ff5722'};">
                    ${isMainMode ? '主图预览 (1:1正方形 / 点击查看大图)' : '媒体图预览 (3:4竖版 / 点击查看大图)'}
                </div>
                <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
                    ${imageCount === 0
                        ? `<div style="grid-column: span 2; text-align: center; color: #999; padding: 40px 20px;">
                            ${isMainMode ? '暂无主图' : '暂无媒体图<br>请先点击【提取媒体图】按钮'}
                        </div>`
                        : currentImageList.map((url, index) => `
                            <div style="border: 1px solid #eee; border-radius: 4px; overflow: hidden; background: #f9f9f9;">
                                <a href="${url}" target="_blank" style="display: block; text-decoration: none;">
                                    <!-- 核心比例控制:主图1:1,媒体图3:4 -->
                                    <img
                                        src="${url}"
                                        style="width: 100%; aspect-ratio: ${isMainMode ? '1/1' : '3/4'}; object-fit: cover; display: block; border-bottom: 1px solid #eee;"
                                        alt="${isMainMode ? '主图' : '媒体图'}${index}"
                                        onerror="this.style.display='none'"
                                    >
                                    <div style="padding: 6px; text-align: center; font-size: 12px; color: #666;">
                                        ${isMainMode ? `${index}.jpg` : `media_${index}.jpg`}
                                    </div>
                                </a>
                            </div>
                        `).join('')
                    }
                </div>
            </div>
        `;

        // 插入到页面
        document.body.appendChild(panelDom);
        // 绑定所有按钮事件
        bindPanelEvents();
    }

    // ========== 6. 按钮事件绑定 ==========
    function bindPanelEvents() {
        // 关闭按钮
        document.getElementById('panel-close-btn').addEventListener('click', () => {
            if (panelDom) document.body.removeChild(panelDom);
            panelDom = null;
        });

        // 模式切换按钮
        document.getElementById('mode-main-btn').addEventListener('click', () => switchMode('main'));
        document.getElementById('mode-media-btn').addEventListener('click', () => switchMode('media'));

        // 复制SKU按钮(两个模式通用)
        document.getElementById('copy-btn').addEventListener('click', async () => {
            const { sku, title } = globalProductInfo;
            const textToCopy = `${sku}----${title}`;

            try {
                await navigator.clipboard.writeText(textToCopy);
                const copyBtn = document.getElementById('copy-btn');
                const originalText = copyBtn.innerText;
                copyBtn.innerText = '✅ 已复制';
                copyBtn.style.background = '#218838';
                setTimeout(() => {
                    copyBtn.innerText = originalText;
                    copyBtn.style.background = '#28a745';
                }, 1500);
            } catch (err) {
                console.error('复制失败:', err);
                alert('❌ 复制失败,请手动复制');
            }
        });

        // ========== 主图模式按钮事件 ==========
        if (currentMode === 'main') {
            // 重新提取主图
            document.getElementById('refresh-btn').addEventListener('click', () => {
                extractMainImages();
                renderInfoPanel();
                alert('✅ 主图重新提取完成');
            });
            // 下载主图
            document.getElementById('main-download-btn').addEventListener('click', downloadMainImages);
        }

        // ========== 媒体图模式按钮事件 ==========
        if (currentMode === 'media') {
            // 提取媒体图
            document.getElementById('media-extract-btn').addEventListener('click', extractMediaImages);
            // 下载媒体图
            document.getElementById('media-download-btn').addEventListener('click', downloadMediaImages);
        }
    }

    // ========== 7. 主图下载逻辑(原有规范不变) ==========
    async function downloadMainImages() {
        const { sku, title, folderName } = globalProductInfo;
        const imageUrls = globalMainImageList;

        if (imageUrls.length === 0) { alert("⚠️ 无主图可下载"); return; }
        if (!window.showDirectoryPicker) { alert("⚠️ 请使用 Chrome/Edge 86+ 版本浏览器"); return; }

        try {
            const rootDirHandle = await window.showDirectoryPicker({ mode: 'readwrite', startIn: 'downloads' });
            const productDirHandle = await rootDirHandle.getDirectoryHandle(folderName, { create: true });

            // 写入skutitle.txt
            const textFileHandle = await productDirHandle.getFileHandle('skutitle.txt', { create: true });
            const textWritable = await textFileHandle.createWritable();
            await textWritable.write(title);
            await textWritable.close();

            // 批量下载主图
            let successCount = 0;
            const downloadBtn = document.getElementById('main-download-btn');
            const originalText = downloadBtn.innerText;

            for (let i = 0; i < imageUrls.length; i++) {
                const url = imageUrls[i];
                try {
                    downloadBtn.innerText = `下载中 ${i+1}/${imageUrls.length}`;
                    downloadBtn.disabled = true;

                    const response = await axios.get(url, { responseType: 'blob', timeout: 15000 });
                    let fileExt = 'jpg';
                    if (url.includes('.png')) fileExt = 'png';
                    if (url.includes('.webp')) fileExt = 'webp';

                    const fileName = `${i}.${fileExt}`;
                    const imgFileHandle = await productDirHandle.getFileHandle(fileName, { create: true });
                    const imgWritable = await imgFileHandle.createWritable();
                    await imgWritable.write(response.data);
                    await imgWritable.close();

                    successCount++;
                } catch (err) {
                    console.error(`❌ 主图第${i}张下载失败`, err);
                }
            }

            downloadBtn.innerText = originalText;
            downloadBtn.disabled = false;
            alert(`🎉 主图下载完成!\n成功保存 ${successCount}/${imageUrls.length} 张`);

        } catch (err) {
            if (err.name !== 'AbortError') {
                console.error(err);
                alert("❌ 主图下载出错: " + err.message);
                const btn = document.getElementById('main-download-btn');
                if(btn) { btn.innerText = '下载主图'; btn.disabled = false; }
            }
        }
    }

    // ========== 8. 媒体图下载逻辑(原有规范不变) ==========
    async function downloadMediaImages() {
        const { sku, title, folderName } = globalProductInfo;
        const imageUrls = globalMediaImageList;

        if (imageUrls.length === 0) {
            alert("⚠️ 无媒体图可下载\n请先点击【提取媒体图】按钮,成功提取后再下载");
            return;
        }
        if (!window.showDirectoryPicker) {
            alert("⚠️ 请使用 Chrome/Edge 86+ 版本浏览器");
            return;
        }

        try {
            const rootDirHandle = await window.showDirectoryPicker({ mode: 'readwrite', startIn: 'downloads' });
            const productDirHandle = await rootDirHandle.getDirectoryHandle(folderName, { create: true });

            // 补全skutitle.txt
            try {
                await productDirHandle.getFileHandle('skutitle.txt', { create: false });
            } catch {
                const textFileHandle = await productDirHandle.getFileHandle('skutitle.txt', { create: true });
                const textWritable = await textFileHandle.createWritable();
                await textWritable.write(title);
                await textWritable.close();
            }

            // 批量下载媒体图
            let successCount = 0;
            const downloadBtn = document.getElementById('media-download-btn');
            const originalText = downloadBtn.innerText;

            for (let i = 0; i < imageUrls.length; i++) {
                const url = imageUrls[i];
                try {
                    downloadBtn.innerText = `下载中 ${i+1}/${imageUrls.length}`;
                    downloadBtn.disabled = true;

                    const response = await axios.get(url, { responseType: 'blob', timeout: 15000 });
                    let fileExt = 'jpg';
                    if (url.includes('.png')) fileExt = 'png';
                    if (url.includes('.webp')) fileExt = 'webp';

                    const fileName = `media_${i}.${fileExt}`;
                    const imgFileHandle = await productDirHandle.getFileHandle(fileName, { create: true });
                    const imgWritable = await imgFileHandle.createWritable();
                    await imgWritable.write(response.data);
                    await imgWritable.close();

                    successCount++;
                } catch (err) {
                    console.error(`❌ 媒体图第${i}张下载失败`, err);
                }
            }

            downloadBtn.innerText = originalText;
            downloadBtn.disabled = false;
            alert(`🎉 媒体图下载完成!\n成功保存 ${successCount}/${imageUrls.length} 张`);

        } catch (err) {
            if (err.name !== 'AbortError') {
                console.error(err);
                alert("❌ 媒体图下载出错: " + err.message);
                const btn = document.getElementById('media-download-btn');
                if(btn) { btn.innerText = '下载媒体图'; btn.disabled = false; }
            }
        }
    }

    // ========== 9. 主执行函数 ==========
    function mainExtract() {
        console.log("========== 开始提取京东商品信息 ==========");
        extractProductInfo();
        extractMainImages();
        renderInfoPanel();
        console.log("========== 提取完成,已渲染左侧面板 ==========");
    }

    // ========== 10. 页面加载监听 ==========
    window.addEventListener('load', () => {
        if (CONFIG.autoRun) {
            setTimeout(() => mainExtract(), CONFIG.waitParseTime);
        }
    });

    // ========== 11. 手动执行菜单 ==========
    GM_registerMenuCommand("🔍 手动提取商品信息", mainExtract);
    GM_registerMenuCommand("📸 手动提取媒体层图片", extractMediaImages);

})();


评论

我要评论

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。