├── LICENSE ├── README.md └── 百度网盘_文件库清单导出.user.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Evan Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![image.png](https://note.youdao.com/yws/public/resource/7118a972bf860164e6674f7b167271ae/WEBRESOURCE98a421a7955663862f67696fde34aabf) 3 | 4 | > 掘金用法: 5 | [导出新版本百度网盘文件库目录——篡改猴脚本](https://juejin.cn/spost/7300876243953025075) 6 | 7 | # 0 更新日志 8 | 9 | ## 2024.1.20 10 | 11 | 1.增加Excel导出模式 12 | 13 | # 1 背景 14 | 15 | 当前版本适用于新的百度网盘进行目录导出。 16 | 脚本基于[【Avens666/BaidunNetDisk-script】](https://github.com/Avens666/BaidunNetDisk-script),虽然已经全改,但还是拉一个fork分支表示尊敬,【Avens666/BaidunNetDisk-script】当前已不适用于新版本的百度网盘页面,所以干脆重写了。 17 | 18 | github地址: 19 | 20 | 21 | *** 22 | 23 | # 2 作用 24 | 25 | > 当你找到这个插件的时候,你已经知道它是做什么用的了,也知道为什么要用这个了... 26 | 27 | 虽然自己的网盘,有导出目录的工具,但是很多用户间分享的网盘数据,动辄10T 20T,不可能都转存到自己的网盘中。而且转存也有文件数量限制等。 28 | 现在买网盘资源,基本都是这种分享方式,而不是卖账号。 29 | 因此,分享的网盘资源,里面到底有些什么内容,其实很不方便查看 30 | 31 | 搜索了一圈,没有现成的方案,大家都在关注如何加速下载等,github上虽然百度网盘相关项目有近百个,但是没有专门导出目录信息的项目。 而且主要都还是针对自己的网盘文件,这种在群中分享的网盘资源,没有现成的访问方法。 32 | 没办法,大佬们不关注这个需求,只能自己动手了。 33 | 在参考了github中的相关项目,比较了多种技术手段(python脚本,java程序,go语言,油猴脚本)后,本人使用油猴脚本方式来实现这个功能。 34 | 功能设计: 导出分享文件库的所有目录和文件内容清单,同时对文件夹内的文件数量和尺寸进行递归统计。 35 | 36 | 针对百度网盘的油猴脚本,可导出百度网盘共享文件库目录清单 37 | chrome浏览器+油猴脚本测试通过 38 | 油猴脚本怎么用我就不写了,请自行百度。 39 | 40 | # 3 插件使用说明 41 | 42 | ## 3.1 安装插件 43 | 44 | 45 | 46 | ## 3.2 导出方法 47 | 48 | 1.进入百度网盘 49 | 50 | 1. 点击【消息】 51 | 2. 按下f12,选中【Console】,便于观察导出的日志和进度 52 | 53 | ![image.png](https://note.youdao.com/yws/public/resource/7118a972bf860164e6674f7b167271ae/WEBRESOURCE7619d57f533d1fc280bb12b75a41eb54) 54 | 55 | 2.选择资源 56 | 57 | 1. 选择一个共享群 58 | 2. 点击右上角的【文件库】(切记一定要从右上角打开文件库) 59 | 3. 然后点击进入一个目录 60 | 4. 控制台出现准备就绪字样,就可以进行下一步操作 61 | 62 | ![image.png](https://note.youdao.com/yws/public/resource/7118a972bf860164e6674f7b167271ae/WEBRESOURCE5e0735a913a12f529131c66ece8fdbd0) 63 | 64 | 3.导出资源目录 65 | 66 | 1. 勾选需要导出的目录 67 | 2. 出现导出按钮,点击导出 68 | 3. 控制台出现导出进度 69 | 70 | ![image.png](https://note.youdao.com/yws/public/resource/7118a972bf860164e6674f7b167271ae/WEBRESOURCEa6c9e7718f5f6137b992a87b7c865d55) 71 | 72 | 4.导出完成后,将目录自动下载到浏览器配置的下载路径。 73 | 74 | # 4 导出的目录格式 75 | 76 | 例如: 77 | 78 | ```txt 79 | 总耗时:15816ms 80 | 共导出资源数:1588 81 | 共计资源大小:81.89GB = 83857.63MB 82 | 83 | 人月神话.ppt, /xxxxxx/xxxxxx/xxxxxx/人月神话.ppt, 1.53, 836611238345266 84 | ... 85 | ``` 86 | 87 | 格式为: 88 | 89 | 资源名称, 路径, 大小MB,网盘资源的fs_id 90 | 91 | 如需其他序列化格式,可自行修改脚本。 92 | 93 | # 5 使用建议 94 | 95 | 由于文件库目录可能较为庞大,导出过程采用递归遍历的方式,可能会花费较大的时间,而插件在执行过程中只会有一个线程在跑,所以最好的做法就是,**开启多个浏览器页签**,分多个目录进行导出,实现并行过程。 96 | 97 | # 6 性能 98 | 99 | 这是我在一次导出过程中截的图,导出3.7w的资源数,花费716秒,将近12分钟的样子,这很大程度上取决于文件目录的深度和目录数量,以及你网络带宽和cpu性能。 100 | 101 | ![image.png](https://note.youdao.com/yws/public/resource/7118a972bf860164e6674f7b167271ae/WEBRESOURCEc53b219a5a66f6c5d3d6d29ede76ad2c) 102 | 103 | -------------------------------------------------------------------------------- /百度网盘_文件库清单导出.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name 百度网盘文件库目录导出 3 | // @namespace https://github.com/liong911/BaidunNetDisk-script 4 | // @version 1.1.0 5 | // @description 适用于新版本百度网盘文件库目录导出的篡改猴脚本。js牛逼! 6 | // @author liong 7 | // @license MIT 8 | // @match https://pan.baidu.com/disk* 9 | // @run-at document-start 10 | // @grant unsafeWindow 11 | // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.3/FileSaver.min.js 12 | // @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.16.9/xlsx.full.min.js 13 | // @require https://code.jquery.com/jquery-3.1.1.min.js 14 | // ==/UserScript== 15 | 16 | (function() { 17 | 'use strict' 18 | // Your code here... 19 | 20 | const Mode = { 21 | FILE: 1, 22 | EXCEL: 2 23 | }; 24 | 25 | const config = { 26 | // 请求分页每页500条 27 | pageSize: 500, 28 | // 每500条打印一次日志 29 | tipsCount: 500, 30 | 31 | exportMode: { 32 | mode: Mode.EXCEL, 33 | file: { 34 | SPLITTER: ', ' 35 | }, 36 | excel: {} 37 | } 38 | } 39 | 40 | var exportInfo = { 41 | content: [], 42 | statistic: { 43 | fileCount: 0, 44 | fileSize: 0, 45 | startTime: new Date(), 46 | }, 47 | 48 | exportSelectedDir: function() { 49 | // 弹出选择框,选择导出模式 50 | let mode = prompt("请选择导出模式:\n1. 文件\n2. Excel", "1") 51 | if (mode == null || mode.length <= 0) { 52 | return 53 | } 54 | config.exportMode.mode = parseInt(mode) 55 | 56 | exportInfo.init() 57 | exportInfo.exportEveryDir() 58 | exportInfo.save() 59 | }, 60 | init: function() { 61 | this.content = [] 62 | 63 | this.statistic.fileCount = 0 64 | this.statistic.fileSize = 0 65 | this.statistic.startTime = new Date() 66 | }, 67 | exportEveryDir: function() { 68 | let selectedList = $("tr[class='im-pan-table__body-row mouse-choose-item selected']") 69 | for (let i = 0; i < selectedList.length; i++) { 70 | let selected = selectedList[i]; 71 | let fsId = selected.dataset.id.substring(16,31) 72 | this.loopQueryPage(fsId, 1) 73 | } 74 | }, 75 | loopQueryPage: function (fsId, pageNo) { 76 | let gidUrl = `https://pan.baidu.com/mbox/msg/shareinfo?page=${pageNo}&num=${config.pageSize}&fs_id=${fsId}&from_uk=${authInfo.fromUk}&msg_id=${authInfo.msgId}&type=2&gid=${authInfo.gid}&limit=50&desc=1&clienttype=0&app_id=250528&web=1&dp-logid=${authInfo.dpLogid}` 77 | $.ajax({ 78 | type:'GET', 79 | url: gidUrl, 80 | data:{}, 81 | dataType: "json", 82 | async: false, 83 | success: (res) => { 84 | console.log(`url:`, gidUrl) 85 | if (res.errno != 0) { 86 | console.error('获取异常', res) 87 | } 88 | let hasMore = res.has_more 89 | this.processBiz(res.records) 90 | if (hasMore === 1) { 91 | this.loopQueryPage(fsId, pageNo + 1) 92 | } 93 | }, 94 | error:function(err){ 95 | console.error(err) 96 | } 97 | }) 98 | }, 99 | processBiz: function(records) { 100 | if (records == null || records.length == 0) { 101 | return 102 | } 103 | 104 | for (let i = 0; i < records.length; i++) { 105 | let record = records[i] 106 | if (record.isdir == 0) { 107 | this.doBiz(record) 108 | } else { 109 | this.loopQueryPage(record.fs_id, 1) 110 | } 111 | } 112 | }, 113 | doBiz: function(record) { 114 | let fileMb = record.size / 1024 / 1024 115 | let fileObj = { 116 | name: record.server_filename, 117 | path: record.path, 118 | size: fileMb.toFixed(2), 119 | fsId: record.fs_id, 120 | localMtime: record.local_mtime, 121 | serverMtime: record.server_mtime 122 | } 123 | this.content.push(fileObj) 124 | 125 | // 计数 126 | this.recordStatistic(record) 127 | }, 128 | recordStatistic: function(record) { 129 | this.statistic.fileCount++ 130 | this.statistic.fileSize += record.size 131 | if (this.statistic.fileCount % config.tipsCount === 0) { 132 | console.log(`累计导出资源数:${this.statistic.fileCount}`) 133 | let gb = this.statistic.fileSize / 1024 / 1024 / 1024 134 | let mb = this.statistic.fileSize / 1024 / 1024 135 | console.log(`累计资源大小:${gb.toFixed(2)}GB = ${mb.toFixed(2)}MB`) 136 | let time = new Date().getTime() - this.statistic.startTime.getTime(); 137 | console.log(`累计耗时:${time}ms,开始时间:${this.statistic.startTime}`) 138 | } 139 | }, 140 | save: function() { 141 | // 打印执行结果 142 | let result = this.printResult() 143 | 144 | if (config.exportMode.mode === Mode.FILE) { 145 | this.writeToFile(result) 146 | } 147 | 148 | if (config.exportMode.mode === Mode.EXCEL) { 149 | this.writeToExcel(result) 150 | } 151 | }, 152 | printResult: function() { 153 | let gb = this.statistic.fileSize / 1024 / 1024 / 1024 154 | let mb = this.statistic.fileSize / 1024 / 1024 155 | let result = `总耗时:${new Date().getTime() - this.statistic.startTime.getTime()}ms\n` 156 | + `共导出资源数:${this.statistic.fileCount}\n` 157 | + `共计资源大小:${gb.toFixed(2)}GB = ${mb.toFixed(2)}MB` 158 | console.log(result) 159 | 160 | return result 161 | }, 162 | writeToFile: function(result) { 163 | let contentStr = result + '\n\n' 164 | for (let i = 0; i < this.content.length; i++) { 165 | let fileObj = this.content[i] 166 | // 资源名称, 路径, 大小MB,网盘资源的fs_id 167 | contentStr += fileObj.name + config.exportMode.file.SPLITTER + fileObj.path + config.exportMode.file.SPLITTER + fileObj.size + config.exportMode.file.SPLITTER + fileObj.fsId + '\n' 168 | } 169 | 170 | let blob = new Blob([contentStr], {type: "text/plaincharset=utf-8"}) 171 | saveAs(blob, "exportDirList.txt") 172 | }, 173 | writeToExcel: function(result) { 174 | let wsContent = this.processExcelData() 175 | this.doExcel(wsContent) 176 | }, 177 | processExcelData: function() { 178 | // 数据 179 | let ws_data = [] 180 | let maxLevelCount = 0 181 | for (let i = 0; i < this.content.length; i++) { 182 | let fileObj = this.content[i] 183 | 184 | let fileArr = [fileObj.name, fileObj.size, this.timestampToDatetime(fileObj.localMtime), this.timestampToDatetime(fileObj.serverMtime), fileObj.path] 185 | let pathLevelArr = this.getPathLevelArr(fileObj.path); 186 | let concat = fileArr.concat(pathLevelArr); 187 | ws_data.push(concat) 188 | 189 | maxLevelCount = Math.max(maxLevelCount, pathLevelArr.length) 190 | } 191 | 192 | // 表头 193 | let wd_header = ['文件名', '大小(mb)', '修改时间' ,'云端时间', '完整路径'] 194 | for (let i = 0; i < maxLevelCount; i++) { 195 | wd_header.push(`${i + 1}级`) 196 | } 197 | 198 | // sheet 199 | let wsContent = [] 200 | wsContent.push(wd_header) 201 | wsContent = wsContent.concat(ws_data) 202 | 203 | return wsContent 204 | }, 205 | timestampToDatetime: function(timestamp) { 206 | let date = new Date(timestamp * 1000); 207 | // 使用toLocaleString方法格式化日期和时间 208 | let datetime = date.toLocaleString('zh-CN', { hour12: false }); 209 | return datetime; 210 | }, 211 | getPathLevelArr: function(path) { 212 | let pathLevel = [] 213 | let pathSplit = path.split('/'); 214 | for (let i = 1; i < pathSplit.length - 1; i++) { 215 | pathLevel.push(pathSplit[i]) 216 | } 217 | 218 | return pathLevel 219 | }, 220 | doExcel(wsContent) { 221 | let wb = XLSX.utils.book_new(); 222 | let ws = XLSX.utils.aoa_to_sheet(wsContent); 223 | XLSX.utils.book_append_sheet(wb, ws, 'Sheet1'); 224 | XLSX.writeFile(wb, 'exportDirList.xlsx'); 225 | } 226 | } 227 | 228 | let authInfo = { 229 | gid: '', 230 | fromUk: '', 231 | msgId: '', 232 | dpLogid: '', 233 | isChangeFileFolder: false, 234 | 235 | init: function() { 236 | this.initAuthInfo() 237 | this.addExportButton() 238 | }, 239 | initAuthInfo: function() { 240 | let _this = this 241 | const originOpen = XMLHttpRequest.prototype.open 242 | XMLHttpRequest.prototype.open = function (_, url) { 243 | _this.clickGroupFileLibrary(url) 244 | _this.clickOneFileFolder(url) 245 | originOpen.apply(this, arguments) 246 | } 247 | }, 248 | addExportButton: function() { 249 | let $dropdownbutton = $('') 250 | $dropdownbutton.click(exportInfo.exportSelectedDir) 251 | var task = setInterval(() => { 252 | var buttons = $("div[class='im-file-nav__operate']") 253 | if (buttons.text().indexOf('导出') < 0) { 254 | buttons.append($dropdownbutton) 255 | } 256 | }, 2000) 257 | }, 258 | clickGroupFileLibrary: function(url) { 259 | if (url.indexOf("/mbox/group/listshare") >= 0) { 260 | let gidNew = this.getUrlParamValueByName(url, 'gid') 261 | if (authInfo.gid != gidNew) { 262 | authInfo.isChangeFileFolder = true 263 | authInfo.gid = gidNew 264 | console.log('准备进度[1/4],拿到[gid]啦,玲宝O(∩_∩)O哈哈~:' + authInfo.gid) 265 | } 266 | } 267 | }, 268 | clickOneFileFolder: function(url) { 269 | if (url.indexOf("/mbox/msg/shareinfo") >= 0 && authInfo.fromUk.length <= 0) { 270 | authInfo.fromUk = this.getUrlParamValueByName(url, 'from_uk') 271 | console.log('准备进度[2/4],拿到[from_uk]啦,玲宝O(∩_∩)O哈哈~:' + authInfo.fromUk) 272 | } 273 | 274 | if (url.indexOf("/mbox/msg/shareinfo") >= 0 && authInfo.isChangeFileFolder) { 275 | authInfo.msgId = this.getUrlParamValueByName(url, 'msg_id') 276 | console.log('准备进度[3/4],拿到[msg_id]啦,玲宝O(∩_∩)O哈哈~:' +authInfo. msgId) 277 | authInfo.dpLogid = this.getUrlParamValueByName(url, 'dp-logid') 278 | console.log('准备进度[4/4],拿到[dp_logid]啦,玲宝O(∩_∩)O哈哈~:' + authInfo.dpLogid) 279 | console.log('准备完成,可以开始导出啦,玲宝O(∩_∩)O哈哈~') 280 | authInfo.isChangeFileFolder = false 281 | } 282 | }, 283 | getUrlParamValueByName: function(url, name) { 284 | // \b 边界 285 | // ?<= 向后匹配 286 | // 字符串转成正则表达式,其中的'\b'类型的特殊字符要多加一个'\' 287 | let reg = new RegExp(`(?<=\\b${name}=)[^&]*`) 288 | let target = url.match(reg) 289 | 290 | if(target) { 291 | return target[0] 292 | } 293 | 294 | return 295 | } 296 | } 297 | authInfo.init() 298 | })() 299 | --------------------------------------------------------------------------------