实现功能

  1. 添加按钮
    1. 获取想要添加的位置
    2. 画按钮
  2. 提取知识库页面的标题和超链接
    1. 支持筛选标题 / 超链接
    2. 筛选使用for的情况须有:break或continue
  3. 给筛选后超链接
    1. 追加拼接参数“/markdown?attachment=true&latexcode=false&anchor=true&linebreak=true”拼接为指定类型资源链接
  4. 将3中的URL数据导出超链接数据到本地文件(要求为一行一个的txt文件)
  5. 下载3中指定类型资源(参考《前端下载文件的几种方式》

    JavaScript脚本

    ```javascript // ==UserScript== // @name 😎建瓯最坏🐷导出指定语雀知识库指定文档的标题和链接并下载MarkDown // @namespace https:// www.yuque.com/jianouzuihuai // @version 22.05.20 // @description 通过油猴脚本导出语雀知识库中带锚点和换行的MarkDown文件 // @author 😎建瓯最坏🐷 // @include .yuque.com/lwb6kc/ // @exclude .yuque.com/// // @icon https:// cdn.nlark.com/yuque/0/2021/png/1632223/1635923818567-7f54e0a1-144e-4fd0-9856-a24e5b2664c5.png // ==/UserScript==

(function() { “use strict”

  1. setTimeout(function()
  2. {
  3. // alert("设置按钮")
  4. var buttonExport = document.createElement("button"); // 创建一个按钮
  5. buttonExport.textContent = "🖨️ 导出知识库 🖨️"; // 按钮内容
  6. buttonExport.style.width = '138px'; // 按钮宽度
  7. buttonExport.style.height = '32px'; // 按钮高度
  8. buttonExport.style.marginLeft = '16px'; // 距离左边16px
  9. buttonExport.style.color = '#BA7CFF'; // 按钮文字颜色
  10. buttonExport.style.border = '2px #BA7CFF solid'; // 边框
  11. buttonExport.style.font = 'bold'; // 文字加粗
  12. buttonExport.style.background = '#EEC6FF'; // 按钮底色
  13. buttonExport.style.borderRadius = '4px'; // 按钮四个角弧度
  14. var eleButton = document.getElementsByClassName("favor-wrapper")[0]
  15. eleButton.appendChild(buttonExport)
  16. // 过滤的数组
  17. var list2Filter = []
  18. // 保留的数组
  19. var listLinkReport = []
  20. // 当前知识库链接
  21. var strBaseURI = document.baseURI
  22. // 定位到文档树形列表中
  23. var eleTreeList = document.getElementsByClassName("ant-tree-list-holder-inner")[0]
  24. // 文档树形列表中的<a>标签
  25. var arrTagA = eleTreeList.getElementsByTagName('a')
  26. var nTagA = arrTagA.length
  27. var iTitle, iLink
  28. var arrReport = []
  29. // 遍历子元素
  30. for(var i = 0; i < nTagA; i++)
  31. {
  32. var iTagA = arrTagA[i]
  33. // 2. 无“target”属性
  34. // var bTarget = iTagA.getAttribute('target')
  35. // if (bTarget == null)
  36. // or 有“draggable” / “title”属性 的<a>标签
  37. var bTitle = iTagA.getAttribute('title')
  38. if (bTitle)
  39. {
  40. // 获取报错<a>标签中的“title”属性
  41. iTitle = iTagA.getAttribute('title')
  42. if (iTitle.indexOf("#") != -1)
  43. {
  44. // 井号会导致导出的数据被截断
  45. iTitle = iTitle.replace("#", "-")
  46. }
  47. // 获取报错<a>标签中的“href”属性
  48. iLink = iTagA.getAttribute('href')
  49. arrReport.push([iTitle, iLink])
  50. }
  51. }
  52. /*
  53. console.log(listLinkReport)
  54. console.log(list2Filter)
  55. */
  56. // 遍历链接进行筛选
  57. arrReport.forEach(FilterLinks)
  58. // 按键点击事件
  59. buttonExport.onclick = function ()
  60. {
  61. // 指定下载组织列表
  62. var arrAPT = ['APT32', 'APT37', 'DarkHotel', 'Kimsuky', 'Lazarus', '白象', '毒云藤', '蓝宝菇', '蔓灵花', '旺刺', '响尾蛇']
  63. var nLink = listLinkReport.length
  64. var nAPT = arrAPT.length
  65. // ——————————导出文件——————————
  66. // Data2File("所有APT报告的标题和URL.csv", listLinkReport)
  67. // Data2File("过滤的URL.csv", list2Filter)
  68. // ——————————下载文件——————————
  69. var iTitle, iURL, iExport
  70. var listLink2Down = []
  71. for(var d = 0; d < nLink; d++)
  72. {
  73. iTitle = listLinkReport[d][0]
  74. iURL = listLinkReport[d][1]
  75. // 遍历是否是下载列表中的组织
  76. Loop2FilterAPT2DownLoad:
  77. for(var p = 0; p < nAPT; p++)
  78. {
  79. var strAPT = arrAPT[p]
  80. if (iTitle.startsWith(strAPT) == true)
  81. {
  82. listLink2Down.push([iTitle, iURL])
  83. // console.log(iTitle, "\r\n", iURL)
  84. iExport = iURL + "/markdown?attachment=true&latexcode=false&anchor=true&linebreak=true"
  85. // console.log("下载链接:", iExport)
  86. // 下载代码
  87. // window.open(iExport)// 弹窗体验感不好
  88. // setTimeout(window.location.href = iExport, 999)
  89. // window.location.href = iExport// 多文件不延时下载会重置
  90. // selfSleep(999)
  91. break Loop2FilterAPT2DownLoad
  92. }
  93. }
  94. }
  95. Data2File("11个APT报告的标题和URL.csv", listLink2Down)
  96. }
  97. // alert("脚本结束")
  98. function FilterLinks(iArr)
  99. {
  100. /*
  101. for 过滤
  102. 1. if 过滤
  103. 1.1 for 保留
  104. 1.1.1 if (保留){保留后break outer}
  105. 1.1.2 if (不保留){不保留后break outer}
  106. 2. if 不过滤
  107. 2.1 if (次数和数组个数一样){保留}
  108. */
  109. var iTitle = iArr[0]
  110. var iHref = iArr[1]
  111. // 排除的文章
  112. var arrURI2Filter = ['toc?tempStore=1', 'toc', 'introduce']
  113. // 排除的字符串(前缀)
  114. var arrStr2Filter = ['area', 'group', 'theme', 'summary', 'classify']
  115. // 不排除的字符串(前缀)
  116. var arrStr2Save = ['theme_', 'summary_']
  117. // 取最后一个“/”后的字符串
  118. var arrSplit = iHref.split("/");
  119. var strURI = arrSplit[arrSplit.length-1];
  120. if (strURI.length)
  121. {
  122. // alert("切分后元素:" + strURI)
  123. var strURL = strBaseURI + "/" + strURI;
  124. // URI字符串是否在过滤【文章】数组中
  125. var bURIinArr = (arrURI2Filter.indexOf(strURI) != -1)
  126. if (bURIinArr)
  127. {
  128. // 在字符串数组中,直接过滤
  129. list2Filter.push([iTitle, strURL])
  130. }
  131. else
  132. {
  133. // 不在,继续判断
  134. // 是否在【过滤前缀】数组中
  135. var bInArry = 0
  136. var nFilterStr = arrStr2Filter.length
  137. Loop2Filter:
  138. // 排除的字符串(前缀)
  139. for(var i = 0; i < nFilterStr; i++)
  140. {
  141. var iStr2Filter = arrStr2Filter[i]
  142. if (strURI.startsWith(iStr2Filter))
  143. {
  144. // 如果前缀需要过滤
  145. // 继续判断是否在【不过滤前缀】数组中
  146. var nSaveStr = arrStr2Save.length
  147. var bInArrySave = 0
  148. Loop2Save:
  149. for(var j = 0; j < nSaveStr; j++)
  150. {
  151. var iStr2Save = arrStr2Save[j]
  152. if (strURI.startsWith(iStr2Save))
  153. {
  154. // 保留的路径
  155. listLinkReport.push([iTitle, strURL])
  156. // 跳出外层循环(outer改为Loop)
  157. break Loop2Filter
  158. }
  159. else
  160. {
  161. bInArrySave += 1
  162. }
  163. }
  164. if (bInArrySave == arrStr2Save.length)
  165. {
  166. // 过滤的路径
  167. list2Filter.push([iTitle, strURL])
  168. // 跳出外层循环(outer改为Loop)
  169. break Loop2Filter
  170. }
  171. }
  172. else
  173. {
  174. // 不匹配次数
  175. bInArry += 1
  176. }
  177. // 不匹配次数和数组元素个数一致时,才是需要保留的URL
  178. if (bInArry == nFilterStr)
  179. {
  180. // 保留的路径
  181. listLinkReport.push([iTitle, strURL])
  182. }
  183. }
  184. }
  185. }
  186. }
  187. }, 1666)// 1.666秒后执行
  188. //自定义js延时
  189. function selfSleep(nTime)
  190. {
  191. var timeNow = Date.now();
  192. while((Date.now() - timeNow) <=parseInt(nTime)){};
  193. }
  194. // 数据保存为文件
  195. function Data2File(filename, data)
  196. {
  197. var data2Write = data.join("\n")
  198. var elementSaveFile = document.createElement('a')
  199. /*
  200. 以Json格式
  201. // elementSaveFile.setAttribute('href', 'data:text/plaincharset=utf-8, ' + JSON.stringify(data))
  202. 以CSV格式
  203. elementSaveFile.setAttribute('href', 'data:text/csv;charset=utf-8,' + data2Write)
  204. */
  205. // 以原格式(数组)
  206. elementSaveFile.setAttribute('href', 'data:text/plaincharset=utf-8, ' + data2Write)
  207. // alert("导出文件开始")
  208. // 利用A标签的download属性完成下载文件功能
  209. elementSaveFile.setAttribute('download', filename)
  210. // 设置为不显示的元素
  211. elementSaveFile.style.display = 'none'
  212. // 添加A标签元素
  213. document.body.appendChild(elementSaveFile)
  214. // 触发A标签元素的点击事件
  215. elementSaveFile.click()
  216. // A标签元素利用后释放
  217. document.body.removeChild(elementSaveFile)
  218. // alert("导出文件结束")
  219. }
  220. }

)() ``` 建议先从单个导出开始写:
JavaScript - 通过油猴插件添加脚本功能给指定域的URL添加参数 | 一键语雀(单个)文档导出

运行效果

image.png

升级

22.03.25

为了能导出别人的知识库,修改脚本执行页面的“@include/@exclude/@match”。
由于在别人的知识库处,无ClassName = “doc-*”的元素,改为在客主知识库页面都有的收藏按键“favor-wrapper”旁。
去掉按钮旁边的边界“.style.border = 'none'”:
image.png
去掉前:
image.png

22.03.28

越来越漂亮噜🤗🤗🤗
image.png