您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
一键批量下载微信公众号文章中的摘要、封面、图片、视频和音频文件,适用普通长文和小绿书
// ==UserScript== // @name 2025最新_微信公众号媒体文件批量下载器(公众号:掌心向暖) // @namespace http://tampermonkey.net/ // @version 1.7 // @description 一键批量下载微信公众号文章中的摘要、封面、图片、视频和音频文件,适用普通长文和小绿书 // @author You // @match https://mp.weixin.qq.com/s/* // @grant none // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js // ==/UserScript== (function() { 'use strict'; // 字符串违规字符替换函数 function sanitizeFilename(input) { if (!input) { return ""; } // 定义字符替换映射表 const charMapping = { '\\': '\', // 反斜杠替换为全角反斜杠 '/': '/', // 斜杠替换为全角斜杠 ':': ':', // 冒号替换为全角冒号 '*': '*', // 星号替换为全角星号 '?': '?', // 问号替换为全角问号 '"': '"', // 双引号替换为全角双引号 '<': '<', // 小于号替换为全角小于号 '>': '>', // 大于号替换为全角大于号 '|': '|' // 竖线替换为全角竖线 }; // 执行字符替换 let result = input; for (const [illegalChar, legalChar] of Object.entries(charMapping)) { result = result.replace(new RegExp('\\' + illegalChar, 'g'), legalChar); } // 移除制表符和换行符(保留空格) result = result.replace(/\t/g, ''); // 移除制表符 result = result.replace(/\n/g, ''); // 移除换行符 result = result.replace(/\r/g, ''); // 移除回车符 return result; } // 等待页面加载完成 window.addEventListener('load', function() { init(); }); function init() { // 创建按钮容器 createButtons(); } // 创建扫描媒体和一键下载按钮 function createButtons() { // 创建按钮容器 - 固定在页面右侧 const buttonContainer = document.createElement('div'); buttonContainer.id = 'mediaDownloadContainer'; buttonContainer.style.cssText = ` position: fixed; top: 50%; right: 20px; transform: translateY(-50%); z-index: 9999; background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); padding: 15px; width: 150px; text-align: center; `; // 扫描媒体按钮 const scanButton = document.createElement('button'); scanButton.textContent = '扫描媒体'; scanButton.style.cssText = ` width: 100%; margin: 8px 0; padding: 10px 15px; background: #1aad19; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; transition: background-color 0.3s; `; scanButton.onclick = scanMedia; // 添加悬停效果 scanButton.onmouseover = function() { this.style.background = '#16941a'; }; scanButton.onmouseout = function() { this.style.background = '#1aad19'; }; // 一键下载按钮 const downloadButton = document.createElement('button'); downloadButton.textContent = '一键下载'; downloadButton.style.cssText = ` width: 100%; margin: 8px 0; padding: 10px 15px; background: #576b95; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; transition: background-color 0.3s; `; downloadButton.onclick = downloadAllMedia; // 添加悬停效果 downloadButton.onmouseover = function() { this.style.background = '#4a5a82'; }; downloadButton.onmouseout = function() { this.style.background = '#576b95'; }; // 进度显示区域 const progressDiv = document.createElement('div'); progressDiv.id = 'downloadProgress'; progressDiv.style.cssText = ` margin: 10px 0; padding: 8px; font-size: 12px; color: #666; background: #f8f8f8; border-radius: 4px; line-height: 1.4; word-wrap: break-word; display: none; `; buttonContainer.appendChild(scanButton); buttonContainer.appendChild(downloadButton); buttonContainer.appendChild(progressDiv); // 将按钮容器添加到页面 document.body.appendChild(buttonContainer); console.log('媒体下载器按钮已创建在页面右侧'); } // 扫描媒体文件 function scanMedia() { const summary = getSummary(); const cover = getCoverImage(); const images = getImages(); const videos = getVideos(); const audios = getAudios(); const progressDiv = document.getElementById('downloadProgress'); progressDiv.style.display = 'block'; progressDiv.innerHTML = ` <div><strong>扫描结果:</strong></div> <div>摘要:${summary ? '1 个' : '0 个'}</div> <div>封面:${cover.length} 张</div> <div>图片:${images.length} 张</div> <div>视频:${videos.length} 个</div> <div>音频:${audios.length} 个</div> `; console.log('扫描结果:', { summary, cover, images, videos, audios }); // 3秒后自动隐藏扫描结果 setTimeout(() => { progressDiv.style.display = 'none'; }, 3000); } // 获取所有图片(修改后的逻辑,兼容两种类型的文章) function getImages() { const images = []; const imageUrls = []; // 用于去重 // 方式1:针对包含视频类型的文章 - 从js_content区域获取图片 const jsContent = document.getElementById('js_content'); if (jsContent) { const imgElements = jsContent.getElementsByTagName('img'); for (let i = 0; i < imgElements.length; i++) { const img = imgElements[i]; // 过滤条件 if (img.getAttribute('data-w') === '64') continue; if (img.closest('.swiper_indicator_wrp_pc')) continue; let imgUrl = ''; // 优先使用dataset.src,然后使用src if (img.dataset.src) { imgUrl = img.dataset.src; } else if (img.src && !img.src.startsWith('data:')) { imgUrl = img.src; // 处理微信资源链接 imgUrl = imgUrl.replace("//res.wx.qq.com/mmbizwap", "http://res.wx.qq.com/mmbizwap"); } // 去重检查 if (!imgUrl || imageUrls.includes(imgUrl)) continue; imageUrls.push(imgUrl); // 根据URL判断图片格式 let extension = '.jpg'; // 默认jpg if (imgUrl.indexOf('wx_fmt=gif') > 0 || imgUrl.indexOf('mmbiz_gif') > 0) { extension = '.gif'; } else if (imgUrl.indexOf('wx_fmt=png') > 0 || imgUrl.indexOf('mmbiz_png') > 0) { extension = '.png'; } else if (imgUrl.indexOf('wx_fmt=bmp') > 0 || imgUrl.indexOf('mmbiz_bmp') > 0) { extension = '.bmp'; } else if (imgUrl.indexOf('wx_fmt=webp') > 0 || imgUrl.indexOf('mmbiz_webp') > 0) { extension = '.webp'; } images.push({ url: imgUrl, filename: `image_${images.length + 1}${extension}` }); } // 如果从js_content找到了图片,直接返回 if (images.length > 0) { return images; } } // 方式2:针对小绿书类型的文章 - 查找 .swiper_item_img 下的图片 const swiperImages = document.querySelectorAll('.swiper_item_img img'); if (swiperImages.length > 0) { swiperImages.forEach(img => { if (img.src && !imageUrls.includes(img.src)) { imageUrls.push(img.src); // 获取图片扩展名 let extension = getImageExtension(img.src); images.push({ url: img.src, filename: `image_${images.length + 1}.${extension}` }); } }); // 如果从swiper找到了图片,直接返回 if (images.length > 0) { return images; } } // 方式3:通用方法 - 如果以上两种方式都没找到图片 const allImgElements = document.querySelectorAll('img'); allImgElements.forEach((img) => { // 过滤条件 if (img.getAttribute('data-w') === '64') return; if (img.closest('.swiper_indicator_wrp_pc')) return; if (!img.src || img.src.startsWith('data:')) return; // 去重检查 if (imageUrls.includes(img.src)) return; imageUrls.push(img.src); // 获取图片后缀 let extension = '.png'; // 默认png const srcUrl = img.src; if (srcUrl.includes('.jpg') || srcUrl.includes('.jpeg')) { extension = '.jpg'; } else if (srcUrl.includes('.gif')) { extension = '.gif'; } else if (srcUrl.includes('.webp')) { extension = '.webp'; } images.push({ url: srcUrl, filename: `image_${images.length + 1}${extension}` }); }); return images; } // 获取图片扩展名(从参考脚本中提取的函数) function getImageExtension(url) { const match = url.match(/\.([a-zA-Z0-9]+)(?:\?|$)/); if (match) { return match[1].toLowerCase(); } // 检查微信图片URL中的格式参数 if (url.includes('wx_fmt=')) { const formatMatch = url.match(/wx_fmt=([a-zA-Z0-9]+)/); if (formatMatch) { return formatMatch[1].toLowerCase(); } } return 'jpg'; // 默认扩展名 } // 获取所有视频 function getVideos() { const videos = []; const videoElements = document.querySelectorAll('video'); videoElements.forEach((video, index) => { let videoUrl = video.src; // 如果video标签没有src,查找source标签 if (!videoUrl) { const source = video.querySelector('source'); if (source) { videoUrl = source.src; } } // 只处理微信视频链接 if (videoUrl && videoUrl.includes('mpvideo.qpic.cn')) { videos.push({ url: videoUrl, filename: `video_${index + 1}.mp4` }); } }); return videos; } // 获取所有音频(修改后的逻辑) function getAudios() { const audios = []; const audioUrls = []; // 用于去重 // 方法1:查找具有voice_encode_fileid属性的元素 const voiceElements = document.querySelectorAll('[voice_encode_fileid]'); voiceElements.forEach((element, index) => { const voiceId = element.getAttribute('voice_encode_fileid'); if (voiceId) { const audioUrl = `https://res.wx.qq.com/voice/getvoice?mediaid=${voiceId}`; // 去重检查 if (!audioUrls.includes(audioUrl)) { audioUrls.push(audioUrl); audios.push({ url: audioUrl, filename: `audio_${audios.length + 1}.mp3` }); } } }); // 方法2:查找audio标签中的微信语音链接(作为备用方法) const audioElements = document.querySelectorAll('audio'); audioElements.forEach((audio, index) => { if (audio.src && audio.src.includes('res.wx.qq.com/voice')) { // 去重检查 if (!audioUrls.includes(audio.src)) { audioUrls.push(audio.src); audios.push({ url: audio.src, filename: `audio_${audios.length + 1}.mp3` }); } } }); console.log('音频识别结果:', audios); return audios; } // 获取文章封面 function getCoverImage() { const coverMeta = document.querySelector('meta[property="twitter:image"]'); if (coverMeta && coverMeta.content) { const coverUrl = coverMeta.content; // 使用我们已有的函数来判断图片扩展名 const extension = getImageExtension(coverUrl); return [{ url: coverUrl, // 命名为 封面,使其在文件中排序靠前 filename: `封面.${extension}` }]; } return []; // 如果没找到,返回空数组 } // 获取文章摘要 function getSummary() { const summaryMeta = document.querySelector('meta[name="description"]'); if (summaryMeta && summaryMeta.content) { return summaryMeta.content; } return null; // 如果没找到,返回null } // 下载单个文件 async function downloadFile(url, filename) { try { // 尝试直接fetch let response = await fetch(url); // 如果跨域失败,尝试通过代理或其他方式 if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const blob = await response.blob(); return { filename, blob, success: true }; } catch (error) { console.error(`下载失败: ${filename}`, error); // 尝试使用代理方式下载 try { const proxyUrl = `https://cors-anywhere.herokuapp.com/${url}`; const response = await fetch(proxyUrl); const blob = await response.blob(); return { filename, blob, success: true }; } catch (proxyError) { console.error(`代理下载也失败: ${filename}`, proxyError); return { filename, success: false, error: error.message }; } } } // 批量下载所有媒体文件 async function downloadAllMedia() { const progressDiv = document.getElementById('downloadProgress'); progressDiv.style.display = 'block'; progressDiv.innerHTML = '正在准备下载...'; // 1. 获取所有媒体 const cover = getCoverImage(); const images = getImages(); const videos = getVideos(); const audios = getAudios(); // 2. 合并并去重媒体文件 const combinedFiles = [...cover, ...images, ...videos, ...audios]; const uniqueFilesMap = new Map(); combinedFiles.forEach(file => { if (!uniqueFilesMap.has(file.url)) { uniqueFilesMap.set(file.url, file); } }); const allFiles = Array.from(uniqueFilesMap.values()); if (allFiles.length === 0) { progressDiv.innerHTML = '未找到可下载的媒体文件'; setTimeout(() => { progressDiv.style.display = 'none'; }, 3000); return; } // 创建JSZip实例 const zip = new JSZip(); // 新增:获取摘要并直接添加到压缩包 const summaryText = getSummary(); if (summaryText) { zip.file("摘要.txt", summaryText); } let downloadedCount = 0; let failedCount = 0; progressDiv.innerHTML = `开始下载 ${allFiles.length} 个媒体文件...`; // 并发下载文件 const downloadPromises = allFiles.map(async (file, index) => { const result = await downloadFile(file.url, file.filename); if (result.success) { zip.file(result.filename, result.blob); downloadedCount++; } else { failedCount++; console.error(`文件下载失败: ${file.filename}`); } // 更新进度 progressDiv.innerHTML = ` 下载进度: ${downloadedCount + failedCount}/${allFiles.length}<br> 成功: ${downloadedCount} 个<br> 失败: ${failedCount} 个 `; return result; }); // 等待所有下载完成 await Promise.all(downloadPromises); if (downloadedCount === 0) { progressDiv.innerHTML = '所有媒体文件下载失败,请检查网络连接'; setTimeout(() => { progressDiv.style.display = 'none'; }, 5000); return; } // 生成压缩包 progressDiv.innerHTML = '正在生成压缩包...'; try { const zipContent = await zip.generateAsync({ type: 'blob' }); // 获取页面标题并应用字符替换规则作为压缩包名称 const title = sanitizeFilename(document.title); const zipFilename = `${title}.zip`; // 下载压缩包 const link = document.createElement('a'); link.href = URL.createObjectURL(zipContent); link.download = zipFilename; document.body.appendChild(link); link.click(); document.body.removeChild(link); progressDiv.innerHTML = ` <div><strong>下载完成!</strong></div> <div>成功: ${downloadedCount} 个媒体</div> ${failedCount > 0 ? `<div>失败: ${failedCount} 个</div>` : ''} ${summaryText ? `<div>摘要: 1 个</div>` : ''} <div>压缩包: ${zipFilename}</div> `; // 5秒后自动隐藏完成信息 setTimeout(() => { progressDiv.style.display = 'none'; }, 5000); } catch (error) { console.error('生成压缩包失败:', error); progressDiv.innerHTML = '生成压缩包失败,请重试'; setTimeout(() => { progressDiv.style.display = 'none'; }, 3000); } } })();