懒人小说下载器

通用网站内容抓取工具,可批量抓取小说、论坛内容等并保存为TXT文档

当前为 2021-12-16 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name DownloadAllContent
  3. // @name:zh-CN 懒人小说下载器
  4. // @name:zh-TW 懶人小説下載器
  5. // @name:ja 怠惰者小説ダウンロードツール
  6. // @namespace hoothin
  7. // @version 2.1
  8. // @description Fetch and download main content on current page, provide special support for chinese novel
  9. // @description:zh-CN 通用网站内容抓取工具,可批量抓取小说、论坛内容等并保存为TXT文档
  10. // @description:zh-TW 通用網站內容抓取工具,可批量抓取小說、論壇內容等並保存為TXT文檔
  11. // @description:ja ユニバーサルサイトコンテンツクロールツール、クロール、フォーラム内容など
  12. // @author hoothin
  13. // @include *
  14. // @grant GM_xmlhttpRequest
  15. // @grant GM_registerMenuCommand
  16. // @grant GM_setValue
  17. // @grant GM_getValue
  18. // @require https://cdn.jsdelivr.net/npm/file-saver@1.3.8/FileSaver.min.js
  19. // @license MIT License
  20. // @compatible chrome
  21. // @compatible firefox
  22. // @compatible opera 未测试
  23. // @compatible safari 未测试
  24. // @contributionURL https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=rixixi@sina.com&item_name=Greasy+Fork+donation
  25. // @contributionAmount 1
  26. // ==/UserScript==
  27.  
  28. (function() {
  29. 'use strict';
  30. var downThreadNum = 20;//下載綫程數
  31. var indexReg=/PART\b|Prologue|-\d+|分卷|Chapter\s*[\-_]?\d+|^序$|序\s*言|序\s*章|前\s*言|引\s*言|引\s*子|摘\s*要|楔\s*子|契\s*子|后\s*记|後\s*記|附\s*言|结\s*语|結\s*語|最終話|最终话|番\s*外|[\d|〇|零|一|二|三|四|五|六|七|八|九|十|百|千|万|萬|-]+\s*(、|)|\.\D|章|节|節|回|卷|折|篇|幕|集|话|話)/i;
  32. var innerNextPage=/下一(页|张)|next page/i;
  33. var lang = navigator.appName=="Netscape"?navigator.language:navigator.userLanguage;
  34. var i18n={};
  35. var rCats=[];
  36. switch (lang){
  37. case "zh-CN":
  38. case "zh-SG":
  39. i18n={
  40. fetch:"开始下载小说或其他【Ctrl+F9】",
  41. info:"本文是使用懒人小说下载器(DownloadAllContent)下载的",
  42. error:"该段内容获取失败",
  43. downloading:"已下载完成 %s 段,剩余 %s 段<br>正在下载 %s",
  44. complete:"已全部下载完成,共 %s 段",
  45. del:"设置文本干扰码的CSS选择器",
  46. custom:"自定义下载",
  47. customInfo:"输入网址或者章节CSS选择器",
  48. reSort:"按标题名重新排序",
  49. setting:"懒人小说下载设置",
  50. abort:"跳过此章",
  51. save:"临时保存"
  52. };
  53. break;
  54. case "zh-TW":
  55. case "zh-HK":
  56. i18n={
  57. fetch:"開始下載小說或其他【Ctrl+F9】",
  58. info:"本文是使用懶人小說下載器(DownloadAllContent)下載的",
  59. error:"該段內容獲取失敗",
  60. downloading:"已下載完成 %s 段,剩餘 %s 段<br>正在下載 %s",
  61. complete:"已全部下載完成,共 %s 段",
  62. del:"設置文本干擾碼的CSS選擇器",
  63. custom:"自定義下載",
  64. customInfo:"輸入網址或者章節CSS選擇器",
  65. reSort:"按標題名重新排序",
  66. setting:"懶人小說下載設置",
  67. abort:"跳過此章",
  68. save:"保存當前"
  69. };
  70. break;
  71. default:
  72. i18n={
  73. fetch:"Download All Content[Ctrl+F9]",
  74. info:"The TXT is downloaded by 'DownloadAllContent'",
  75. error:"Failed in downloading current chapter",
  76. downloading:"%s pages are downloaded, there are still %s pages left<br>Downloading %s ......",
  77. complete:"Completed! Get %s pages in total",
  78. del:"Set css selectors for ignore",
  79. custom:"Custom to download",
  80. customInfo:"Input urls OR sss selectors for chapter links",
  81. reSort:"ReSort by title",
  82. setting:"DownloadAllContent Setting",
  83. abort:"Abort",
  84. save:"Save"
  85. };
  86. break;
  87. }
  88. var firefox=navigator.userAgent.toLowerCase().indexOf('firefox')!=-1,curRequests=[];
  89. var rocketContent,txtDownContent,txtDownWords,txtDownQuit,txtDownDivInited=false;
  90.  
  91. function initTxtDownDiv(){
  92. if(txtDownDivInited)return;
  93. txtDownDivInited=true;
  94. rocketContent=document.createElement("div");
  95. document.body.appendChild(rocketContent);
  96. rocketContent.outerHTML=`
  97. <div id="txtDownContent" style="display: none;">
  98. <div style="width:360px;height:90px;position:fixed;left:50%;top:50%;margin-top:-25px;margin-left:-150px;z-index:100000;background-color:#ffffff;border:1px solid #afb3b6;border-radius:10px;opacity:0.95;filter:alpha(opacity=95);box-shadow:5px 5px 20px 0px #000;">
  99. <div id="txtDownWords" style="position:absolute;width:275px;max-height: 90%;border: 1px solid #f3f1f1;padding: 8px;border-radius: 10px;overflow: auto;">
  100. </div>
  101. <div id="txtDownQuit" style="width:36px;height:28px;border-radius:10px;position:absolute;right:2px;top:2px;cursor: pointer;background-color:#ff5a5a;">
  102. <span style="height:28px;line-height:28px;display:block;color:#FFF;text-align:center;font-size:20px;">╳</span>
  103. </div>
  104. <div style="position:absolute;right:0px;bottom:2px;cursor: pointer;max-width:85px">
  105. <button id="abortRequest" style="background: #008aff;border: 0;padding: 5px;border-radius: 10px;color: white;float: right;margin: 1px;height: 25px;display:none;">${getI18n('abort')}</button>
  106. <button id="tempSaveTxt" style="background: #008aff;border: 0;padding: 5px;border-radius: 10px;color: white;float: right;margin: 1px;height: 25px;">${getI18n('save')}</button>
  107. </div>
  108. </div>
  109. </div>`;
  110. txtDownContent=document.querySelector("#txtDownContent");
  111. txtDownWords=document.querySelector("#txtDownWords");
  112. txtDownQuit=document.querySelector("#txtDownQuit");
  113. txtDownQuit.onclick=function(){
  114. txtDownContent.style.display="none";
  115. txtDownContent.parentNode.removeChild(txtDownContent);
  116. };
  117. initTempSave();
  118. }
  119.  
  120. function initTempSave(){
  121. var tempSavebtn = document.getElementById('tempSaveTxt');
  122. var abortbtn = document.getElementById('abortRequest');
  123. tempSavebtn.onclick = function(){
  124. var blob = new Blob([i18n.info+"\r\n"+document.title+"\r\n\r\n"+rCats.join("\r\n\r\n")], {type: "text/plain;charset=utf-8"});
  125. saveAs(blob, document.title+".txt");
  126. }
  127. abortbtn.onclick = function(){
  128. let curRequest = curRequests.pop();
  129. if(curRequest)curRequest[1].abort();
  130. }
  131. }
  132.  
  133. function indexDownload(aEles){
  134. if(aEles.length<1)return;
  135. initTxtDownDiv();
  136. rCats=[];
  137. var insertSigns=[];
  138. // var j=0,rCats=[];
  139. var downIndex=0,downNum=0,downOnce=function(){
  140. if(downNum>=aEles.length)return;
  141. let curIndex=downIndex;
  142. let aTag=aEles[curIndex];
  143. let request=(aTag, curIndex)=>{
  144. return [curIndex,GM_xmlhttpRequest({
  145. method: 'GET',
  146. url: aTag.href,
  147. headers:{referer:aTag.href},
  148. timeout:15000,
  149. overrideMimeType:"text/html;charset="+document.charset,
  150. onload: function(result) {
  151. var doc = getDocEle(result.responseText);
  152. let nextPage=checkNextPage(doc);
  153. if(nextPage){
  154. nextPage.innerText=aTag.innerText+"\t>>";
  155. aEles.push(nextPage);
  156. let targetIndex = curIndex;
  157. for(let a=0;a<insertSigns.length;a++){
  158. let signs=insertSigns[a],breakSign=false;
  159. if(signs){
  160. for(let b=0;b<signs.length;b++){
  161. let sign=signs[b];
  162. if(sign==curIndex){
  163. targetIndex=a;
  164. breakSign=true;
  165. break;
  166. }
  167. }
  168. }
  169. if(breakSign)break;
  170. }
  171. let insertSign = insertSigns[targetIndex];
  172. if(!insertSign)insertSigns[targetIndex] = [];
  173. insertSigns[targetIndex].push(aEles.length-1);
  174. }
  175. downIndex++;
  176. downNum++;
  177. processDoc(curIndex, aTag, doc);
  178. let request=downOnce();
  179. if(request)curRequests.push(request);
  180. },
  181. onerror: function(e) {
  182. console.warn("error:");
  183. console.log(e);
  184. downIndex++;
  185. downNum++;
  186. processDoc(curIndex, aTag, null);
  187. let request=downOnce();
  188. if(request)curRequests.push(request);
  189. },
  190. ontimeout: function(e) {
  191. console.warn("timeout:");
  192. console.log(e);
  193. downIndex++;
  194. downNum++;
  195. processDoc(curIndex, aTag, null);
  196. let request=downOnce();
  197. if(request)curRequests.push(request);
  198. },
  199. })];
  200. }
  201. if(!aTag){
  202. let waitAtagReadyInterval=setInterval(function(){
  203. if(downNum>=aEles.length)clearInterval(waitAtagReadyInterval);
  204. aTag=aEles[curIndex];
  205. if(aTag){
  206. clearInterval(waitAtagReadyInterval);
  207. request(aTag, curIndex);
  208. }
  209. },1000);
  210. return null;
  211. }
  212. return request(aTag, curIndex);
  213. };
  214. function getDocEle(str){
  215. var doc = null;
  216. try {
  217. doc = document.implementation.createHTMLDocument('');
  218. doc.documentElement.innerHTML = str;
  219. }
  220. catch (e) {
  221. console.log('parse error');
  222. }
  223. return doc;
  224. }
  225. function sortInnerPage(){
  226. var pageArrs=[],maxIndex=0,i,j;
  227. for(i=0;i<insertSigns.length;i++){
  228. var signs=insertSigns[i];
  229. if(signs){
  230. for(j=0;j<signs.length;j++){
  231. var sign=signs[j];
  232. var cat=rCats[sign];
  233. rCats[sign]=null;
  234. if(!pageArrs[i])pageArrs[i]=[];
  235. pageArrs[i].push(cat);
  236. }
  237. }
  238. }
  239. for(i=pageArrs.length-1;i>=0;i--){
  240. let pageArr=pageArrs[i];
  241. if(pageArr){
  242. for(j=pageArr.length-1;j>=0;j--){
  243. rCats.splice(i+1, 0, pageArr[j]);
  244. }
  245. }
  246. }
  247. rCats = rCats.filter(function(e){return e!=null});
  248. }
  249. function processDoc(i, aTag, doc){
  250. curRequests = curRequests.filter(function(e){return e[0]!=i});
  251. rCats[i]=(aTag.innerText+"\r\n"+getPageContent(doc));
  252. txtDownContent.style.display="block";
  253. txtDownWords.innerHTML=getI18n("downloading",[downNum,(aEles.length-downNum),aTag.innerText]);
  254. if(downNum==aEles.length){
  255. txtDownWords.innerHTML=getI18n("complete",[downNum]);
  256. sortInnerPage();
  257. var blob = new Blob([i18n.info+"\r\n"+document.title+"\r\n\r\n"+rCats.join("\r\n\r\n")], {type: "text/plain;charset=utf-8"});
  258. saveAs(blob, document.title+".txt");
  259. }
  260. }
  261. for(var i=0;i<downThreadNum;i++){
  262. let request=downOnce();
  263. if(request)curRequests.push(request);
  264. if(downIndex>=aEles.length-1)break;
  265. if(downIndex<downThreadNum-1)downIndex++;
  266. }
  267.  
  268. /*for(let i=0;i<aEles.length;i++){
  269. let aTag=aEles[i];
  270. GM_xmlhttpRequest({
  271. method: 'GET',
  272. url: aTag.href,
  273. overrideMimeType:"text/html;charset="+document.charset,
  274. onload: function(result) {
  275. var doc = getDocEle(result.responseText);
  276. processDoc(i, aTag, doc);
  277. }
  278. });
  279. }*/
  280. }
  281.  
  282. function checkNextPage(doc){
  283. let aTags=doc.querySelectorAll("a"),nextPage=null;
  284. for(var i=0;i<aTags.length;i++){
  285. let aTag=aTags[i];
  286. if(innerNextPage.test(aTag.innerText) && /^http/i.test(aTag.href)){
  287. nextPage=aTag;
  288. break;
  289. }
  290. }
  291. return nextPage;
  292. }
  293.  
  294. function getPageContent(doc){
  295. if(!doc)return i18n.error;
  296. if(doc.defaultView)
  297. [].forEach.call(doc.querySelectorAll("span,div"),function(item){
  298. var thisStyle=doc.defaultView.getComputedStyle(item);
  299. if(thisStyle && (thisStyle.display=="none"||thisStyle.fontSize=="0px"))
  300. item.parentNode.removeChild(item);
  301. });
  302. var i,j,k,rStr="",pageData=(doc.body?doc.body:doc).cloneNode(true),delList=[];
  303. [].forEach.call(pageData.querySelectorAll("font.jammer"),function(item){
  304. item.parentNode.removeChild(item);
  305. });
  306. var selectors=GM_getValue("selectors");
  307. if(selectors){
  308. [].forEach.call(pageData.querySelectorAll(selectors),function(item){
  309. item.parentNode.removeChild(item);
  310. });
  311. }
  312. [].forEach.call(pageData.querySelectorAll("script,style,link,img,noscript,iframe"),function(item){delList.push(item);});
  313. [].forEach.call(delList,function(item){item.parentNode.removeChild(item);});
  314. var largestContent,contents=pageData.querySelectorAll("span,div,article,p,td"),largestNum=0;
  315. for(i=0;i<contents.length;i++){
  316. let content=contents[i],hasText=false,allSingle=true,item,curNum=0;
  317. for(j=content.childNodes.length-1;j>=0;j--){
  318. item=content.childNodes[j];
  319. if(item.nodeType==3){
  320. if(/^\s*$/.test(item.data))
  321. item.parentNode.removeChild(item);
  322. else hasText=true;
  323. }else if(/^(I|A|STRONG|B|FONT|P|DL|DD|H\d)$/.test(item.tagName))hasText=true;
  324. }
  325. for(j=content.childNodes.length-1;j>=0;j--){
  326. item=content.childNodes[j];
  327. if(item.nodeType==1 && !/^(I|A|STRONG|B|FONT|BR)$/.test(item.tagName) && /^\s*$/.test(item.innerHTML))
  328. item.parentNode.removeChild(item);
  329. }
  330. if(content.childNodes.length>1){
  331. for(j=0;j<content.childNodes.length;j++){
  332. item=content.childNodes[j];
  333. if(item.nodeType==1){
  334. for(k=0;k<item.childNodes.length;k++){
  335. var childNode=item.childNodes[k];
  336. if(childNode.nodeType!=3 && !/^(I|A|STRONG|B|FONT|BR)$/.test(childNode.tagName)){
  337. allSingle=false;
  338. break;
  339. }
  340. }
  341. if(!allSingle)break;
  342. }
  343. }
  344. }else{
  345. allSingle=false;
  346. }
  347. if(allSingle){
  348. curNum=(firefox?content.textContent.length:content.innerText.length);
  349. }else {
  350. if(!hasText)continue;
  351. if(pageData==document && content.offsetWidth<=0 && content.offsetHeight<=0)
  352. continue;
  353. [].forEach.call(content.childNodes,function(item){
  354. if(item.nodeType==3)curNum+=item.data.length;
  355. else if(/^(I|A|STRONG|B|FONT|P|DL|DD|H\d)$/.test(item.tagName))curNum+=(firefox?item.textContent.length:item.innerText.length);
  356. });
  357. }
  358. if(curNum>largestNum){
  359. largestNum=curNum;
  360. largestContent=content;
  361. }
  362. }
  363. if(!largestContent)return i18n.error;
  364. var childlist=pageData.querySelectorAll(largestContent.tagName);//+(largestContent.className?"."+largestContent.className.replace(/(^\s*)|(\s*$)/g, '').replace(/\s+/g, '.'):""));
  365. function getRightStr(ele, noTextEnable){
  366. let childNodes=ele.childNodes,cStr="\r\n",hasText=false;
  367. for(let j=0;j<childNodes.length;j++){
  368. let childNode=childNodes[j];
  369. if(childNode.nodeType==3 && childNode.data && !/^\s*$/.test(childNode.data))hasText=true;
  370. if(childNode.innerHTML){
  371. childNode.innerHTML=childNode.innerHTML.replace(/\<\s*br\s*\>/gi,"\r\n").replace(/\n+/gi,"\n").replace(/\r+/gi,"\r");
  372. }
  373. if(childNode.textContent){
  374. cStr+=childNode.textContent.replace(/ +/g," ").replace(/([^\r]|^)\n([^\r]|$)/gi,"$1\r\n$2");
  375. }
  376. if(childNode.nodeType!=3 && !/^(I|A|STRONG|B|FONT)$/.test(childNode.tagName))cStr+="\r\n";
  377. }
  378. if(hasText || noTextEnable || ele==largestContent)rStr+=cStr+"\r\n";
  379. }
  380. for(i=0;i<childlist.length;i++){
  381. var child=childlist[i];
  382. if(getDepth(child)==getDepth(largestContent)){
  383. if(!largestContent.className && child.className)continue;
  384. if((largestContent.className && largestContent.className==child.className)||largestContent.parentNode ==child.parentNode){
  385. getRightStr(child, true);
  386. }else {
  387. getRightStr(child, false);
  388. }
  389. }
  390. }
  391. return rStr;
  392. }
  393.  
  394. function getI18n(key, args){
  395. var resultStr=i18n[key];
  396. if(args && args.length>0){
  397. args.forEach(function(item){
  398. resultStr=resultStr.replace(/%s/,item);
  399. });
  400. }
  401. return resultStr;
  402. }
  403.  
  404. function getDepth(dom){
  405. var pa=dom,i=0;
  406. while(pa.parentNode){
  407. pa=pa.parentNode;
  408. i++;
  409. }
  410. return i;
  411. }
  412.  
  413. function fetch(){
  414. var aEles=document.querySelectorAll("a"),list=[];
  415. for(var i=0;i<aEles.length;i++){
  416. var aEle=aEles[i],has=false;
  417. for(var j=0;j<list.length;j++){
  418. if(list[j].href==aEle.href){
  419. list.splice(j,1);
  420. list.push(aEle);
  421. has=true;
  422. break;
  423. }
  424. }
  425. if(!has && aEle.href && /^http/i.test(aEle.href) && (indexReg.test(aEle.innerText) || /chapter[\-_]?\d/.test(aEle.href))){
  426. list.push(aEle);
  427. }
  428. }
  429. if(list.length>2){
  430. if(GM_getValue("contentSort")){
  431. list.sort(function(a,b){
  432. return parseInt(a.innerText.replace(/[^0-9]/ig,"")) - parseInt(b.innerText.replace(/[^0-9]/ig,""));
  433. });
  434. }
  435. indexDownload(list);
  436. }else{
  437. var blob = new Blob([i18n.info+"\r\n"+document.title+"\r\n\r\n"+getPageContent(document)], {type: "text/plain;charset=utf-8"});
  438. saveAs(blob, document.title+".txt");
  439. }
  440. }
  441.  
  442. document.addEventListener("keydown", function(e) {
  443. if(e.keyCode == 120 && e.ctrlKey) {
  444. fetch();
  445. }
  446. });
  447. function setDel(){
  448. var selValue=GM_getValue("selectors");
  449. var selectors=prompt(i18n.del,selValue?selValue:"");
  450. GM_setValue("selectors",selectors);
  451. if(window.confirm(i18n.reSort)){
  452. GM_setValue("contentSort", true);
  453. }else{
  454. GM_setValue("contentSort", false);
  455. }
  456. }
  457. function customDown(){
  458. var urls=window.prompt(i18n.customInfo,"https://xxx.xxx/book-[20-99].html, https://xxx.xxx/book-[01-10].html");
  459. if(urls){
  460. var processEles=[];
  461. if(/^http|^ftp/.test(urls)){
  462. [].forEach.call(urls.split(","),function(i){
  463. var varNum=/\[\d+\-\d+\]/.exec(i)[0].trim();
  464. var num1=/\[(\d+)/.exec(varNum)[1].trim();
  465. var num2=/(\d+)\]/.exec(varNum)[1].trim();
  466. var num1Int=parseInt(num1);
  467. var num2Int=parseInt(num2);
  468. var numLen=num1.length;
  469. var needAdd=num1.charAt(0)=="0";
  470. if(num1Int>=num2Int)return;
  471. for(var j=num1Int;j<=num2Int;j++){
  472. var urlIndex=j.toString();
  473. if(needAdd){
  474. while(urlIndex.length<numLen)urlIndex="0"+urlIndex;
  475. }
  476. var curUrl=i.replace(/\[\d+\-\d+\]/,urlIndex).trim();
  477. var curEle=document.createElement("a");
  478. curEle.href=curUrl;
  479. processEles.push(curEle);
  480. curEle.innerText=processEles.length.toString();
  481. }
  482. });
  483. }else{
  484. let urlsArr=urls.split("@@");
  485. [].forEach.call(document.querySelectorAll(urlsArr[0]),function(item){
  486. let has=false;
  487. for(var j=0;j<processEles.length;j++){
  488. if(processEles[j].href==item.href){
  489. processEles.splice(j,1);
  490. processEles.push(item);
  491. has=true;
  492. break;
  493. }
  494. }
  495. if(!has && item.href && /^http/i.test(item.href)){
  496. processEles.push(item.cloneNode(1));
  497. }
  498. });
  499. if(urlsArr.length>1){
  500. processEles.forEach(ele=>{
  501. ele.href=ele.href.replace(new RegExp(urlsArr[1]), urlsArr[2]);
  502. });
  503. }
  504. }
  505. if(GM_getValue("contentSort")){
  506. processEles.sort(function(a,b){
  507. return parseInt(a.innerText.replace(/[^0-9]/ig,"")) - parseInt(b.innerText.replace(/[^0-9]/ig,""));
  508. });
  509. }
  510. indexDownload(processEles);
  511. }
  512. }
  513. GM_registerMenuCommand(i18n.fetch, fetch);
  514. GM_registerMenuCommand(i18n.custom, customDown);
  515. GM_registerMenuCommand(i18n.setting, setDel);
  516. })();