豆瓣读书+电影+音乐+游戏+舞台剧导出工具

将【读过/看过/听过/玩过】的 【读书/电影/音乐/游戏/舞台剧】条目分别导出为 csv 文件

  1. // ==UserScript==
  2. // @name 豆瓣读书+电影+音乐+游戏+舞台剧导出工具
  3. // @namespace https://ulyc.github.io/
  4. // @version 0.1.2
  5. // @description 将【读过/看过/听过/玩过】的 【读书/电影/音乐/游戏/舞台剧】条目分别导出为 csv 文件
  6. // @author ulyc
  7. // @match https://book.douban.com/people/*/collect*
  8. // @match https://movie.douban.com/people/*/collect*
  9. // @match https://music.douban.com/people/*/collect*
  10. // @match https://www.douban.com/location/people/*/drama/collect*
  11. // @match https://*.douban.com/people/*
  12. // @require https://unpkg.com/dexie@latest/dist/dexie.js
  13. // @grant none
  14. // @original-script https://gf.qytechs.cn/en/scripts/420999-%E8%B1%86%E7%93%A3%E8%AF%BB%E4%B9%A6-%E7%94%B5%E5%BD%B1-%E9%9F%B3%E4%B9%90-%E6%B8%B8%E6%88%8F-%E8%88%9E%E5%8F%B0%E5%89%A7%E5%AF%BC%E5%87%BA%E5%B7%A5%E5%85%B7
  15. // @original-license MIT
  16. // @license MIT
  17. // ==/UserScript==
  18.  
  19.  
  20. (function () {
  21. 'use strict';
  22. let people;
  23. const MOVIE = 'movie', BOOK = 'book', MUSIC = 'music', GAME = 'game', DRAMA = 'drama';
  24. const commonItem = "++id, cover, title, rating, rating_date, comment,";
  25.  
  26. /* global $, Dexie */
  27.  
  28. function getExportLink(type, people) {
  29. return 'https://' + type + '.douban.com/people/' + people + '/collect?start=0&sort=time&mode=grid&rating=all&export=1';
  30. }
  31.  
  32. function getGameExportLink(people) { // type=game
  33. return 'https://www.douban.com/people/' + people + '/games?action=collect&start=0&export=1';
  34. }
  35.  
  36. function getDramaExportLink(people) { // type=drama
  37. return 'https://www.douban.com/location/people/' + people + '/drama/collect?start=0&sort=time&mode=grid&rating=all&export=1';
  38. }
  39.  
  40.  
  41. function addButton(type, people, isGeneral) {
  42. let typeElement;
  43. if (isGeneral) {
  44. typeElement = $('#' + type + ' h2 .pl a:last');
  45. } else {
  46. typeElement = $('#db-' + type + '-mine h2:contains(过) .pl');
  47. }
  48. switch (type) {
  49. case BOOK:
  50. typeElement.after('&nbsp;·&nbsp;<a href="' + getExportLink(type, people) + '">导出读过的书</a>');
  51. break;
  52. case MOVIE:
  53. typeElement.after('&nbsp;·&nbsp;<a href="' + getExportLink(type, people) + '">导出看过的片</a>');
  54. break;
  55. case MUSIC:
  56. typeElement.after('&nbsp;·&nbsp;<a href="' + getExportLink(type, people) + '">导出听过的碟</a>');
  57. break;
  58. case GAME:
  59. typeElement.after('&nbsp;·&nbsp;<a href="' + getGameExportLink(people) + '">导出玩过的游戏</a>');
  60. break;
  61. case DRAMA:
  62. typeElement.after('&nbsp;·&nbsp;<a href="' + getDramaExportLink(people) + '">导出看过的舞台剧</a>');
  63. break;
  64. default:
  65. return;
  66. }
  67. }
  68.  
  69. function getType() {
  70. let type = location.hostname.split(".")[0];
  71.  
  72. if (type === "www") {
  73. if (location.pathname.indexOf('games') > -1) {
  74. type = GAME;
  75. }
  76. if (location.pathname.indexOf('drama') > -1) {
  77. type = DRAMA;
  78. }
  79. }
  80. return type;
  81. }
  82.  
  83. // 根据hostname判断type
  84. // 加入导出按钮
  85. if (location.href.indexOf('douban.com') > -1 && location.href.indexOf('export=1') < 0) {
  86. let match;
  87. let type = getType();
  88. if (type === DRAMA) {
  89. match = location.pathname.match(/location\/people\/([^/]+)\//);
  90. } else {
  91. match = location.pathname.match(/people\/([^/]+)\//);
  92. }
  93. people = match ? match[1] : null;
  94. if (!people) {
  95. return
  96. }
  97.  
  98. if (type === "www") {
  99. addButton(BOOK, people, true);
  100. addButton(MOVIE, people, true);
  101. addButton(MUSIC, people, true);
  102. addButton(GAME, people, true);
  103. addButton(DRAMA, people, true);
  104. } else {
  105. addButton(type, people, false);
  106. }
  107. }
  108.  
  109. if (location.href.indexOf('export=1') > -1) {
  110. let type = getType();
  111. if ([DRAMA, GAME, BOOK, MUSIC, MOVIE].indexOf(type) < 0) {
  112. alert("类型错误,请检查路径")
  113. return;
  114. }
  115.  
  116. init(type);
  117. }
  118.  
  119.  
  120. function escapeQuote(str) {
  121. return str.replaceAll('"', '""'); // " need to be replaced with two quotes to escape inside csv quoted string
  122. }
  123.  
  124. // 获取当前页数据
  125. function getCurPageItems(type) {
  126. var items = [];
  127. var elems = $('.grid-view .item');
  128.  
  129. if (type === GAME) {
  130. elems = $('.game-list .common-item');
  131. } else if (type === BOOK) {
  132. elems = $('.interest-list .subject-item');
  133. }
  134.  
  135. elems.each(function (index) {
  136. let item = {
  137. title: "",
  138. rate: 5,
  139. comment: "",
  140. cover: escapeQuote($(this).find('.pic img').attr('src').trim()),
  141. 'rating_date': $(this).find('.date').text().trim().replaceAll('-', '/'), // 2020-07-17 => 2020/07/17
  142. };
  143. transWebp2Jpg(item);
  144. getTitleAndLink($(this), item, type);
  145. //兼容被封图书项目,电影被封项目似乎不展示
  146. if (item.cover.indexOf("book-default-lpic") > -1 && item.title.indexOf("未知") > -1) {
  147. item.title = "被封禁的图书";
  148. item.rate = 5;
  149. item.comment = "无";
  150. item.author = "佚名";
  151. item.release_date = "1984/01/01";
  152. items[index] = item;
  153. } else if (item.cover.indexOf("lpic/music-default") > -1 && item.title.indexOf("未知") > -1) {
  154. item.title = "被封禁的音乐";
  155. item.rate = 5;
  156. item.comment = "无";
  157. item.musician = "佚名";
  158. item.release_date = "1984/01/01";
  159. items[index] = item;
  160. } else {
  161. getRate($(this), item, type);
  162. getComment($(this), item, type);
  163. getExtraInfo($(this), item, type, index);
  164. }
  165.  
  166. });
  167. return items;
  168.  
  169. function getTitleAndLink(elem, item, type) {
  170. if (type === BOOK) {
  171. item.title = elem.find('.info a').attr("title");
  172. item.link = elem.find('.info a').attr("href").trim();
  173. } else {
  174. item.title = elem.find('.title a').text();
  175. item.link = elem.find('.title a').attr('href').trim();
  176. }
  177. // 处理空格/换行符 , 优化格式
  178. item.title = escapeQuote(item.title.trim().replace(/\s*/g, "").replace(/\r\n/g, "").replace(/\n/g, ""))
  179. }
  180.  
  181. function getRate(elem, item, type) {
  182. // 获取 评分
  183. if (type === GAME) {
  184. let rating = elem.find('.rating-info .rating-star').attr('class');
  185. rating = rating
  186. ? (rating.slice(19, 20) === 'N' ? '' : Number(rating.slice(19, 20)))
  187. : '';
  188. item.rating = rating;
  189. } else {
  190. let rating = elem.find('.date')[0].previousElementSibling;
  191. if (rating) {
  192. rating = $(rating).attr('class').slice(6, 7);
  193. }
  194. item.rating = rating ? Number(rating) : '';
  195. }
  196. }
  197.  
  198. function getComment(elem, item, type) {
  199. let co = elem.find('.comment');
  200. if (co.length) {
  201. co = co[0];
  202. item.comment = escapeQuote(co.textContent.trim());
  203. } else if (type === GAME) {
  204. co = elem.find('.user-operation');
  205. if (co.length) {
  206. co = co[0];
  207. item.comment = co.previousElementSibling.textContent.trim();
  208. item.comment = escapeQuote(item.comment);
  209. }
  210. } else if (type === DRAMA || type === MUSIC) {
  211. co = elem.find('ul li:last');
  212. item.comment = escapeQuote(co.text().trim());
  213. }
  214. }
  215.  
  216. function getExtraInfo(elem, item, type, index) {
  217. let extra;
  218. switch (type) {
  219. case GAME:
  220. extra = elem.find('.desc')[0].firstChild.textContent.trim();
  221. item.release_date = extra.split(' / ').slice(-1)[0];
  222. items[index] = item;
  223. return; // for type=game, here is over
  224. case DRAMA:
  225. extra = elem.find('.intro')[0].textContent.trim();
  226. item.mixed_info = extra;
  227. items[index] = item;
  228. return; // for type=drama, here is over
  229. case MOVIE:
  230. extra = elem.find('.intro').text().split(' / ')[0];
  231. let res = extra.match(/^(\d{4}-\d{2}-\d{2})\((.*)\)$/);
  232. if (res) {
  233. item.release_date = res[1].replaceAll('-', '/');
  234. item.country = res[2];
  235. }
  236. break;
  237. case MUSIC:
  238. case BOOK:
  239. let className = type === BOOK ? 'pub' : 'intro';
  240. extra = elem.find('.' + className).text().split(' / ');
  241. // 不一定有准确日期,可能是 2009-5 这样的, 也可能就只有年份 2000
  242. let dateReg = /\d{4}(?:-\d{1,2})?(?:-\d{1,2})?/;
  243. if (!dateReg.test(extra[0])) { // extra 首项非日期,则一般为作者或音乐家
  244. let author = escapeQuote(extra[0].trim());
  245. item.musician = item.author = author;
  246. }
  247. let d = extra.filter(function (txt) {
  248. return dateReg.test(txt);
  249. });
  250. if (d.length) {
  251. item.release_date = d[0].replaceAll('-', '/');
  252. }
  253. break;
  254. }
  255.  
  256. items[index] = item;
  257. }
  258.  
  259. function transWebp2Jpg(item) {
  260. // notion不识别webp格式图片
  261. // 将豆瓣webp格式图片 转换为 jpg
  262.  
  263. if (!!item.cover && item.cover.endsWith("webp")) {
  264. item.cover = item.cover.replace(".webp", ".jpg")
  265. }
  266. }
  267.  
  268. }
  269.  
  270. function getDBItems(type) {
  271. let items;
  272. switch (type) {
  273. case MOVIE:
  274. items = commonItem + ' release_date, country, link';
  275. break;
  276. case MUSIC:
  277. items = commonItem + ' release_date, author, link';
  278. break;
  279. case BOOK:
  280. items = commonItem + ' release_date, author, link';
  281. break;
  282. case GAME:
  283. items = commonItem + ' release_date, link';
  284. break;
  285. case DRAMA:
  286. items = commonItem + ' mixed_info, link';
  287. break;
  288. }
  289. return items;
  290. }
  291.  
  292. function init(type) {
  293. const db = new Dexie('db_export'); // init indexedDB
  294. let dbItems = getDBItems(type);
  295. db.version(1).stores({
  296. items: dbItems
  297. });
  298.  
  299. const items = getCurPageItems(type);
  300. db.items.bulkAdd(items).then(function () {
  301. console.log('添加成功+', items.length);
  302.  
  303. let nextPageLink = $('.paginator span.next a').attr('href');
  304. if (nextPageLink) {
  305. nextPageLink = nextPageLink + '&export=1';
  306. window.location.href = nextPageLink;
  307. } else {
  308. exportAll(type);
  309. }
  310. }).catch(function (error) {
  311. console.error("Ooops: " + error);
  312. });
  313. }
  314.  
  315. function exportAll(type) {
  316. const db = new Dexie('db_export');
  317. let items = getDBItems(type);
  318.  
  319. db.version(1).stores({
  320. items: items
  321. });
  322. db.items.orderBy('rating_date').reverse().toArray().then(function (all) {
  323. all = all.map(function (item) {
  324. delete item.id;
  325. return item;
  326. });
  327.  
  328. let title = ['封面', '标题', '个人评分', '打分日期', '我的短评'];
  329. let key = ['cover', 'title', 'rating', 'rating_date', 'comment', 'release_date'];
  330. if (type === MOVIE) {
  331. title = title.concat(['上映日期', '制片国家', '条目链接']);
  332. key = key.concat(['country', 'link']);
  333. } else if (type === BOOK) {
  334. title = title.concat(['出版日期', '作者', '条目链接']);
  335. key = key.concat(['author', 'link']);
  336. } else if (type === MUSIC) {
  337. title = title.concat(['发行日期', '音乐家', '条目链接']);
  338. key = key.concat(['musician', 'link']);
  339. } else if (type === GAME) {
  340. title = title.concat(['发行日期', '条目链接']);
  341. key.push('link');
  342. } else if (type === DRAMA) {
  343. title = title.concat(['混合信息', '条目链接']);
  344. key.pop();
  345. key = key.concat(['mixed_info', 'link']);
  346. }
  347.  
  348. JSonToCSV.setDataConver({
  349. data: all,
  350. fileName: 'db-' + type + '-' + new Date().toISOString().split('T')[0].replaceAll('-', ''),
  351. columns: {title, key},
  352. });
  353. db.delete();
  354. });
  355. }
  356.  
  357. // 导出CSV函数
  358. // https://github.com/liqingzheng/pc/blob/master/JsonExportToCSV.js
  359. var JSonToCSV = {
  360. /*
  361. * obj是一个对象,其中包含有:
  362. * ## data 是导出的具体数据
  363. * ## fileName 是导出时保存的文件名称 是string格式
  364. * ## showLabel 表示是否显示表头 默认显示 是布尔格式
  365. * ## columns 是表头对象,且title和key必须一一对应,包含有
  366. title:[], // 表头展示的文字
  367. key:[], // 获取数据的Key
  368. formatter: function() // 自定义设置当前数据的 传入(key, value)
  369. */
  370. setDataConver: function (obj) {
  371. var bw = this.browser();
  372. if (bw['ie'] < 9) return; // IE9以下的
  373. var data = obj['data'],
  374. ShowLabel = typeof obj['showLabel'] === 'undefined' ? true : obj['showLabel'],
  375. fileName = (obj['fileName'] || 'UserExport') + '.csv',
  376. columns = obj['columns'] || {
  377. title: [],
  378. key: [],
  379. formatter: undefined
  380. };
  381. ShowLabel = typeof ShowLabel === 'undefined' ? true : ShowLabel;
  382. var row = "",
  383. CSV = '',
  384. key;
  385. // 如果要现实表头文字
  386. if (ShowLabel) {
  387. // 如果有传入自定义的表头文字
  388. if (columns.title.length) {
  389. columns.title.map(function (n) {
  390. row += n + ',';
  391. });
  392. } else {
  393. // 如果没有,就直接取数据第一条的对象的属性
  394. for (key in data[0]) row += key + ',';
  395. }
  396. row = row.slice(0, -1); // 删除最后一个,号,即a,b, => a,b
  397. CSV += row + '\r\n'; // 添加换行符号
  398. }
  399. // 具体的数据处理
  400. data.map(function (n) {
  401. row = '';
  402. // 如果存在自定义key值
  403. if (columns.key.length) {
  404. columns.key.map(function (m) {
  405. row += '"' + (typeof columns.formatter === 'function' ? columns.formatter(m, n[m]) || n[m] || '' : n[m] || '') + '",';
  406. });
  407. } else {
  408. for (key in n) {
  409. row += '"' + (typeof columns.formatter === 'function' ? columns.formatter(key, n[key]) || n[key] || '' : n[key] || '') + '",';
  410. }
  411. }
  412. row.slice(0, row.length - 1); // 删除最后一个,
  413. CSV += row + '\r\n'; // 添加换行符号
  414. });
  415. if (!CSV) return;
  416. this.SaveAs(fileName, CSV);
  417. },
  418. SaveAs: function (fileName, csvData) {
  419. var bw = this.browser();
  420. if (!bw['edge'] || !bw['ie']) {
  421. var alink = document.createElement("a");
  422. alink.id = "linkDwnldLink";
  423. alink.href = this.getDownloadUrl(csvData);
  424. document.body.appendChild(alink);
  425. var linkDom = document.getElementById('linkDwnldLink');
  426. linkDom.setAttribute('download', fileName);
  427. linkDom.click();
  428. document.body.removeChild(linkDom);
  429. } else if (bw['ie'] >= 10 || bw['edge'] == 'edge') {
  430. var _utf = "\uFEFF";
  431. var _csvData = new Blob([_utf + csvData], {
  432. type: 'text/csv'
  433. });
  434. navigator.msSaveBlob(_csvData, fileName);
  435. } else {
  436. var oWin = window.top.open("about:blank", "_blank");
  437. oWin.document.write('sep=,\r\n' + csvData);
  438. oWin.document.close();
  439. oWin.document.execCommand('SaveAs', true, fileName);
  440. oWin.close();
  441. }
  442. },
  443. getDownloadUrl: function (csvData) {
  444. var _utf = "\uFEFF"; // 为了使Excel以utf-8的编码模式,同时也是解决中文乱码的问题
  445. if (window.Blob && window.URL && window.URL.createObjectURL) {
  446. csvData = new Blob([_utf + csvData], {
  447. type: 'text/csv'
  448. });
  449. return URL.createObjectURL(csvData);
  450. }
  451. // return 'data:attachment/csv;charset=utf-8,' + _utf + encodeURIComponent(csvData);
  452. },
  453. browser: function () {
  454. var Sys = {};
  455. var ua = navigator.userAgent.toLowerCase();
  456. var s;
  457. (s = ua.indexOf('edge') !== -1 ? Sys.edge = 'edge' : ua.match(/rv:([\d.]+)\) like gecko/)) ? Sys.ie = s[1] :
  458. (s = ua.match(/msie ([\d.]+)/)) ? Sys.ie = s[1] :
  459. (s = ua.match(/firefox\/([\d.]+)/)) ? Sys.firefox = s[1] :
  460. (s = ua.match(/chrome\/([\d.]+)/)) ? Sys.chrome = s[1] :
  461. (s = ua.match(/opera.([\d.]+)/)) ? Sys.opera = s[1] :
  462. (s = ua.match(/version\/([\d.]+).*safari/)) ? Sys.safari = s[1] : 0;
  463. return Sys;
  464. }
  465. };
  466.  
  467. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址