淘宝评论下载

// ==UserScript==
// @name         淘宝/天猫评论图集高清原图分组下载器
// @namespace    tb-comment-hd-gallery-downloader
// @version      3.1
// @description  淘宝/天猫/天猫国际评论图下载:支持评论分组、图集单图多选、图集按ID自动分组,自动转 i0 高清原图
// @match        *://*.taobao.com/*
// @match        *://*.tmall.com/*
// @match        *://*.tmall.hk/*
// @match        *://*.world.taobao.com/*
// @grant        GM_download
// @connect      gw.alicdn.com
// @connect      img.alicdn.com
// @connect      alicdn.com
// ==/UserScript==

(function () {
    'use strict';

    const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

    function fullUrl(url) {
        if (!url) return '';
        if (url.startsWith('//')) return 'https:' + url;
        return url;
    }

    function cleanName(name) {
        return (name || '未命名')
            .replace(/[\\/:*?"<>|]/g, '_')
            .replace(/\s+/g, '')
            .slice(0, 60);
    }

    function getHDImage(url) {
        if (!url) return '';

        url = fullUrl(url).split('?')[0];

        // i1/i2/i3/i4/i5... 全部换成 i0
        url = url.replace(/\/i\d+\//i, '/i0/');

        // 保留到:
        // -0-rate.jpg
        // -0-tbbala.jpg
        // -2-rate.png
        // -0-rate_livephoto.jpg
        const match = url.match(/.*?-(0|2)-(rate|tbbala)(?:_livephoto)?\.(jpg|jpeg|png)/i);
        if (match) return match[0];

        const match2 = url.match(/.*?!!\d+-(rate|tbbala)(?:_livephoto)?\.(jpg|jpeg|png)/i);
        if (match2) return match2[0];

        const imgMatch = url.match(/.*?\.(jpg|jpeg|png|webp)/i);
        if (imgMatch) return imgMatch[0];

        return url;
    }

    function getImageSrc(img) {
        return img.getAttribute('data-src') ||
            img.getAttribute('src') ||
            img.currentSrc ||
            img.src ||
            '';
    }

    function isValidImageUrl(url) {
        if (!url) return false;
        if (url.includes('tps-145-145')) return false;
        if (url.includes('tps-56-56')) return false;
        if (url.includes('playerIcon')) return false;
        if (url.includes('6000000002189-2-tps')) return false;
        if (url.includes('6000000006294-2-tps')) return false;
        return /\.(jpg|jpeg|png|webp)$/i.test(url);
    }

    function getGalleryGroupId(url) {
        if (!url) return '未分组';

        // 优先提取 !! 后面的评论/买家核心ID
        // O1CNxxx_!!4611686018427381519-0-rate.jpg
        let match = url.match(/!!(\d+)-(0|2)-(rate|tbbala)/i);
        if (match) return match[1];

        // 兼容路径中的 ID
        // /imgextra/i0/4611686018427381519/O1CNxxx...
        match = url.match(/\/i0\/(\d+)\//i);
        if (match) return match[1];

        return '未分组';
    }

    function getFileExt(url) {
        const match = url.match(/\.(jpg|jpeg|png|webp)$/i);
        return match ? match[1].toLowerCase() : 'jpg';
    }

    function getComments() {
        return [...document.querySelectorAll(
            '[class*="Comment--"], [class*="comment--"], [class*="comment"]'
        )];
    }

    function getImages(comment) {
        const imgs = [...comment.querySelectorAll('img')];

        const urls = imgs
            .map(img => getHDImage(getImageSrc(img)))
            .filter(isValidImageUrl);

        return [...new Set(urls)];
    }

    function getGroupName(comment, index) {
        const user = comment.querySelector('[class*="userName"], [class*="username"]')
            ?.innerText
            ?.trim() || `评论${index + 1}`;

        const meta = comment.querySelector('[class*="meta"], [class*="date"]')
            ?.innerText
            ?.trim() || '';

        const date = meta.match(/\d{4}-\d{2}-\d{2}/)?.[0] || '';

        return cleanName(`${index + 1}_${date}_${user}`);
    }

    function getImageItems() {
        return [...document.querySelectorAll(
            '[class*="commentsImgItem"], [class*="photo--"], [class*="cover--"]'
        )];
    }

    function injectStyle() {
        if (document.querySelector('#tb-hd-style')) return;

        const style = document.createElement('style');
        style.id = 'tb-hd-style';

        style.innerHTML = `
            #tb-hd-download-panel {
                position: fixed;
                right: 24px;
                top: 110px;
                z-index: 999999999;
                width: 500px;
                padding: 14px 16px;
                border-radius: 18px;
                background: rgba(255,255,255,.95);
                backdrop-filter: blur(16px);
                box-shadow: 0 14px 40px rgba(0,0,0,.12), 0 6px 18px rgba(255,80,0,.18);
                border: 1px solid rgba(255,80,0,.18);
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, "Microsoft YaHei", sans-serif;
                color: #222;
            }

            .tb-panel-top {
                display: flex;
                align-items: center;
                justify-content: space-between;
                gap: 12px;
                margin-bottom: 12px;
            }

            .tb-panel-title {
                display: flex;
                align-items: center;
                gap: 10px;
            }

            .tb-logo {
                width: 34px;
                height: 34px;
                border-radius: 11px;
                background: linear-gradient(135deg, #ff8a1c, #ff3d00);
                color: #fff;
                display: flex;
                align-items: center;
                justify-content: center;
                font-weight: 900;
                font-size: 18px;
                box-shadow: 0 6px 14px rgba(255,80,0,.25);
            }

            .tb-title-main {
                font-size: 16px;
                font-weight: 800;
                line-height: 1.2;
            }

            .tb-title-sub {
                font-size: 12px;
                color: #999;
                margin-top: 2px;
            }

            .tb-mini-tip {
                font-size: 12px;
                color: #ff5000;
                background: #fff3eb;
                border: 1px solid rgba(255,80,0,.16);
                padding: 5px 9px;
                border-radius: 999px;
                white-space: nowrap;
            }

            .tb-btn-row {
                display: grid;
                grid-template-columns: repeat(4, 1fr);
                gap: 8px;
            }

            .tb-btn {
                height: 38px;
                border: none;
                outline: none;
                border-radius: 12px;
                font-size: 13px;
                font-weight: 700;
                cursor: pointer;
                transition: all .18s ease;
            }

            .tb-btn:hover {
                transform: translateY(-1px);
                filter: brightness(1.04);
            }

            .tb-btn:active {
                transform: scale(.98);
            }

            .tb-btn.primary {
                color: #fff;
                background: linear-gradient(135deg, #ff8a1c, #ff5000);
                box-shadow: 0 6px 14px rgba(255,80,0,.24);
            }

            .tb-btn.success {
                color: #fff;
                background: linear-gradient(135deg, #24c6a0, #12b886);
                box-shadow: 0 6px 14px rgba(18,184,134,.20);
            }

            .tb-btn.blue {
                color: #fff;
                background: linear-gradient(135deg, #4dabf7, #228be6);
                box-shadow: 0 6px 14px rgba(34,139,230,.20);
            }

            .tb-btn.light {
                color: #ff5000;
                background: #fff4ed;
                border: 1px solid rgba(255,80,0,.22);
            }

            .tb-download-label {
                display: inline-flex;
                align-items: center;
                gap: 8px;
                padding: 7px 12px;
                margin-bottom: 10px;
                border-radius: 999px;
                background: linear-gradient(135deg, #fff7f2, #fff);
                color: #ff5000;
                font-size: 13px;
                font-weight: 700;
                border: 1px solid rgba(255,80,0,.25);
                box-shadow: 0 4px 12px rgba(255,80,0,.08);
                cursor: pointer;
                user-select: none;
            }

            .tb-download-label input {
                accent-color: #ff5000;
                width: 15px;
                height: 15px;
            }

            .tb-img-select-wrap {
                position: absolute !important;
                right: 8px;
                top: 8px;
                z-index: 99999999;
                width: 28px;
                height: 28px;
                border-radius: 50%;
                background: rgba(0,0,0,.42);
                backdrop-filter: blur(8px);
                display: flex;
                align-items: center;
                justify-content: center;
                cursor: pointer;
                box-shadow: 0 4px 12px rgba(0,0,0,.18);
                border: 1px solid rgba(255,255,255,.45);
            }

            .tb-img-select-wrap input {
                width: 17px;
                height: 17px;
                margin: 0;
                accent-color: #ff5000;
                cursor: pointer;
            }

            .tb-img-selected {
                outline: 3px solid #ff5000 !important;
                outline-offset: -3px !important;
                border-radius: 8px !important;
            }

            .tb-img-selected::after {
                content: "已选";
                position: absolute;
                left: 8px;
                top: 8px;
                z-index: 99999998;
                color: #fff;
                background: linear-gradient(135deg, #ff8a1c, #ff5000);
                font-size: 12px;
                font-weight: 700;
                padding: 4px 8px;
                border-radius: 999px;
                box-shadow: 0 4px 10px rgba(255,80,0,.25);
            }
        `;

        document.head.appendChild(style);
    }

    function addPanel() {
        if (document.querySelector('#tb-hd-download-panel')) return;

        injectStyle();

        const panel = document.createElement('div');
        panel.id = 'tb-hd-download-panel';

        panel.innerHTML = `
            <div class="tb-panel-top">
                <div class="tb-panel-title">
                    <span class="tb-logo">淘</span>
                    <div>
                        <div class="tb-title-main">评论原图下载</div>
                        <div class="tb-title-sub">分组下载 · 图集按ID分组 · i0 原图</div>
                    </div>
                </div>
                <div class="tb-mini-tip">高清大图</div>
            </div>

            <div class="tb-btn-row">
                <button class="tb-btn primary" id="tb-scan-group-btn">评论分组</button>
                <button class="tb-btn blue" id="tb-scan-img-btn">图集选择</button>
                <button class="tb-btn success" id="tb-download-btn">下载选中</button>
                <button class="tb-btn light" id="tb-select-all-btn">全选/反选</button>
            </div>
        `;

        document.body.appendChild(panel);

        document.querySelector('#tb-scan-group-btn').onclick = markComments;
        document.querySelector('#tb-scan-img-btn').onclick = markImageItems;
        document.querySelector('#tb-download-btn').onclick = downloadSelected;
        document.querySelector('#tb-select-all-btn').onclick = toggleAll;
    }

    function markComments() {
        const comments = getComments();
        let validCount = 0;

        comments.forEach((comment, index) => {
            if (comment.querySelector('.tb-download-check')) return;

            const imgs = getImages(comment);
            if (!imgs.length) return;

            validCount++;

            const box = document.createElement('label');
            box.className = 'tb-download-label';

            box.innerHTML = `
                <input type="checkbox" class="tb-download-check tb-group-check">
                <span>本组 ${imgs.length} 张原图</span>
            `;

            comment.prepend(box);
            comment.dataset.tbGroupName = getGroupName(comment, index);
        });

        alert(`评论分组扫描完成:找到 ${validCount} 个图片评论分组`);
    }

    function markImageItems() {
        const items = getImageItems();
        let validCount = 0;

        items.forEach((item, index) => {
            if (item.querySelector('.tb-single-check')) return;

            const img = item.querySelector('img');
            if (!img) return;

            const url = getHDImage(getImageSrc(img));
            if (!isValidImageUrl(url)) return;

            const groupId = getGalleryGroupId(url);

            item.style.position = 'relative';
            item.dataset.tbImageUrl = url;
            item.dataset.tbImageIndex = String(index + 1);
            item.dataset.tbGalleryGroupId = groupId;

            const wrap = document.createElement('label');
            wrap.className = 'tb-img-select-wrap';
            wrap.title = `选中下载高清原图,分组:${groupId}`;

            wrap.innerHTML = `
                <input type="checkbox" class="tb-single-check">
            `;

            const checkbox = wrap.querySelector('input');

            checkbox.addEventListener('change', () => {
                item.classList.toggle('tb-img-selected', checkbox.checked);
            });

            wrap.addEventListener('click', e => {
                e.stopPropagation();
            });

            item.appendChild(wrap);
            validCount++;
        });

        alert(`图集扫描完成:找到 ${validCount} 张可选择图片`);
    }

    function toggleAll() {
        const checks = [
            ...document.querySelectorAll('.tb-download-check'),
            ...document.querySelectorAll('.tb-single-check')
        ];

        if (!checks.length) {
            alert('请先点击“评论分组”或“图集选择”');
            return;
        }

        const hasUnchecked = checks.some(c => !c.checked);

        checks.forEach(c => {
            c.checked = hasUnchecked;

            const item = c.closest('[class*="commentsImgItem"], [class*="photo--"], [class*="cover--"]');
            if (item) {
                item.classList.toggle('tb-img-selected', c.checked);
            }
        });
    }

    async function downloadSelected() {
        const tasks = [];

        const selectedGroups = [...document.querySelectorAll('.tb-group-check:checked')]
            .map(input => input.closest('[class*="Comment--"], [class*="comment--"], [class*="comment"]'))
            .filter(Boolean);

        selectedGroups.forEach((comment, groupIndex) => {
            const groupName = comment.dataset.tbGroupName || `评论分组_${groupIndex + 1}`;
            const imgs = getImages(comment);

            imgs.forEach((url, imgIndex) => {
                const ext = getFileExt(url);
                tasks.push({
                    url,
                    filename: `${groupName}/${String(imgIndex + 1).padStart(2, '0')}.${ext}`
                });
            });
        });

        const selectedSingles = [...document.querySelectorAll('.tb-single-check:checked')]
            .map(input => input.closest('[class*="commentsImgItem"], [class*="photo--"], [class*="cover--"]'))
            .filter(Boolean);

        const groupCounters = {};

        selectedSingles.forEach(item => {
            const url = item.dataset.tbImageUrl;
            if (!url) return;

            const groupId = item.dataset.tbGalleryGroupId || getGalleryGroupId(url);
            const ext = getFileExt(url);

            if (!groupCounters[groupId]) {
                groupCounters[groupId] = 0;
            }

            groupCounters[groupId]++;

            tasks.push({
                url,
                filename: `图集分组/${groupId}/${String(groupCounters[groupId]).padStart(2, '0')}.${ext}`
            });
        });

        const uniqueTasks = [];
        const seen = new Set();

        tasks.forEach(task => {
            if (!task.url || seen.has(task.url)) return;
            seen.add(task.url);
            uniqueTasks.push(task);
        });

        if (!uniqueTasks.length) {
            alert('请先勾选要下载的图片或评论分组');
            return;
        }

        for (let i = 0; i < uniqueTasks.length; i++) {
            const task = uniqueTasks[i];

            console.log('下载高清原图:', task.url, task.filename);

            GM_download({
                url: task.url,
                name: task.filename,
                saveAs: false,
                onload: () => console.log('下载完成:', task.filename),
                onerror: err => console.warn('下载失败:', task.url, err)
            });

            await sleep(500);
        }

        alert(`已开始下载 ${uniqueTasks.length} 张高清原图。\n\n图集图片会按核心ID自动分组保存。\n首次使用请允许浏览器“多个文件下载”。`);
    }

    addPanel();

    const observer = new MutationObserver(() => {
        addPanel();
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

})();


评论

我要评论

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