cejs
Version:
A JavaScript module framework that is simple to use.
317 lines (279 loc) • 10.9 kB
JavaScript
/**
* @name CeL module for downloading syosetu.com novels.
*
* @fileoverview 本檔案包含了批量下載小説家になろう/小説を読もう!的工具。
*
* TODO: なろう小説API https://dev.syosetu.com/man/api/
*
* <code>
CeL.yomou().start(work_id);
</code>
*
* @see 小説投稿サイト https://matome.naver.jp/odai/2139450042041120001
* http://www.akatsuki-novels.com/novels/ranking_total
* http://www.mai-net.net/bbs/sst/sst.php?act=list&cate=all&page=1
* https://github.com/whiteleaf7/narou https://github.com/59naga/naroujs
* https://github.com/59naga/scrape-narou
*
* @since 2017/2/22 0:18:34 模組化。
*/
// More examples:
// @see https://github.com/kanasimi/work_crawler/blob/master/yomou.js
// @see https://github.com/kanasimi/work_crawler/blob/master/noc.js
;
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.yomou',
require : 'application.net.work_crawler.'
//
+ '|application.storage.EPUB.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
var default_configuration = {
// auto_create_ebook, automatic create ebook
// MUST includes CeL.application.locale!
need_create_ebook : true,
// recheck:從頭檢測所有作品之所有章節與所有圖片。不會重新擷取圖片。對漫畫應該僅在偶爾需要從頭檢查時開啟此選項。default:false
// recheck='changed': 若是已變更,例如有新的章節,則重新下載/檢查所有章節內容。否則只會自上次下載過的章節接續下載。
recheck : 'changed',
site_name : '小説を読もう!',
base_URL : 'https://yomou.syosetu.com/',
novel_base_URL : 'https://ncode.syosetu.com/',
// 解析 作品名稱 → 作品id get_work()
search_URL : 'search.php?order=hyoka&word=',
parse_search_result : function(html, get_label) {
// console.log(html);
var id_data = [],
// {Array}id_list = [id,id,...]
id_list = [],
// 2019/6/24 目前僅有 ミッドナイトノベルズ 採用這個 header
header = '<article class="search_novel">';
if (!html.includes(header))
header = '<div class="novel_h">';
html.each_between(header, '</a>', function(text) {
id_list.push(text
.between(' href="' + this.novel_base_URL, '/"'));
id_data.push(get_label(text.between('/">')));
}, this);
return [ id_list, id_data ];
},
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
return this.novel_base_URL + 'novelview/infotop/ncode/' + work_id
+ '/';
},
parse_work_data : function(html, get_label) {
var work_data = Object.create(null);
html.between('<table', '<div id="ad_s_box">')
//
.each_between('<tr>', '</tr>', function(text) {
work_data[get_label(text.between('<th', '</th>').between('>'))]
//
= get_label(text.between('<td', '</td>').between('>'));
});
// console.log(work_data);
work_data = Object.assign({
// 必要屬性:須配合網站平台更改。
title : get_label(html.between('dc:title="', '"')),
// 選擇性屬性:須配合網站平台更改。
// e.g., 连载中, 連載中
// <span id="noveltype">完結済</span>全1部
// <span id="noveltype_notend">連載中</span>全1部
status : [ html.between('<span id="noveltype', '<')
.between('>') ],
author : work_data.作者名,
last_update : work_data.最終話掲載日 || work_data.掲載日,
description : work_data.あらすじ
}, work_data);
// 此兩者為未分割的字串。
if (work_data.ジャンル)
work_data.genre = work_data.ジャンル.split(/\s+/);
if (work_data.キーワード) {
// No キーワード:
// https://novel18.syosetu.com/novelview/infotop/ncode/n3731fh/
work_data.tags = work_data.キーワード.split(/\s+/);
}
return work_data;
},
// 對於章節列表與作品資訊分列不同頁面(URL)的情況,應該另外指定.chapter_list_URL。
chapter_list_URL : function(work_id) {
return this.novel_base_URL + work_id + '/';
},
get_chapter_list : function get_chapter_list(work_data, html) {
// TODO: 對於單話,可能無目次。
if (!work_data.next_chapter_list_NO)
work_data.chapter_list = [];
html.between('<div class="index_box">', '<div id="novel_footer">')
//
.each_between('<dl class="novel_sublist2">', '</dl>',
//
function(text) {
var matched = text.match(
// [ , href, inner ]
/ href="\/[^\/]+\/([^ "<>]+)[^<>]*>(.+?)<\/a>/);
if (!matched) {
throw text;
}
var chapter_data = {
url : matched[1].replace(/^\.\//, ''),
// 掲載日
date : [ text.match(/>\s*(2\d{3}[年\/][^"<>]+?)</)[1]
//
.to_Date({
zone : work_data.time_zone
}) ],
title : matched[2]
};
if (matched = text.match(/ title="(2\d{3}[年\/][^"<>]+?)改稿"/)) {
chapter_data.date.push(matched[1].to_Date({
zone : work_data.time_zone
}) || matched[1]);
}
work_data.chapter_list.push(chapter_data);
// console.log(chapter_data);
});
if (work_data.chapter_list.length === 0
&& html.includes('<div id="novel_honbun"')) {
// 短編小説
work_data.chapter_list.push({
url : this.chapter_list_URL(work_data.id),
// 掲載日
date : [ work_data.last_update.to_Date({
zone : work_data.time_zone
}) ],
title : work_data.title
});
}
// 2024/1/31 之前改版
var matched = html.between('<div class="novelview_pager">',
'</div>');
matched = matched && matched
// <a href="/n2710db/?p=3" class="novelview_pager-next">次へ</a>
.match(/<a href="([^"]+)" class="novelview_pager-next">次へ/);
if (matched) {
work_data.next_chapter_list_URL = this.novel_base_URL
+ matched[1].replace(/^\//, '');
}
},
// 取得每一個章節的各個影像內容資料。 get_chapter_data()
chapter_URL : function(work_data, chapter_NO) {
var url = work_data.chapter_list[chapter_NO - 1].url;
if (url.includes('://')) {
// e.g., 短編小説
return url;
}
return this.chapter_list_URL(work_data.id) + url;
},
// 檢測所取得內容的章節編號是否相符。
check_chapter_NO : [ '<div id="novel_no">', '/' ],
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
var
/** {Number}未發現之index。 const: 基本上與程式碼設計合一,僅表示名義,不可更改。(=== -1) */
NOT_FOUND = ''.indexOf('_'),
//
text = html.between('<div id="novel_color">',
'</div><!--novel_color-->'),
//
index = text.indexOf('<div id="novel_p"');
if (index === NOT_FOUND
//
&& (index = text.indexOf('<div id="novel_honbun"')) === NOT_FOUND) {
// gettext_config:{"id":"unable-to-parse-chapter-data-and-get-chapter-content-text"}
throw new Error('無法解析章節資料並取得章節內容文字!');
}
text = text.slice(index);
text = text.between(null, {
// 後半段的"次の話"
tail : '<div class="novel_bn">'
}) || text;
var links = [], ebook = work_data[this.KEY_EBOOK];
text.each_between('<a ', '</a>', function(text) {
var matched = text.match(/(?:^| )href="([^"<>]+)"/);
// @see https://ncode.syosetu.com/n8611bv/49/
// e.g., <a href="https://11578.mitemin.net/i00000/"
if (matched && matched[1].includes('.mitemin.net')) {
// 下載mitemin.net的圖片
links.push(matched[1]);
}
});
links.forEach(function(url) {
// 登記有url正處理中,須等待。
ebook.downloading[url] = url;
library_namespace.get_URL(url, function(XMLHttp) {
delete ebook.downloading[url];
if (!XMLHttp || !XMLHttp.responseText) {
return;
}
var matched = XMLHttp.responseText
.match(/<a href="([^"<>]+)" target="_blank">/);
if (matched) {
// 因為.add()會自動添加.downloading並在事後檢查.on_all_downloaded,因此這邊不用再檢查。
ebook.add({
url : matched[1]
});
} else {
library_namespace.error({
// gettext_config:{"id":"unable-to-extract-image-url-from-link-in-chapter-content-$1"}
T : [ '無法從章節內容中之連結抽取出圖片網址:%1', url ]
});
}
});
});
var series_title = get_label(
//
html.between('<p class="series_title">', '</p>'));
if (series_title) {
ebook.set([ {
meta : null,
name : "calibre:series",
content : series_title = get_label(series_title)
} ]);
}
this.add_ebook_chapter(work_data, chapter_NO, {
title : html.between('<p class="chapter_title">', '</p>')
// 短編小説
|| series_title,
sub_title : html.between('<p class="novel_subtitle">', '</p>')
// 短編小説
|| html.between('<p class="novel_title">', '</p>'),
text : text
});
}
};
// --------------------------------------------------------------------------------------------
function new_syosetu_crawler(configuration) {
if (configuration && configuration.isR18) {
// 放在這邊,避免覆蓋原設定。
if (!configuration.novel_base_URL)
configuration.novel_base_URL = 'https://novel18.syosetu.com/';
configuration.search_URL = configuration.search_URL
// 解析 作品名稱 → 作品id get_work()
// search/search/search.php hyoka: 総合ポイントの高い順 総合評価の高い順
|| 'search/search/?order=hyoka&word=';
}
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
// 每次呼叫皆創建一個新的實體。
var crawler = new library_namespace.work_crawler(configuration);
if (crawler.isR18) {
// for なろうの関連サイト/R-18サイト 年齢確認
// https://static.syosetu.com/sub/nl/view/js/event/redirect_ageauth.js
crawler.setup_value('cookie', 'over18=yes');
}
if (false) {
crawler.data_of(work_id, function(work_data) {
console.log(work_data);
});
}
return crawler;
}
return new_syosetu_crawler;
}