图片自动下载器

图片自动下载器
// ==UserScript==
// @name         微博 pic_info 图片自动下载器(适配嵌套结构)
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  提取 page_info.pic_info 中的图片URL并自动下载,按页面分类存储
// @author       Your Name
// @match        *://weibo.com/*
// @match        *://www.weibo.com/*
// @grant        GM_download
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // -------------------------- 核心配置 --------------------------
    const TARGET_UID = '6409560260'; // 固定目标用户UID(与提供数据中的用户ID匹配)
    const processedImages = new Set(); // 用于记录已下载的图片URL,避免重复下载
    const processedPages = new Set(); // 避免重复下载同一页面图片
    const DOWNLOAD_DELAY = 1500; // 下载间隔(ms),避免请求过快被拦截
    // 控制台日志样式(不同状态用不同颜色高亮,清晰区分)
    const LOG_STYLES = {
        init: 'color: #9C27B0; font-weight: bold; background: #F3E5F5; padding: 2px 4px; border-radius: 2px',
        intercept: 'color: #1976D2; font-weight: bold; background: #E3F2FD; padding: 2px 4px; border-radius: 2px',
        download: 'color: #2E7D32; font-weight: bold; background: #E8F5E9; padding: 2px 4px; border-radius: 2px',
        success: 'color: #1B5E20; font-weight: bold; background: #C8E6C9; padding: 2px 4px; border-radius: 2px',
        error: 'color: #C62828; font-weight: bold; background: #FFEBEE; padding: 2px 4px; border-radius: 2px'
    };

    // -------------------------- 工具函数 --------------------------
    /**
     * 控制台日志输出(带时间戳和样式)
     * @param {string} msg - 日志内容
     * @param {string} type - 日志类型(init/intercept/download/success/error)
     */
    function log(msg, type = 'init') {
        console.log(`%c[微博pic_info下载器] ${new Date().toLocaleTimeString()} | ${msg}`, LOG_STYLES[type]);
    }

    /**
     * 从URL提取页码(忽略其他参数,仅关注page字段)
     * @param {string} url - 请求URL
     * @returns {string|number} 页码(数字或"未知")
     */
    function getPageNum(url) {
        try {
            const fullUrl = url.startsWith('http') ? url : `https://weibo.com${url}`;
            const pageStr = new URL(fullUrl).searchParams.get('page');
            return pageStr ? parseInt(pageStr, 10) : '未知';
        } catch (e) {
            log(`URL解析出错: ${e.message}`, 'error');
            return '未知';
        }
    }

    /**
     * 从嵌套的 page_info.pic_info 中提取图片URL(适配提供的数据结构,自动去重)
     * @param {Object} weiboItem - 单条微博数据(包含 page_info.pic_info 字段)
     * @returns {Array} 去重后的图片URL数组
     */
    function extractImageUrls(weiboItem) {
        const imageUrls = new Set();
        const pageInfo = weiboItem.page_info;

        if (!pageInfo || typeof pageInfo !== 'object') {
            log(`微博${weiboItem.id || '未知ID'}:无 page_info 字段,跳过`, 'error');
            return [];
        }

        const picInfo = pageInfo.pic_info;
        if (!picInfo || typeof picInfo !== 'object') {
            log(`微博${weiboItem.id || '未知ID'}:page_info 下无 pic_info 字段,跳过`, 'error');
            return [];
        }

        // 【关键修改1:遍历所有可能的图片字段,兼容微博不同数据格式】
        const picSources = [
            picInfo.pic_big?.url,    // 高清图1
            picInfo.pic_middle?.url, // 中等图
            picInfo.pic_small?.url,  // 缩略图
            picInfo.url,             // 直接挂载的URL(部分场景)
            weiboItem.pic_url        // 备用:微博根节点的图片URL(防止嵌套结构缺失)
        ].filter(Boolean); // 过滤空值

        // 【关键修改2:处理URL,强制去除防盗链参数+转换高清格式】
        picSources.forEach(rawUrl => {
            if (typeof rawUrl === 'string' && rawUrl.startsWith('http')) {
                // 1. 去除URL所有参数(如 ?k=xxx&t=xxx 防盗链参数)
                const cleanUrl = rawUrl.split('?')[0];
                // 2. 强制转换为高清格式(替换所有缩略图标识)
                const highDefUrl = cleanUrl
                    .replace('/thumbnail/', '/large/') // 缩略图目录→高清目录
                    .replace('mw690/', 'large/')       // 尺寸参数→高清
                    .replace('mw1080/', 'large/')      // 兼容其他尺寸
                    .replace('mw2000/', 'large/');     // 兼容超大图
                imageUrls.add(highDefUrl);
            }
        });

        const urlsArr = Array.from(imageUrls);
        log(`微博${weiboItem.id || '未知ID'}:提取到 ${urlsArr.length} 张有效图片`, 'intercept');
        return urlsArr;
    }



    /**
     * 自动下载图片(使用 GM_download,按“微博UID-页码”分类文件夹)
     * @param {Array} imageUrls - 图片URL数组
     * @param {number|string} pageNum - 当前页码
     * @param {string} weiboId - 微博ID(用于命名图片,避免重复)
     */
async function downloadImages(imageUrls, pageNum, weiboId) {
    if (imageUrls.length === 0) return;
    if (typeof GM_download === 'undefined') {
        log(`下载初始化失败:GM_download API未加载`, 'error');
        return;
    }

    for (let i = 0; i < imageUrls.length; i++) {
        const imgUrl = imageUrls[i];
        if (processedImages.has(imgUrl)) {
            log(`跳过重复图片:${imgUrl.split('/').pop()}`, 'download');
            continue;
        }

        const fileName = imgUrl.split('/').pop() || `未知文件名_${Date.now()}.jpg`;
        const savePath = `微博_${TARGET_UID}_第${pageNum}页/微博${weiboId}_图片${i + 1}_${fileName}`;

        try {
            // 【关键修改3:添加完整请求头,对抗防盗链和跨域】
            GM_download({
                url: imgUrl,
                name: savePath,
                description: `微博${weiboId}第${i + 1}张图`,
                // 核心:模拟浏览器请求头,通过微博服务器校验
                headers: {
                    "Referer": `https://weibo.com/${TARGET_UID}`, // 必须:请求来源为目标用户主页(非空且匹配微博域名)
                    "User-Agent": navigator.userAgent,            // 用当前浏览器的UA,避免被识别为脚本
                    "Accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
                    "Origin": "https://weibo.com",                // 跨域请求必需:声明请求来源域名
                    "Connection": "keep-alive"                    // 保持连接,避免请求中断
                },
                // 【关键修改4:指定下载方式为blob,避免XHR直接请求被拦截】
                method: "GET",
                responseType: "blob", // 强制以blob格式接收响应,绕过部分XHR限制
                onload: () => {
                    processedImages.add(imgUrl);
                    log(`下载成功:${savePath}`, 'success');
                },
                onerror: (error) => {
                    // 打印更详细的错误信息,便于后续排查
                    log(`下载失败:${fileName} | 原因:${error.error} | 状态码:${error.status || '无'} | URL:${imgUrl.slice(0, 50)}...`, 'error');
                },
                ontimeout: () => {
                    log(`下载超时:${fileName}(建议检查网络)`, 'error');
                }
            });

            log(`开始下载:微博${weiboId}第${i + 1}张图 | 路径:${savePath}`, 'download');
            await new Promise(resolve => setTimeout(resolve, DOWNLOAD_DELAY));
        } catch (e) {
            log(`下载异常:${fileName} | 详情:${e.message}`, 'error');
        }
    }
}
    // -------------------------- 核心处理逻辑 --------------------------
    /**
     * 处理单页微博数据:提取图片URL并触发下载
     * @param {Object} data - 微博页面数据(包含 data.list 数组,与提供数据结构匹配)
     * @param {string} requestUrl - 拦截到的请求URL(用于提取页码)
     */
    function processWeiboPage(data, requestUrl) {
        const pageNum = getPageNum(requestUrl);
        let totalImages = 0; // 统计本页总图片数

        try {
            // 验证数据结构:必须包含 data.list 数组(与提供的 "data.list" 结构匹配)
            if (!data?.data?.list || !Array.isArray(data.data.list)) {
                log(`第${pageNum}页:无有效微博列表(数据结构异常,缺少 data.list)`, 'error');
                return;
            }

            log(`第${pageNum}页:开始处理(共${data.data.list.length}条微博)`, 'intercept');

            // 遍历每条微博,提取图片并下载(异步处理,确保间隔生效)
            data.data.list.forEach(async (weibo) => {
                // 用微博ID作为唯一标识(优先用 idstr,无则用 id,避免ID格式问题)
                const weiboId = weibo.idstr || weibo.id || `未知ID_${Date.now()}`;
                // 提取当前微博的图片URL(调用适配嵌套结构的提取函数)
                const imageUrls = extractImageUrls(weibo);

                if (imageUrls.length > 0) {
                    totalImages += imageUrls.length;
                    // 触发图片下载(异步等待间隔)
                    await downloadImages(imageUrls, pageNum, weiboId);
                }
            });

            // 输出本页汇总信息(延迟执行,确保统计到所有微博的图片数)
            setTimeout(() => {
                log(`第${pageNum}页处理完成:共发现${totalImages}张图片(部分可能仍在下载中)`, 'success');
            }, totalImages * DOWNLOAD_DELAY);

        } catch (e) {
            log(`第${pageNum}页处理出错:${e.message}`, 'error');
        }
    }

    // -------------------------- 请求拦截逻辑 --------------------------
    /**
     * 拦截 fetch 请求(微博主要数据请求方式,适配新版微博)
     */
    const originalFetch = window.fetch;
    window.fetch = async function(url, options) {
        // 匹配规则:仅拦截目标用户的微博列表请求(包含 statuses/mymblog 且 UID 匹配)
        if (url.includes('statuses/mymblog') && url.includes(`uid=${TARGET_UID}`)) {
            const pageNum = getPageNum(url);
            // 避免重复处理同一页面(刷新、滚动重复加载时跳过)
            if (!processedPages.has(pageNum)) {
                processedPages.add(pageNum);
                // 截断URL日志(避免过长影响可读性)
                const shortUrl = url.length > 120 ? `${url.slice(0, 120)}...` : url;
                log(`拦截到fetch请求:第${pageNum}页 | URL: ${shortUrl}`, 'intercept');

                // 发起原始请求,克隆响应(避免原始响应被消耗后页面无法读取数据)
                const response = await originalFetch.apply(this, arguments);
                const responseClone = response.clone();

                // 解析JSON数据并处理(适配微博返回的JSON格式)
                try {
                    const data = await responseClone.json();
                    processWeiboPage(data, url);
                } catch (e) {
                    log(`第${pageNum}页fetch数据解析失败:${e.message}(可能是数据格式非JSON)`, 'error');
                }

                // 返回原始响应,确保页面正常加载(不影响用户浏览)
                return response;
            }
        }

        // 非目标请求,直接放行(不干扰页面其他功能)
        return originalFetch.apply(this, arguments);
    };

    /**
     * 拦截 XMLHttpRequest 请求(兼容微博旧版场景或部分异步请求)
     */
    const originalXhrOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url, async) {
        // 同样匹配目标用户的微博列表请求
        if (url.includes('statuses/mymblog') && url.includes(`uid=${TARGET_UID}`)) {
            // 绑定请求成功事件(load),在响应完成后处理数据
            this.addEventListener('load', () => {
                // 仅处理成功响应(状态码200-299),避免处理错误响应
                if (this.status >= 200 && this.status < 300) {
                    const pageNum = getPageNum(url);
                    if (!processedPages.has(pageNum)) {
                        processedPages.add(pageNum);
                        const shortUrl = url.length > 120 ? `${url.slice(0, 120)}...` : url;
                        log(`拦截到XHR请求:第${pageNum}页 | URL: ${shortUrl}`, 'intercept');

                        // 解析响应文本(微博返回的是JSON格式)
                        try {
                            const data = JSON.parse(this.responseText);
                            processWeiboPage(data, url);
                        } catch (e) {
                            log(`第${pageNum}页XHR数据解析失败:${e.message}(可能是响应文本损坏)`, 'error');
                        }
                    }
                }
            });
        }

        // 调用原始 open 方法,确保页面正常发起请求
        originalXhrOpen.call(this, method, url, async);
    };

    // -------------------------- 初始化提示 --------------------------
    
})();
// ==UserScript==
// @name 微博 statuses/show 单条微博缩略图自动下载器
// @namespace http://tampermonkey.net/
// @version 1.7
// @description 仅提取 statuses/show 接口返回数据中 pic_infos 的 thumbnail 图片 URL 并自动下载,按微博 ID 分类存储
// @author Your Name
// @match *://weibo.com/*
// @match *://www.weibo.com/*
// @match *://m.weibo.cn/*
// @grant GM_download
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-start
// ==/UserScript==

(function () {
    'use strict';

    //-------------------------- 核心配置 & 全局存储 --------------------------
    const TARGET_UID = '6409560260'; // 目标用户 UID
    const DOWNLOAD_DELAY = 2000; // 下载间隔(ms)
    const STORAGE_KEY = 'weibo_downloaded_thumbnails'; // 持久化存储键名(用于跨会话去重)

    // 1. 从 Tampermonkey 持久化存储中读取已下载记录(跨页面/重启浏览器不丢失)
    const persistedDownloaded = GM_getValue(STORAGE_KEY, []);
    // 2. 内存缓存(当前会话内快速去重)
    const processedPicIds = new Set(); // 按 pic_id 去重
    const processedImageUrls = new Set(persistedDownloaded); // 全局 URL 去重

    // 控制台日志样式
    const LOG_STYLES = {
        init: 'color: #9C27B0; font-weight: bold; background: #F3E5F5; padding: 2px 4px; border-radius: 2px',
        intercept: 'color: #1976D2; font-weight: bold; background: #E3F2FD; padding: 2px 4px; border-radius: 2px',
        download: 'color: #2E7D32; font-weight: bold; background: #E8F5E9; padding: 2px 4px; border-radius: 2px',
        success: 'color: #1B5E20; font-weight: bold; background: #C8E6C9; padding: 2px 4px; border-radius: 2px',
        skip: 'color: #FF8F00; font-weight: bold; background: #FFF3E0; padding: 2px 4px; border-radius: 2px',
        error: 'color: #C62828; font-weight: bold; background: #FFEBEE; padding: 2px 4px; border-radius: 2px'
    };

    //-------------------------- 工具函数 --------------------------
    /**
     * 带样式和时间戳的控制台日志
     * @param {string} msg - 日志内容
     * @param {string} type - 日志类型(init/intercept/download/success/skip/error)
     */
    function log(msg, type = 'init') {
        console.log(`%c[微博缩略图下载器] ${new Date().toLocaleTimeString()} | ${msg}`, LOG_STYLES[type]);
    }

    /**
     * 持久化存储已下载 URL(同步到 Tampermonkey 存储,跨会话生效)
     * @param {string} url - 已成功下载的图片 URL
     */
    function saveDownloadedUrl(url) {
        processedImageUrls.add(url);
        // 将 Set 转为数组存储(Tampermonkey 不支持直接存储 Set)
        GM_setValue(STORAGE_KEY, Array.from(processedImageUrls));
    }

    /**
     * 从 pic_infos 提取缩略图 URL(只保留 thumbnail 尺寸)
     * @param {Object} picInfos - 微博接口返回的 pic_infos 字段
     * @param {string} weiboId - 微博 ID(用于日志)
     * @returns {Array} 去重后的 thumbnail URL 数组
     */
    function extractThumbnailUrls(picInfos, weiboId) {
        const thumbnailUrls = [];
        if (!picInfos || typeof picInfos !== 'object' || Object.keys(picInfos).length === 0) {
            log(`微博${weiboId || '未知ID'}:无有效 pic_infos 字段`, 'error');
            return thumbnailUrls;
        }

        // 遍历每个 pic_id(单张图片的唯一标识)
        Object.entries(picInfos).forEach(([picId, picDetail]) => {
            // 只处理未处理过的 pic_id
            if (processedPicIds.has(picId)) {
                log(`微博${weiboId}:pic_id=${picId} 已处理,跳过`, 'skip');
                return;
            }

            // 只提取 thumbnail 尺寸的 URL
            if (picDetail.thumbnail && picDetail.thumbnail.url) {
                const thumbUrl = picDetail.thumbnail.url;
                // 简单处理 URL,确保是 thumbnail 版本
                const cleanUrl = thumbUrl.split('?')[0];
                thumbnailUrls.push({ url: cleanUrl, picId });
                processedPicIds.add(picId);
                log(`微博${weiboId}:提取到 thumbnail 图片 (pic_id=${picId})`, 'intercept');
            } else {
                log(`微博${weiboId}:pic_id=${picId} 无 thumbnail 尺寸`, 'error');
            }
        });

        log(`微博${weiboId}:共提取到 ${thumbnailUrls.length} 张 thumbnail 图片`, 'intercept');
        return thumbnailUrls;
    }

    /**
     * 下载 thumbnail 图片
     * @param {Array} thumbnailUrls - thumbnail 图片 URL 数组
     * @param {string} weiboId - 微博 ID(用于文件夹命名)
     * @param {string} screenName - 用户昵称(用于文件夹命名)
     */
    async function downloadThumbnails(thumbnailUrls, weiboId, screenName) {
        // 校验参数和 GM_download 可用性
        if (thumbnailUrls.length === 0) return;
        if (typeof GM_download === 'undefined') {
            log('下载初始化失败:GM_download API未加载(请检查Tampermonkey配置)', 'error');
            return;
        }

        // 遍历图片 URL,按间隔下载
        for (let i = 0; i < thumbnailUrls.length; i++) {
            const { url: imgUrl, picId } = thumbnailUrls[i];
            const imgIndex = i + 1; // 图片序号(从 1 开始)

            // 跳过已下载的图片
            if (processedImageUrls.has(imgUrl)) {
                log(`跳过已下载:微博${weiboId}_第${imgIndex}张(pic_id=${picId})`, 'skip');
                continue;
            }

            // 构建下载路径:用户昵称_微博 ID / thumbnail_序号_picId_文件名
            const fileName = imgUrl.split('/').pop() || `thumbnail_${Date.now()}.jpg`;
            const savePath = `${screenName}_微博${weiboId}/thumbnail_第${imgIndex}张_${picId}_${fileName}`;

            try {
                // 调用 GM_download 下载
                GM_download({
                    url: imgUrl,
                    name: savePath,
                    description: `微博${weiboId}第${imgIndex}张缩略图`,
                    // 防盗链请求头
                    headers: {
                        "Referer": `https://weibo.com/${TARGET_UID}`,
                        "User-Agent": navigator.userAgent,
                        "Accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
                        "Origin": "https://weibo.com",
                        "Connection": "keep-alive"
                    },
                    responseType: "blob",
                    // 下载成功回调
                    onload: () => {
                        saveDownloadedUrl(imgUrl);
                        log(`下载成功:${savePath}`, 'success');
                    },
                    // 下载失败回调
                    onerror: (error) => {
                        log(`下载失败:第${imgIndex}张 | pic_id=${picId} | 错误:${error.error} | 状态码:${error.status || '无'}`, 'error');
                    },
                    // 下载超时回调
                    ontimeout: () => {
                        log(`下载超时:第${imgIndex}张 | pic_id=${picId}(建议检查网络后重试)`, 'error');
                    }
                });

                log(`开始下载:微博${weiboId}第${imgIndex}张缩略图 | 保存路径:${savePath}`, 'download');
                // 等待下载间隔
                await new Promise(resolve => setTimeout(resolve, DOWNLOAD_DELAY));
            } catch (e) {
                log(`下载异常:第${imgIndex}张 | pic_id=${picId} | 详情:${e.message}`, 'error');
            }
        }
    }

    //-------------------------- 核心处理逻辑 --------------------------
    /**
     * 处理 statuses/show 接口返回的单条微博数据
     * @param {Object} responseData - 接口返回的完整数据
     * @param {string} requestUrl - 拦截到的请求 URL
     */
    function processWeiboData(responseData, requestUrl) {
        // 适配接口返回格式
        const weiboData = responseData.data || responseData;
        const { idstr: weiboId, user, ok, pic_infos } = weiboData;

        // 1. 校验数据合法性
        if (!ok || !weiboId || !user) {
            log(`数据非法:接口返回失败(ok = ${ok})或缺少关键字段`, 'error');
            return;
        }

        // 2. 校验是否为目标用户的微博
        const userUid = user.idstr || user.id;
        const screenName = user.screen_name || '未知用户';
        if (userUid !== TARGET_UID) {
            log(`跳过非目标用户微博:用户UID = ${userUid}(目标UID = ${TARGET_UID})`, 'intercept');
            return;
        }

        // 3. 提取 thumbnail 图片并下载
        log(`开始处理目标微博:${weiboId}(发布者:${screenName})`, 'intercept');
        const thumbnailUrls = extractThumbnailUrls(pic_infos, weiboId);
        if (thumbnailUrls.length > 0) {
            downloadThumbnails(thumbnailUrls, weiboId, screenName).catch(err => {
                log(`微博${weiboId}下载流程异常:${err.message}`, 'error');
            });
        } else {
            log(`微博${weiboId}(${screenName}):未提取到有效 thumbnail 图片`, 'intercept');
        }
    }

    //-------------------------- 请求拦截逻辑 --------------------------
    /**
     * 拦截 fetch 请求
     */
    const originalFetch = window.fetch;
    window.fetch = async function (url, options) {
        // 匹配 statuses/show 接口
        if (url.includes('statuses/show') && url.includes('id=')) {
            const shortUrl = url.length > 150 ? `${url.slice(0, 150)}...` : url;
            log(`拦截到fetch请求:statuses/show接口 | URL:${shortUrl}`, 'intercept');

            // 发起原始请求并克隆响应
            const response = await originalFetch.apply(this, arguments);
            const responseClone = response.clone();

            // 解析数据并处理
            try {
                const data = await responseClone.json();
                processWeiboData(data, url);
            } catch (e) {
                log(`fetch数据解析失败:${e.message}`, 'error');
            }

            return response;
        }
        // 非目标接口,直接放行
        return originalFetch.apply(this, arguments);
    };

    /**
     * 拦截 XMLHttpRequest 请求
     */
    const originalXhrOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function (method, url, async) {
        // 匹配 statuses/show 接口
        if (url.includes('statuses/show') && url.includes('id=')) {
            // 绑定请求成功事件
            this.addEventListener('load', () => {
                if (this.status >= 200 && this.status < 300) {
                    const shortUrl = url.length > 150 ? `${url.slice(0, 150)}...` : url;
                    log(`拦截到XHR请求:statuses/show接口 | URL:${shortUrl}`, 'intercept');

                    // 解析响应数据
                    try {
                        const data = JSON.parse(this.responseText);
                        processWeiboData(data, url);
                    } catch (e) {
                        log(`XHR数据解析失败:${e.message}`, 'error');
                    }
                }
            });
        }
        // 调用原始 open 方法
        originalXhrOpen.call(this, method, url, async);
    };

    //-------------------------- 初始化提示 --------------------------
    log('脚本已启动(仅下载 thumbnail 缩略图)', 'init');
    log(`目标用户:UID = ${TARGET_UID} | 下载间隔:${DOWNLOAD_DELAY}ms`, 'init');
    log('操作指引:1. 进入目标用户微博主页 2. 点击任意单条微博进入详情页 → 自动下载缩略图', 'init');
    log('下载路径:浏览器默认下载目录 → 【用户昵称_微博 ID】文件夹', 'init');
    log('注意事项:已下载的缩略图会被记录,不会重复下载', 'init');
})();

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值