// ==UserScript==
// @name 图片爬虫|图片批量自动打包下载|网页图片批量下载器V2
// @namespace http://tampermonkey.net/
// @version 1.0
// @description 自动爬取网页图片并支持预览下载
// @author 白虎万岁
// @license MIT
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @run-at document-start
// @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;
// 初始化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;
bottom: 100px;
right: 30px;
z-index: 2147483647;
width: 50px;
height: 50px;
border-radius: 50%;
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;
}
.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 >= 100 || img.naturalHeight >= 100;
}
// 如果图片未加载,检查显示尺寸
const rect = img.getBoundingClientRect();
return rect.width >= 100 || rect.height >= 100;
}
function isValidImageUrl (url) {
if (!url || typeof url !== 'string') return false;
try {
const urlObj = new URL(url);
// 如果是相对路径,转换为绝对路径
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'];
const ext = getImageExtension(url);
return extensions.includes(ext);
} catch {
return false;
}
}
// 显示预览窗口
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('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
downloadBtn.addEventListener('click', () => {
if (!isDragging) {
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 dragStart (e) {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
if (e.target === downloadBtn || downloadBtn.contains(e.target)) {
isDragging = true;
}
}
function drag (e) {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
setTranslate(currentX, currentY, downloadBtn);
saveButtonPosition();
}
}
function dragEnd () {
initialX = currentX;
initialY = currentY;
isDragging = false;
}
function setTranslate (xPos, yPos, el) {
el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0)`;
}
// 保存和恢复按钮位置
function saveButtonPosition () {
GM_setValue('buttonX', xOffset);
GM_setValue('buttonY', yOffset);
}
function restoreButtonPosition () {
const savedX = GM_getValue('buttonX', 0);
const savedY = GM_getValue('buttonY', 0);
if (savedX !== 0 || savedY !== 0) {
xOffset = savedX;
yOffset = savedY;
setTranslate(xOffset, yOffset, downloadBtn);
}
}
// 状态显示函数
function showStatus (message) {
status.textContent = message;
status.style.display = 'block';
}
function hideStatus () {
status.style.display = 'none';
}
// 获取图片扩展名
function getImageExtension (url) {
const match = url.match(/\.([^\.]+)(?:[\?#]|$)/);
return match ? match[1].toLowerCase() : 'jpg';
}
// 监听页面变化
function setupPageChangeListener () {
// 监听页面滚动
let scrollTimer;
window.addEventListener('scroll', () => {
clearTimeout(scrollTimer);
scrollTimer = setTimeout(() => {
const newImages = getPageImages();
addNewImages(newImages);
}, 300);
});
// 监听 URL 变化
let lastUrl = location.href;
const urlObserver = setInterval(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
setTimeout(() => {
const newImages = getPageImages();
addNewImages(newImages);
}, 1000);
}
}, 500);
// 监听 DOM 变化
const observer = new MutationObserver((mutations) => {
let shouldUpdate = false;
for (const mutation of mutations) {
if (mutation.type === 'childList' ||
(mutation.type === 'attributes' &&
['src', 'data-src', 'data-original', 'style'].includes(mutation.attributeName))) {
shouldUpdate = true;
break;
}
}
if (shouldUpdate) {
clearTimeout(window.updateTimer);
window.updateTimer = setTimeout(() => {
const newImages = getPageImages();
addNewImages(newImages);
}, 500);
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['src', 'data-src', 'data-original', 'style']
});
// 监听动态加载的内容
const intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setTimeout(() => {
const newImages = getPageImages();
addNewImages(newImages);
}, 500);
}
});
});
// 观察可能包含图片的容器
document.querySelectorAll('.content, .container, main, article, [class*="content"], [class*="list"]')
.forEach(el => intersectionObserver.observe(el));
}
// 添加新图片
function addNewImages (newImages) {
let hasNewImages = false;
newImages.forEach(url => {
if (!imageUrls.has(url)) {
imageUrls.add(url);
hasNewImages = true;
}
});
if (hasNewImages) {
updatePreviewIfVisible();
}
}
// 更新预览窗口(如果当前可见)
function updatePreviewIfVisible () {
if (modal.style.display === 'flex') {
showPreview();
}
}
// 初始化
function init () {
try {
createElements();
addStyles();
document.body.appendChild(status);
document.body.appendChild(downloadBtn);
document.body.appendChild(modal);
document.body.appendChild(overlay);
restoreButtonPosition();
setupEventListeners();
setupPageChangeListener();
console.log('图片下载器初始化成功');
} catch (error) {
console.error('初始化失败:', error);
}
}
// 确保在页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => setTimeout(init, 0));
} else {
setTimeout(init, 0);
}
// 添加错误处理和重试机制
window.addEventListener('load', () => {
setTimeout(() => {
if (!document.querySelector('.image-downloader-btn')) {
console.log('重试初始化...');
init();
}
}, 1000);
});
// 修改下载图片的逻辑
async function downloadImage (url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
responseType: 'blob',
headers: {
'Referer': location.href
},
onload: function (response) {
if (response.status === 200) {
resolve(response.response);
} else {
reject(new Error(`下载失败: ${response.status}`));
}
},
onerror: function (error) {
reject(new Error('下载失败: ' + error.message));
}
});
});
}
})();