// ==UserScript== // @name 图片爬虫|图片批量自动打包下载|网页图片批量下载器V2 // @namespace http://tampermonkey.net/ // @version 2.1 // @description 自动爬取网页图片并支持预览下载 // @author 白虎万岁 // @license MIT // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @connect * // @run-at document-end // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js // @downloadURL none // ==/UserScript== (function () { 'use strict'; // 全局变量声明 let imageUrls = new Set(); let isDragging = false; let currentX; let currentY; let initialX; let initialY; let xOffset = 0; let yOffset = 0; // DOM 元素声明 let status, modal, overlay, downloadBtn; // 添加初始化状态标记和重试机制 let initAttempts = 0; const MAX_INIT_ATTEMPTS = 10; const RETRY_INTERVAL = 500; // 初始化DOM元素 function createElements () { // 创建状态显示元素 status = document.createElement('div'); status.style.cssText = ` position: fixed; bottom: 80px; right: 20px; z-index: 2147483647; padding: 10px; background: rgba(0,0,0,0.7); color: white; border-radius: 4px; font-size: 14px; display: none; `; // 创建模态框 modal = document.createElement('div'); modal.className = 'image-preview-modal'; modal.innerHTML = ` `; // 创建遮罩层 overlay = document.createElement('div'); overlay.className = 'modal-overlay'; // 创建下载按钮 downloadBtn = document.createElement('div'); downloadBtn.className = 'image-downloader-btn'; downloadBtn.innerHTML = ` `; } // 添加样式 function addStyles () { const styleSheet = document.createElement('style'); styleSheet.textContent = ` .image-downloader-btn { position: fixed; top: 50%; /* 垂直居中 */ right: 0; /* 固定在右侧 */ z-index: 2147483647; width: 50px; height: 50px; border-radius: 8px 0 0 8px; /* 只设置左侧圆角 */ background: linear-gradient(145deg, #FF9800, #F57C00); color: white; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 12px rgba(255, 152, 0, 0.4); user-select: none; transition: all 0.3s; border: none; padding: 10px; transform: translateY(-50%); /* 垂直居中偏移 */ } .image-downloader-btn svg { width: 100%; height: 100%; transition: transform 0.3s; filter: drop-shadow(0 2px 3px rgba(0,0,0,0.2)); } .image-downloader-btn:hover { background: linear-gradient(145deg, #FFA726, #FB8C00); box-shadow: 0 6px 16px rgba(255, 152, 0, 0.6); } .image-downloader-btn:hover svg { transform: scale(1.1); } .image-preview-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 2147483646; width: 70vw; height: 70vh; background: white; border-radius: 16px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); display: none; flex-direction: column; overflow: hidden; } .modal-header { padding: 16px 24px; background: #fff; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; } .modal-title { font-size: 18px; font-weight: 600; color: #1976D2; } .modal-close { cursor: pointer; font-size: 24px; color: #666; transition: all 0.2s; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background: #f5f5f5; } .modal-close:hover { color: #333; background: #e0e0e0; transform: rotate(90deg); } .modal-content { flex: 1; padding: 20px; overflow-y: auto; display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 16px; background: #fff; } .modal-content::-webkit-scrollbar { width: 8px; } .modal-content::-webkit-scrollbar-track { background: #f5f5f5; border-radius: 4px; } .modal-content::-webkit-scrollbar-thumb { background: #ddd; border-radius: 4px; } .modal-content::-webkit-scrollbar-thumb:hover { background: #ccc; } .image-item { position: relative; padding-top: 100%; border: 2px solid transparent; border-radius: 12px; cursor: pointer; transition: all 0.2s; background: white; box-shadow: 0 2px 8px rgba(0,0,0,0.06); } .image-item::before { content: ''; position: absolute; top: 10px; right: 10px; width: 20px; height: 20px; border-radius: 50%; border: 2px solid #fff; background: transparent; z-index: 1; transition: all 0.2s; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .image-item.selected::before { background: #2196F3; border-color: #fff; } .image-item:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); } .image-item img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; border-radius: 10px; transition: all 0.2s; } .image-item.selected { border-color: #2196F3; box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2); } .modal-footer { padding: 16px 24px; border-top: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; background: white; } .modal-button { padding: 8px 20px; border: none; border-radius: 8px; cursor: pointer; background: #2196F3; color: white; font-size: 14px; font-weight: 500; transition: all 0.2s; min-width: 100px; text-align: center; } .modal-button:hover { background: #1976D2; transform: translateY(-1px); } .modal-button:disabled { background: #e0e0e0; cursor: not-allowed; transform: none; } .selected-count { color: #666; font-size: 14px; background: #f5f5f5; padding: 6px 12px; border-radius: 6px; } .modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(2px); z-index: 2147483645; display: none; } `; document.head.appendChild(styleSheet); } // 修改图片获取逻辑 function getPageImages () { const images = new Set(); // 1. 获取普通图片 document.querySelectorAll('img').forEach(img => { if (isValidImage(img)) { // 检查所有可能的图片源 const sources = [ img.src, img.dataset.src, img.dataset.original, img.getAttribute('data-original'), img.getAttribute('data-src'), img.getAttribute('data-actualsrc'), img.getAttribute('data-echo'), img.getAttribute('data-lazy'), img.getAttribute('data-url'), img.getAttribute('data-original-src') ]; sources.forEach(src => { if (src && isValidImageUrl(src)) { images.add(src); } }); } }); // 2. 获取背景图片 document.querySelectorAll('*').forEach(el => { try { const style = window.getComputedStyle(el); const bgImage = style.backgroundImage; if (bgImage && bgImage !== 'none') { const urls = bgImage.match(/url\(['"]?(.*?)['"]?\)/g); if (urls) { urls.forEach(url => { const cleanUrl = url.replace(/url\(['"]?(.*?)['"]?\)/, '$1'); if (isValidImageUrl(cleanUrl)) { images.add(cleanUrl); } }); } } } catch (e) { } }); // 3. 获取 picture 元素中的图片 document.querySelectorAll('picture source').forEach(source => { const srcset = source.srcset; if (srcset) { srcset.split(',').forEach(src => { const url = src.trim().split(' ')[0]; if (isValidImageUrl(url)) { images.add(url); } }); } }); return Array.from(images); } // 优化图片验证函数 function isValidImage (img) { if (!img) return false; // 检查图片是否加载 if (img.complete) { return img.naturalWidth > 0 || img.naturalHeight > 0; } // 如果图片未加载,检查显示尺寸 const rect = img.getBoundingClientRect(); return rect.width > 0 || rect.height > 0; } function isValidImageUrl (url) { if (!url || typeof url !== 'string') return false; try { // 如果是相对路径,转换为绝对路径 if (url.startsWith('/')) { url = location.origin + url; } else if (url.startsWith('./') || url.startsWith('../')) { url = new URL(url, location.href).href; } // 清理URL url = url.split('?')[0].split('#')[0].toLowerCase(); // 检查文件扩展名 const extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'ico']; const ext = getImageExtension(url); return extensions.includes(ext); } catch { return false; } } // 获取图片扩展名 function getImageExtension (url) { const match = url.match(/\.([^\.]+)(?:[\?#]|$)/); return match ? match[1].toLowerCase() : 'jpg'; } // 显示预览窗口 function showPreview () { const images = Array.from(imageUrls); const content = modal.querySelector('.modal-content'); // 保存已选中的图片 const selectedUrls = new Set( Array.from(content.querySelectorAll('.image-item.selected img')) .map(img => img.src) ); content.innerHTML = ''; images.forEach((url, index) => { const item = document.createElement('div'); item.className = 'image-item'; if (selectedUrls.has(url)) { item.classList.add('selected'); } item.innerHTML = ``; item.addEventListener('click', () => { item.classList.toggle('selected'); updateSelectedCount(); }); content.appendChild(item); }); modal.style.display = 'flex'; overlay.style.display = 'block'; updateSelectedCount(); } // 更新选中数量 function updateSelectedCount () { const selectedCount = modal.querySelectorAll('.image-item.selected').length; modal.querySelector('.selected-count').textContent = `已选择: ${selectedCount}`; modal.querySelector('.download-selected').disabled = selectedCount === 0; } // 设置事件监听 function setupEventListeners () { // 只保留点击事件 downloadBtn.addEventListener('click', showPreview); // 关闭按钮事件 modal.querySelector('.modal-close').addEventListener('click', () => { modal.style.display = 'none'; overlay.style.display = 'none'; }); // 全选按钮事件 modal.querySelector('.select-all').addEventListener('click', function () { const items = modal.querySelectorAll('.image-item'); const allSelected = Array.from(items).every(item => item.classList.contains('selected')); items.forEach(item => { if (allSelected) { item.classList.remove('selected'); } else { item.classList.add('selected'); } }); this.textContent = allSelected ? '全选' : '取消全选'; updateSelectedCount(); }); // 下载选中图片事件 modal.querySelector('.download-selected').addEventListener('click', async () => { const selectedItems = modal.querySelectorAll('.image-item.selected img'); if (selectedItems.length === 0) return; try { showStatus(`准备下载 ${selectedItems.length} 张图片...`); const images = Array.from(selectedItems).map(img => ({ src: img.src, index: img.dataset.index })); const pageTitle = document.title.replace(/[\\/:*?"<>|]/g, '_'); const date = new Date().toISOString().split('T')[0]; const zipName = `${pageTitle}_${date}`; const zip = new JSZip(); let processedCount = 0; let failedCount = 0; for (const img of images) { try { showStatus(`正在下载第 ${processedCount + 1}/${images.length} 张图片...`); const blob = await downloadImage(img.src); const fileName = `image_${img.index}.${getImageExtension(img.src)}`; zip.file(fileName, blob); processedCount++; } catch (error) { console.error('下载图片失败:', img.src, error); failedCount++; } } if (processedCount === 0) { throw new Error('没有成功下载任何图片'); } if (failedCount > 0) { showStatus(`有 ${failedCount} 张图片下载失败,正在打包其他图片...`); } else { showStatus('正在生成压缩包...'); } const content = await zip.generateAsync({ type: 'blob' }); const downloadUrl = URL.createObjectURL(content); const link = document.createElement('a'); link.href = downloadUrl; link.download = `${zipName}.zip`; link.click(); URL.revokeObjectURL(downloadUrl); modal.style.display = 'none'; overlay.style.display = 'none'; if (failedCount > 0) { showStatus(`下载完成,但有 ${failedCount} 张图片下载失败`); } else { showStatus('下载完成!'); } setTimeout(hideStatus, 3000); } catch (error) { console.error('下载过程出错:', error); showStatus('下载失败: ' + error.message); setTimeout(hideStatus, 3000); } }); } // 状态显示函数 function showStatus (message) { status.textContent = message; status.style.display = 'block'; } function hideStatus () { status.style.display = 'none'; } // 修改下载图片的逻辑 async function downloadImage (url, retryCount = 0) { const MAX_RETRIES = 3; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'blob', headers: { 'Referer': location.href, 'User-Agent': navigator.userAgent }, timeout: 30000, // 30秒超时 onload: function (response) { if (response.status === 200) { resolve(response.response); } else if (retryCount < MAX_RETRIES) { // 如果失败但未达到最大重试次数,则重试 setTimeout(() => { downloadImage(url, retryCount + 1) .then(resolve) .catch(reject); }, 1000 * (retryCount + 1)); // 递增延迟 } else { reject(new Error(`下载失败: ${response.status}`)); } }, onerror: function (error) { if (retryCount < MAX_RETRIES) { // 如果失败但未达到最大重试次数,则重试 setTimeout(() => { downloadImage(url, retryCount + 1) .then(resolve) .catch(reject); }, 1000 * (retryCount + 1)); } else { reject(new Error('下载失败: ' + error.message)); } }, ontimeout: function () { if (retryCount < MAX_RETRIES) { setTimeout(() => { downloadImage(url, retryCount + 1) .then(resolve) .catch(reject); }, 1000 * (retryCount + 1)); } else { reject(new Error('下载超时')); } } }); }); } // 初始化函数 function init () { try { createElements(); addStyles(); document.body.appendChild(status); document.body.appendChild(downloadBtn); document.body.appendChild(modal); document.body.appendChild(overlay); setupEventListeners(); // 初始获取页面图片 const newImages = getPageImages(); imageUrls = new Set(newImages); console.log('图片下载器初始化成功'); } catch (error) { console.error('初始化失败:', error); } } // 根据浏览器类型调整初始化时机 if (navigator.userAgent.includes('Edg/')) { // Edge浏览器特殊处理 window.addEventListener('load', () => { setTimeout(init, 500); }); } else { // 其他浏览器正常处理 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } } })();