// ==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
// ==/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 = `
<div class="modal-header">
<span class="modal-title">图片预览</span>
<span class="modal-close">×</span>
</div>
<div class="modal-content"></div>
<div class="modal-footer">
<button class="modal-button select-all">全选</button>
<span class="selected-count">已选择: 0</span>
<button class="modal-button download-selected" disabled>下载选中</button>
</div>
`;
// 创建遮罩层
overlay = document.createElement('div');
overlay.className = 'modal-overlay';
// 创建下载按钮
downloadBtn = document.createElement('div');
downloadBtn.className = 'image-downloader-btn';
downloadBtn.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="white">
<path d="M512 1015.125333c-129.024 0-250.88-31.402667-343.04-88.746666-95.573333-59.392-148.138667-139.605333-148.138667-225.621334 0-28.672 5.802667-57.002667 17.408-83.968 17.066667-39.936 25.258667-79.872 24.576-118.101333v-9.216c0-118.442667 45.738667-230.058667 128.341334-314.368 82.602667-84.309333 193.194667-132.096 311.296-134.485333 245.077333-5.12 450.56 190.122667 458.069333 434.858666 0.341333 7.509333 0.341333 15.018667 0 22.528-0.682667 41.301333 6.826667 79.530667 22.528 114.005334 12.970667 28.672 19.797333 58.709333 19.797333 89.088 0 86.016-52.565333 166.229333-148.138666 225.621333-91.818667 57.002667-213.674667 88.405333-342.698667 88.405333z"/>
<path d="M132.437333 354.986667l-24.234666-19.456C64.512 300.373333 39.594667 248.149333 39.594667 192.512c0-101.034667 82.261333-183.637333 183.637333-183.637333 57.685333 0 110.933333 26.282667 146.090667 72.362666l18.773333 24.576-28.672 11.946667C263.168 157.354667 187.392 231.424 145.066667 326.656l-12.629334 28.330667z"/>
<path d="M891.221333 354.986667l-12.629333-28.330667c-41.642667-93.525333-119.808-169.301333-214.357333-208.554667l-28.672-11.946666 18.773333-24.576C689.493333 35.498667 742.741333 8.874667 800.768 8.874667c101.034667 0 183.637333 82.261333 183.637333 183.637333 0 55.978667-25.258667 108.202667-68.949333 143.36l-24.234667 19.114667z"/>
<path d="M479.232 64.170667h47.786667V368.64h-47.786667z"/>
<path d="M603.477333 195.584c-55.296-55.296-145.066667-55.296-200.362666 0l-33.792-33.792c73.728-73.728 194.218667-73.728 267.946666 0l-33.792 33.792z"/>
<path d="M583.68 293.546667c-47.786667-34.133333-112.64-35.157333-161.450667-2.730667L395.946667 250.88c65.194667-43.349333 151.893333-41.984 215.722666 4.096L583.68 293.546667z"/>
<path d="M564.906667 403.456c-38.912-20.821333-84.309333-22.186667-124.586667-4.437333l-19.456-43.690667c53.589333-23.893333 114.346667-21.845333 166.570667 5.802667l-22.528 42.325333z"/>
<path d="M503.125333 678.570667c-20.48 0-38.912-10.581333-49.152-28.330667l-17.066666-29.696c-10.24-17.749333-10.24-38.912 0-56.661333 10.24-17.749333 28.672-28.330667 49.152-28.330667h34.474666c20.48 0 38.912 10.581333 49.152 28.330667 10.24 17.749333 10.24 38.912 0 56.661333l-17.066666 29.696c-10.581333 17.749333-29.013333 28.330667-49.493334 28.330667z"/>
<path d="M365.909333 808.96c-42.325333 0-82.261333-15.701333-112.64-44.714667l32.768-34.816c21.504 20.138667 49.834667 31.402667 79.872 31.402667 62.464 0 113.322667-48.810667 113.322667-108.544h47.786667c0 86.357333-72.021333 156.672-161.109334 156.672z"/>
<path d="M640.341333 808.96c-88.746667 0-161.109333-69.973333-161.109333-156.330667h47.786667c0 59.733333 50.858667 108.544 113.322666 108.544 30.037333 0 58.368-11.264 79.872-31.402666l32.768 34.816c-30.037333 28.330667-69.973333 44.373333-112.64 44.373333z"/>
</svg>
`;
}
// 添加样式
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 = `<img src="${url}" data-index="${index}">`;
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();
}
}
})();