您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
下载微博(weibo.com)的图片和视频。(支持LivePhoto、短视频、动/静图,可以打包下载)
当前为
// ==UserScript== // @name 微博 [ 图片 | 视频 ] 下载 // @namespace http://tampermonkey.net/ // @version 1.1 // @description 下载微博(weibo.com)的图片和视频。(支持LivePhoto、短视频、动/静图,可以打包下载) // @author Mr.Po // @match https://weibo.com/* // @match https://www.weibo.com/* // @match https://d.weibo.com/* // @match https://s.weibo.com/* // @require https://code.jquery.com/jquery-1.11.0.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.2.0/jszip.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js // @resource iconError https://raw.githubusercontent.com/Mr-Po/weibo-resource-download/master/media/error.png // @resource iconSuccess https://raw.githubusercontent.com/Mr-Po/weibo-resource-download/master/media/success.png // @resource iconInfo https://raw.githubusercontent.com/Mr-Po/weibo-resource-download/master/media/info.png // @resource iconExtract https://raw.githubusercontent.com/Mr-Po/weibo-resource-download/master/media/extract.png // @resource iconZip https://raw.githubusercontent.com/Mr-Po/weibo-resource-download/master/media/zip.png // @connect sinaimg.cn // @connect miaopai.com // @connect youku.com // @connect weibo.com // @grant GM_notification // @grant GM_setClipboard // @grant GM_download // @grant GM_xmlhttpRequest // @grant GM_getResourceURL // ==/UserScript== (function() { 'use strict'; // TODO 直播回放 /** * 资源命名策略 * * 0:资源原始名称(如:0065x5rwly1g3c6exw0a2j30u012utyg.jpg) * 1:微博用户名-微博ID-序号(如:小米商城-4375413591293810-01.jpg)[缺省] * 2:微博用户ID-微博ID-序号(如:5578564422-4375413591293810-01.jpg) * * @type 整数 */ var resourceNamingStrategy = 1; /** * 打包命名策略 * * 1:微博用户名-微博ID(如:小米商城-4375413591293810.zip)[缺省] * 2:微博用户ID-微博ID(如:5578564422-4375413591293810.zip) * * @type 整数 */ var zipNamingStrategy = 1; /** * 命名连接符 * 即:“微博用户名-微博ID-序号”,中的短横线“-” * @type {String} */ var nameingSeparator = "-"; /** * 最大等待请求时间(超时时间),单位:毫秒 * 若经常请求超时,可适当增大此值 * * @type {Number} */ var maxRequestTime = 8000; /** * 每隔 space 毫秒检查一次,是否有新的微博被加载出来 * 此值越小,检查越快;过小会造成浏览器卡顿 * @type {Number} */ var space = 5000; /** * 是否启用调试模式 * 启用后,浏览器控制台会显示此脚本运行时的调试数据 * @type {Boolean} */ var isDebug = false; var handledWeiBoCardClass = "weibo_383402_extend"; /** * 搜索微博解析器 * * @type {Object} */ var searchWeiBoResolver = { getOperationList: function() { return $("div .menu ul:not([class='" + handledWeiBoCardClass + "'])"); }, getPhoto: function($ul) { return $ul.parents(".card-wrap").find(".media.media-piclist img"); }, getLivePhotoContainer: function($ul) { return $(null); }, getWeiBoId: function($ul) { var mid = $ul.parents(".card-wrap").attr("mid").trim(); return mid; }, getWeiBoUserId: function($ul) { var $a = $ul.parents(".card-wrap").find("a.name").first(); var id = $a.attr("href").match(/weibo\.com\/(\d+)/)[1].trim(); if (isDebug) { console.log("得到的微博ID为:" + id); } return id; }, getWeiBoUserName: function($ul) { var name = $ul.parents(".card-wrap").find("a.name").first().text().trim(); if (isDebug) { console.log("得到的名称为:" + name); } return name; }, getProgressContainer: function($sub) { return $sub.parents(".card-wrap").find("a.name").first().parent(); }, getVideoBox: function($ul) { return $ul.parents(".card-wrap").find(".WB_video_h5").first(); }, geiVideoSrc: function($box) { var src = $box.attr("action-data").match(/video_src=([\w\/\.%]+)/)[1]; src = decodeURIComponent(decodeURIComponent(src)); if (src.indexOf("http") != 0) { src = "https:" + src; } return src; } }; /** * 我的微博解析器(含:我的微博、他人微博、我的收藏、热门微博) * * @type {Object} */ var myWeiBoResolver = { getOperationList: function() { return $("div .screen_box ul:not([class='" + handledWeiBoCardClass + "'])"); }, getPhoto: function($ul) { return $ul.parents(".WB_feed_detail").find("li.WB_pic img"); }, getLivePhotoContainer: function($ul) { return $ul.parents(".WB_feed_detail").find(".WB_media_a"); }, getWeiBoId: function($ul) { var mid = $ul.parents(".WB_cardwrap").attr("mid").trim(); return mid; }, getWeiBoUserId: function($ul) { var $a = $ul.parents("div.WB_feed_detail").find("div.WB_info a").first(); var id = $a.attr("usercard").match(/id=(\d+)/)[1].trim(); if (isDebug) { console.log("得到的微博ID为:" + id); } return id; }, getWeiBoUserName: function($ul) { var name = $ul.parents("div.WB_feed_detail").find("div.WB_info a").first().text().trim(); if (isDebug) { console.log("得到的名称为:" + name); } return name; }, getProgressContainer: function($sub) { return $sub.parents("div.WB_feed_detail").find("div.WB_info").first(); }, getVideoBox: function($ul) { return $ul.parents(".WB_feed_detail").find(".WB_video,.WB_video_a,.li_story"); }, geiVideoSrc: function($box) { var video_sources = $box.attr("video-sources"); // 多清晰度源 var sources = video_sources.split("&"); if (isDebug) { console.log(sources); } var src; // 逐步下调清晰度 for (var i = sources.length - 2; i >= 0; i -= 2) { if (sources[i].trim().split("=")[1].trim().length > 0) { // 解码 var source = decodeURIComponent(decodeURIComponent(sources[i].trim())); if (isDebug) { console.log(source); } src = source.substring(source.indexOf("=") + 1); } } return src; } }; /** * 处理微博卡片 */ function handleWeiBoCard() { // 查找未被扩展的box var $uls = getWeiBoResolver().getOperationList(); if ($uls.length > 0) { console.info("找到未扩展的box:" + $uls.length); $uls.each(function(i, it) { handlePictureIfNeed($(it)); handleVideoIfNeed($(it)); }); // 批量给这些box添加已扩展标记 $uls.addClass(handledWeiBoCardClass); } } /** * 得到操作列表 * @return {$标签对象} 操作列表 */ function getWeiBoResolver() { var resolver; // 微博搜索 if (window.location.href.indexOf("https://s.weibo.com") === 0) { resolver = searchWeiBoResolver; } else { // 我的微博、他人微博、我的收藏、热门微博 resolver = myWeiBoResolver; } return resolver; } /** * 处理图片,如果需要 */ function handlePictureIfNeed($ul) { // 得到大图片 var $links = getLargePhoto($ul); if (isDebug) { console.log("此Item有图:" + $links.length); } // 判断图片是否存在 if ($links.length > 0) { // 得到LivePhoto的链接 var lp_links = getLivePhoto($ul, $links.length); // 存在LivePhoto if (lp_links) { $links = $($links.get().concat(lp_links)); } handleCopy($ul, $links); handleDownload($ul, $links); handleDownloadZip($ul, $links); } } /** * 处理视频如果需要 * @param {$标签对象} $ul 操作列表 */ function handleVideoIfNeed($ul) { var $box = getWeiBoResolver().getVideoBox($ul); // 不存在视频 if ($box.length === 0) { return; } var type = getVideoType($box); var fun; if (type === "feedvideo") { // 短视屏(秒拍、梨视频、优酷) fun = function() { downloadBlowVideo($box); }; } else if (type === "feedlive") { // 直播回放 //TODO 暂不支持 } else if (type === "story") { // 微博故事 fun = function() { downloadWeiboStory($box); }; } else { console.warn("未知的类型:" + type); } if (fun) { putButton($ul, "下载当前视频", fun); } } /** * 提取LivePhoto的地址 * @param {$标签对象} $owner ul或li * @return {字符串数组} LivePhoto地址集,可能为null */ function extractLivePhotoSrc($owner) { var action_data = $owner.attr("action-data"); if (action_data) { var urlsRegex = action_data.match(/pic_video=([\w:,]+)/); if (urlsRegex) { var urls = urlsRegex[1].split(",").map(function(it, i) { return it.split(":")[1]; }); return urls; } } return null; } /** * 得到视频类型 * @param {$标签对象} $box 视频容器 * @return {字符串} 视频类型[video、live] */ function getVideoType($box) { // console.log($box); // console.log($box.attr("action-data")); var typeRegex = $box.attr("action-data").match(/type=(\w+)&/); // console.log(typeRegex); return typeRegex[1]; } /** * 添加按钮 * @param {$标签对象} $ul 操作列表 * @param {字符串} name 按钮名称 * @param {方法} op 按钮操作 */ function putButton($ul, name, op) { var $li = $("<li><a href='javascript:void(0)'>—> " + name + " <—</a></li>"); $li.click(op); $ul.append($li); } // 处理拷贝 function handleCopy($ul, $links) { putButton($ul, "复制图片链接", function() { var link = $links.get().map(function(it, i) { return it.src; }).join("\n"); GM_setClipboard(link, "text"); tipSuccess("链接已复制到剪贴板!"); }); } // 处理下载 function handleDownload($ul, $links) { putButton($ul, "逐个下载图片", function() { $links.each(function(i, it) { // console.log("name:" + it.name + ",src=" + it.src); GM_download(it.src, it.name); }); }); } /** * 处理打包下载 */ function handleDownloadZip($ul, $links) { putButton($ul, "打包下载图片", function() { startZip($ul, $links); }); } /** * 下载微博故事视频 * * @param {$标签对象} $box 视频box */ function downloadWeiboStory($box) { var action_data = $box.attr("action-data"); var urlRegex = action_data.match(/gif_url=([\w%.]+)&/); var url = urlRegex[1]; var src = decodeURIComponent(decodeURIComponent(url)); var name = getResourceName($box, src.split("?")[0], 0); if (src.indexOf("//") === 0) { src = "https:" + src; } downloadVideo($box, name, src); } /** * 下载酷燃视频 * @param {$标签对象} $box 视频box */ function downloadBlowVideo($box) { var src, name; try { src = getWeiBoResolver().geiVideoSrc($box); if (!src) { // 未找到合适的视频地址 tipError("未能找到视频地址!"); throw new Error("未能找到视频地址!"); } name = getResourceName($box, src.split("?")[0], 0); if (isDebug) { console.log("download:" + name + "=" + src); } } catch (e) { console.error(e); tipError("提取视频地址失败!"); } downloadVideo($box, name, src); } /** * 下载直播回放 * @param {$标签对象} $li 视频box */ function downloadLiveVCRVideo($ul, $li) { // TODO 暂不支持 } /** * 下载视频 * @param {$标签对象} $box 视频box */ function downloadVideo($box, name, src) { tipInfo("即将开始下载..."); var progress = bornProgress($box); GM_download({ url: src, name: name, onprogress: function(p) { var value = p.loaded / p.total; progress.value = value; }, onerror: function(e) { console.error(e); tipError("视频下载出错!"); } }); } /** * 得到LivePhoto链接集 * * @param {$标签对象} $ul 操作列表 * @param {整数} start 下标开始的位置 * @return {Link数组} 链接集,可能为null */ function getLivePhoto($ul, start) { var $box = getWeiBoResolver().getLivePhotoContainer($ul); var srcs; // 仅有一张LivePhoto if ($box.hasClass('WB_media_a_m1')) { srcs = extractLivePhotoSrc($box.find(".WB_pic")); } else { srcs = extractLivePhotoSrc($box); } // 判断是否存在LivePhoto的链接 if (srcs) { srcs = srcs.map(function(it, i) { var src = "https://video.weibo.com/media/play?livephoto=//us.sinaimg.cn/" + it + ".mov&KID=unistore,videomovSrc"; var name = getResourceName($ul, "https://weibo.com/" + it + ".mp4", i + start); return bornLink(name, src); }); } return srcs; } function bornLink(name, src) { return { name: name, src: src }; } /** * 得到大图链接 * * @param {$标签对象} $ul 操作列表 * @return {Link数组} 链接集,可能为null */ function getLargePhoto($ul) { // 得到每一个图片 var links = getWeiBoResolver().getPhoto($ul).map(function(i, it) { var parts = $(it).attr("src").split("/"); // 替换为大图链接 var src = "http://wx2.sinaimg.cn/large/" + parts[parts.length - 1]; if (isDebug) { console.log(src); } var name = getResourceName($ul, src, i); return bornLink(name, src); }); return links; } /** * 得到打包名称 * * @param {$标签对象} $ul 操作列表 * @return {字符串} 压缩包名称(不含后缀) */ function getZipName($ul) { var name; var weiBoResolver = getWeiBoResolver(); // 2:微博用户ID-微博ID(如:5578564422-4375413591293810.zip) if (zipNamingStrategy === 2) { name = weiBoResolver.getWeiBoUserId($ul) + nameingSeparator + weiBoResolver.getWeiBoId($ul); } else { // 1:微博用户名-微博ID(如:小米商城-4375413591293810.zip)[缺省] name = weiBoResolver.getWeiBoUserName($ul) + nameingSeparator + weiBoResolver.getWeiBoId($ul); } return name; } /** * 得到资源名称 * * @param {$标签对象} $ul 操作列表 * @param {字符串} src 资源地址 * @param {整数} index 序号 * @return {字符串} 资源名称(含后缀) */ function getResourceName($ul, src, index) { var name; // 0:资源原始名称(如:0065x5rwly1g3c6exw0a2j30u012utyg.jpg) if (resourceNamingStrategy === 0) { name = getPathName(src); } else { // 修正,从1开始 index++; // 补齐位数:01、02、03... if (index.toString().length === 1) { index = "0" + index.toString(); } var postfix = getPathPostfix(src); var weiBoResolver = getWeiBoResolver(); // 2:微博用户ID-微博ID-序号(如:5578564422-4375413591293810-01.jpg) if (resourceNamingStrategy == 2) { name = weiBoResolver.getWeiBoUserId($ul) + nameingSeparator + weiBoResolver.getWeiBoId($ul) + nameingSeparator + index + postfix; } else { // 1:微博用户名-微博ID-序号(如:小米商城-4375413591293810-01.jpg)[缺省] name = weiBoResolver.getWeiBoUserName($ul) + nameingSeparator + weiBoResolver.getWeiBoId($ul) + nameingSeparator + index + postfix; } } return name; } /** * 得到后缀 * @param {字符串} path 路径 * @return {字符串} 后缀(含.) */ function getPathPostfix(path) { var postfix = path.substring(path.lastIndexOf(".")); if (isDebug) { console.log("截得后缀为:" + postfix); } return postfix; } /** * 得到资源原始名称 * @param {字符串} path 路径 * @return {字符串} 名称(含后缀) */ function getPathName(path) { var name = path.substring(path.lastIndexOf("/") + 1); if (isDebug) { console.log("截得名称为:" + name); } return name; } /** * 生成一个进度条 * @param {$标签对象} $sub card的子节点 * @param {int} max 最大值 * @return {标签对象} 进度条 */ function bornProgress($sub) { var $div = getWeiBoResolver().getProgressContainer($sub); // 尝试获取进度条 var $progress = $div.find('progress'); // 进度条不存在时,生成一个 if ($progress.length === 0) { $progress = $("<progress max='1' style='margin-left:10px;' />"); $div.append($progress); } else { // 已存在时,重置value $progress[0].value = 0; } return $progress[0]; } /** * 开始打包 * @param {$数组} $links 图片地址集 */ function startZip($ul, $links) { tip("正在提取,请稍候...", "iconExtract"); var progress = bornProgress($ul); var zip = new JSZip(); var names = []; $links.each(function(i, it) { var name = it.name; GM_xmlhttpRequest({ method: 'GET', url: it.src, timeout: maxRequestTime, responseType: "blob", onload: function(response) { zip.file(name, response.response); downloadZipIfComplete($ul, progress, name, zip, names, $links.length); }, onerror: function(e) { console.error(e); tipError("第" + (i + 1) + "个对象,获取失败!"); downloadZipIfComplete($ul, progress, name, zip, names, $links.length); }, ontimeout: function() { tipError("第" + (i + 1) + "个对象,请求超时!"); downloadZipIfComplete($ul, progress, name, zip, names, $links.length); } }); }); } /** * 下载打包,如果完成 */ function downloadZipIfComplete($ul, progress, name, zip, names, length) { names.push(name); var value = names.length / length; progress.value = value; if (names.length === length) { tip("正在打包,请稍候...", "iconZip"); zip.generateAsync({ type: "blob" }, function(metadata) { progress.value = metadata.percent / 100; }).then(function(content) { tipSuccess("打包完成,即将开始下载!"); var zipName = getZipName($ul); saveAs(content, zipName + ".zip"); }); } } function tip(text, iconName) { GM_notification({ text: text, image: GM_getResourceURL(iconName) }); } function tipInfo(text) { tip(text, "iconInfo"); } function tipError(text) { tip(text, "iconError"); } function tipSuccess(text) { tip(text, "iconSuccess"); } setInterval(handleWeiBoCard, space); })();