豆瓣日记导出工具

将豆瓣日记导出为 markdown

  1. // ==UserScript==
  2. // @name 豆瓣日记导出工具
  3. // @namespace https://www.douban.com/people/MoNoMilky/
  4. // @version 0.2.1
  5. // @description 将豆瓣日记导出为 markdown
  6. // @author Bambooom
  7. // @icon https://www.google.com/s2/favicons?domain=douban.com
  8. // @match https://www.douban.com/people/*/notes*
  9. // @match https://www.douban.com/people/*
  10. // @require https://unpkg.com/dexie@latest/dist/dexie.js
  11. // @require https://unpkg.com/turndown/dist/turndown.js
  12. // @require https://unpkg.com/jszip@3.2.0/dist/jszip.min.js
  13. // @require https://unpkg.com/file-saver@2.0.0-rc.2/dist/FileSaver.min.js
  14. // @grant none
  15. // ==/UserScript==
  16.  
  17. (function () {
  18. 'use strict';
  19. /* global $, Dexie, TurndownService, JSZip, saveAs */
  20.  
  21. var tdService = new TurndownService({
  22. headingStyle: 'atx', // use # for header
  23. hr: '---',
  24. codeBlockStyle: 'fenced',
  25. bulletListMarker: '-',
  26. });
  27.  
  28. // <mark>, keep this html since no corresponding format in markdown
  29. tdService.keep(['mark']);
  30.  
  31. // span font-weight:bold => **content**
  32. tdService.addRule('bold', {
  33. filter: function(node) {
  34. return (
  35. node.nodeName === 'SPAN' &&
  36. node.style[0] &&
  37. node.style[0] === 'font-weight' &&
  38. node.style.fontWeight === 'bold'
  39. );
  40. },
  41. replacement: function (content) {
  42. return '**' + content + '** '; // add one more space in the end as in Chinese if no space, the markdown syntax is not rendered correctly
  43. },
  44. });
  45.  
  46. // span text-decoration:line-through => ~~content~~
  47. tdService.addRule('line-through', {
  48. filter: function(node) {
  49. return (
  50. node.nodeName === 'SPAN' &&
  51. node.style[0] &&
  52. node.style[0].startsWith('text-decoration') &&
  53. node.style.textDecoration === 'line-through'
  54. );
  55. },
  56. replacement: function (content) {
  57. return '~~' + content + '~~ '; // add one more space in the end
  58. },
  59. });
  60.  
  61. // highlight-block 文本区块 => ref block `>`
  62. tdService.addRule('hilite- block', {
  63. filter: function(node) {
  64. return (
  65. node.nodeName === 'DIV' &&
  66. node.className === 'highlight-block'
  67. );
  68. },
  69. replacement: function (content) {
  70. return '> ' + content;
  71. },
  72. });
  73.  
  74. // introduction 导语 => ref block `>`
  75. tdService.addRule('introduction', {
  76. filter: function(node) {
  77. return (
  78. node.nodeName === 'DIV' &&
  79. node.className === 'introduction'
  80. );
  81. },
  82. replacement: function (_, node) {
  83. return '> ' + node.textContent + '\n\n';
  84. },
  85. });
  86.  
  87. // handle video => make it to link format
  88. tdService.addRule('video', {
  89. filter: function(node) {
  90. return (
  91. node.nodeName === 'DIV' &&
  92. node.className === 'video-player-iframe'
  93. );
  94. },
  95. replacement: function(_, node) {
  96. var link = node.children[0].getAttribute('src'); // first child is iframe, src it the the video link
  97. var title = node.children[1].textContent.trim(); // second child is video-title
  98. return '[' + title + '](' + link + ')';
  99. },
  100. });
  101.  
  102. // movie/music/book/... item card => link format now
  103. tdService.addRule('subject', {
  104. filter: function(node) {
  105. return (
  106. node.nodeName === 'DIV' &&
  107. node.className === 'subject-wrapper'
  108. );
  109. },
  110. replacement: function(_, node) {
  111. var link = node.children[0].getAttribute('href'); // item link
  112. var title = node.querySelector('.subject-title').textContent.trim(); // item title
  113. return '[' + title + '](' + link + ')';
  114. },
  115. });
  116.  
  117. // item caption => ref
  118. tdService.addRule('subject-caption', {
  119. filter: function(node) {
  120. return (
  121. node.nodeName === 'DIV' &&
  122. node.className === 'subject-caption'
  123. );
  124. },
  125. replacement: function(content) {
  126. return '> ' + content;
  127. },
  128. });
  129.  
  130. var people;
  131.  
  132. if (location.href.indexOf('//www.douban.com/people/') > -1) {
  133. // 加入导出按钮
  134. let match = location.href.match(/www\.douban\.com\/people\/([^/]+)\//);
  135. people = match ? match[1] : null;
  136. $('#note h2 .pl a:last').after('&nbsp;·&nbsp;<a href="https://www.douban.com/people/' + people + '/notes?start=0&type=note&export=1">导出</a>');
  137. }
  138.  
  139. if (people && location.href.indexOf('//www.douban.com/people/' + people + '/notes') > -1 && location.href.indexOf('export=1') > -1) {
  140. init();
  141. }
  142.  
  143. function escapeQuote(str) {
  144. return str.replaceAll('"', '""'); // " need to be replaced with two quotes to escape inside csv quoted string
  145. }
  146.  
  147. async function init() {
  148. const db = new Dexie('db_notes_export'); // init indexedDB
  149. db.version(1).stores({
  150. notes: '++id, title, datetime, linkid, md',
  151. });
  152.  
  153. const notes = await getCurPageNotes();
  154. db.notes.bulkAdd(notes).then(function () {
  155. console.log('添加成功+', notes.length);
  156.  
  157. let nextPageLink = $('.paginator span.next a').attr('href');
  158. if (nextPageLink) {
  159. nextPageLink = nextPageLink + '&export=1';
  160. window.location.href = nextPageLink;
  161. } else {
  162. exportAll();
  163. }
  164. }).catch(function (error) {
  165. console.error("Ooops: " + error);
  166. });
  167. }
  168.  
  169. // https://gist.github.com/jwilson8767/db379026efcbd932f64382db4b02853e
  170. function noteReady(noteId) {
  171. const selector = '#note_' + noteId + '_full .note';
  172. return new Promise((resolve, reject) => {
  173. let el = document.querySelector(selector);
  174. if (el) { resolve(el); }
  175.  
  176. new MutationObserver((_, observer) => {
  177. // Query for elements matching the specified selector
  178. Array.from(document.querySelectorAll(selector)).forEach((element) => {
  179. resolve(element);
  180. //Once we have resolved we don't need the observer anymore.
  181. observer.disconnect();
  182. });
  183. })
  184. .observe(document.documentElement, {
  185. childList: true,
  186. subtree: true
  187. });
  188. });
  189. }
  190.  
  191. async function getCurPageNotes() {
  192. var notes = [];
  193. var elems = $('.note-container[id^="note-"]').get();
  194.  
  195. // 展开全文
  196. var toggles = document.querySelectorAll('.note-header-container .rr a.a_unfolder_n');
  197. Array.from(toggles).forEach(toggle => {
  198. toggle.click();
  199. });
  200.  
  201. for (let i = 0; i < elems.length; i++) {
  202. var note = elems[i];
  203. var id = note.id.match(/note-(\d+)$/);
  204. id = id[1];
  205. var title = escapeQuote(note.querySelector('.note-header-container h3 > a').textContent.trim());
  206. var datetime = note.querySelector('.note-header-container .pub-date').textContent;
  207.  
  208. await noteReady(id);
  209. var notedom = note.querySelector('#note_' + id + '_full .note');
  210. var md = tdService.turndown(notedom);
  211. notes.push({
  212. title,
  213. datetime, // like '2021-07-10 00:29:36'
  214. linkid: id,
  215. md,
  216. });
  217. }
  218.  
  219. return notes;
  220. }
  221.  
  222. function exportAll() {
  223. const db = new Dexie('db_notes_export');
  224. db.version(1).stores({
  225. notes: '++id, title, datetime, linkid, md',
  226. });
  227.  
  228. var zip = new JSZip();
  229.  
  230. db.notes.toArray().then(function (all) {
  231. all.map(function(item) {
  232. delete item.id;
  233. var date = item.datetime.split(' ')[0];
  234. var frontmatter = '---\n'
  235. + 'layout: post\n'
  236. + 'title: ' + item.title + '\n'
  237. + 'date: ' + date + '\n' // keep date only
  238. + 'disqus: y\n---\n\n'
  239. + 'original link: https://www.douban.com/note/' + item.linkid + '/\n\n';
  240. zip.file(date + '-' + item.title + '.md', frontmatter + item.md);
  241. });
  242.  
  243. zip.generateAsync({type:"blob"}).then(function(content) {
  244. saveAs(content, 'douban-notes-' + new Date().toISOString().split('T')[0].replaceAll('-', '') + '.zip');
  245. });
  246.  
  247. db.delete();
  248. });
  249. }
  250.  
  251. })();

QingJ © 2025

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