v1.0 du site web

This commit is contained in:
22107988t
2023-09-25 13:27:24 +02:00
parent 20cb812095
commit a94f68f22a
2787 changed files with 864804 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
= CeJS 網路作品爬蟲程式庫 =
批量下載網路作品(小說、漫畫)的函式庫。 WWW work crawler library.
More examples: See [https://github.com/kanasimi/work_crawler 各網站工具檔.js]
== 下載作業流程 ==
[[../work_crawler.js]]: [[arguments.js]]
→ [[task.js]]
→ [[search.js]]
→ [[work.js]]
→ [[chapter.js]]
→ [[image.js]] or [[ebook.js]]
# 獲取伺服器列表。 start_downloading()
# 解析設定檔,判別所要下載的作品列表。 parse_work_id(), get_work_list(), .base_URL, .extract_work_id()
# 特別處理特定id。 .convert_id()
# 解析 作品名稱 → 作品id get_work(), .search_URL, .parse_search_result()
# 獲取作品資訊與各章節資料。 get_work_data(), pre_process_chapter_list_data(), process_chapter_list_data()
# 對於章節列表與作品資訊分列不同頁面(URL)的情況,應該另外指定 .chapter_list_URL。 get_work_data(), .work_URL, .parse_work_data(), chapter_list_URL, .get_chapter_list(), .after_get_work_data()
# 獲取每一個章節的內容與各個影像資料。 pre_get_chapter_data(), .chapter_URL, get_chapter_data(), .pre_parse_chapter_data(), .parse_chapter_data()
# 獲取各個章節的每一個影像內容。 get_image(), .image_preprocessor(), .image_post_processor(), .after_get_image()
# finish_up(), .after_download_chapter(), .after_download_work()
== History ==
{| class="wikitable"
|+ History 沿革
! Date !! Modify
|-
| 2016/10/30 21:40:6 || 完成主要架構設計與構思,開始撰寫程式。
|-
| 2016/11/1 23:15:16 || 正式運用:批量下載腾讯漫画 qq。
|-
| 2016/11/5 22:44:17 || 正式運用:批量下載漫画台 manhuatai。
|-
| 2016/11/27 19:7:2 || 模組化。 ([[sites]]/*)
|-
| 2019/10/13 13:23:25 || 分拆至 work_crawler/*.js
|}
== See also ==
* https://github.com/abc9070410/JComicDownloader
* http://pxer.pea3nut.org/md/use https://github.com/eight04/ComicCrawler
* https://github.com/riderkick/FMD https://github.com/yuru-yuri/manga-dl
* https://github.com/Xonshiz/comic-dl
* https://github.com/wellwind/8ComicDownloaderElectron
* https://github.com/inorichi/tachiyomi
* https://github.com/Arachnid-27/Cimoc
* https://github.com/qq573011406/KindleHelper
* https://github.com/InzGIBA/manga
* [https://scrapy.org/ Scrapy 爬蟲框架]

View File

@@ -0,0 +1,540 @@
/**
* @name WWW work crawler sub-functions
*
* @fileoverview WWW work crawler functions: part of command-line arguments
*
* @since 2019/10/20 拆分自 CeL.application.net.work_crawler.task
*/
'use strict';
// --------------------------------------------------------------------------------------------
if (typeof CeL === 'function') {
// 忽略沒有 Windows Component Object Model 的錯誤。
CeL.env.ignore_COM_error = true;
CeL.run({
// module name
name : 'application.net.work_crawler.arguments',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : 'this,*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
}
function module_code(library_namespace) {
// requiring
var Work_crawler = library_namespace.net.work_crawler;
var gettext = library_namespace.locale.gettext;
// --------------------------------------------------------------------------------------------
/**
* 正規化定義參數的規範,例如數量包含可選範圍,可用 RegExp。如'number:0~|string:/v\\d/i',
* 'number:1~400|string:item1;item2;item3'。亦可僅使用'number|string'。
*
* @see CeL.data.fit_filter()
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/text#pattern
*/
function generate_argument_condition(condition) {
if (library_namespace.is_Object(condition))
return condition;
var condition_data = Object.create(null), matched, PATTERN = /([a-z]+)(?::(\/(\\[\s\S]|[^\/])+\/([i]*)|[^|]+))?(?:\||$)/g;
while (matched = PATTERN.exec(condition)) {
var type = matched[1], _condition = undefined;
if (!matched[2]) {
;
} else if (matched[3]) {
_condition = new RegExp(matched[3], matched[4]);
} else if (type === 'number' && (_condition = matched[2].match(
// @see CeL.date.parse_period.PATTERN
/([+\-]?\d+(?:\.\d+)?)?\s*[~-—─~〜﹣至]\s*([+\-]?\d+(?:\.\d+)?)?/))) {
_condition = {
min : _condition[1] && +_condition[1],
max : _condition[2] && +_condition[2]
};
} else if (type === 'number'
&& (matched[2] === 'natural' || matched[2] === '')) {
_condition = function is_natural(value) {
return value >= 1 && value === Math.floor(value);
};
} else if (type === 'number'
&& (matched[2] === 'natural+0' || matched[2] === '+0')) {
// Naturals with zero: non-negative integers 非負整數。
_condition = function is_non_negative(value) {
return value >= 0 && value === Math.floor(value);
};
} else if (type === 'number' && matched[2] === 'integer') {
_condition = function is_integer(value) {
return value === Math.floor(value);
};
} else {
_condition = matched[2].split(';');
}
condition_data[type] = _condition;
}
return condition_data;
}
/**
* 初始設定好命令列選項之型態資料集。
*
* @param {Object}[arg_hash]
* 參數型態資料集。
* @param {Boolean}[append]
* 添加至當前的參數型態資料集。否則會重新設定參數型態資料集。
*
* @returns {Object}命令列選項之型態資料集。
*/
function setup_argument_conditions(arg_hash, append) {
if (append) {
arg_hash = Object.assign(Work_crawler.prototype.import_arg_hash,
arg_hash);
} else if (arg_hash) {
// default: rest import_arg_hash
Work_crawler.prototype.import_arg_hash = arg_hash;
} else {
arg_hash = Work_crawler.prototype.import_arg_hash;
}
Object.keys(arg_hash).forEach(function(key) {
arg_hash[key] = generate_argument_condition(arg_hash[key]);
});
// console.log(arg_hash);
return arg_hash;
}
Work_crawler.setup_argument_conditions = setup_argument_conditions;
/**
* 檢核 crawler 的設定參數。
*
* @param {String}key
* 參數名稱
* @param value
* 欲設定的值
*
* @returns {Boolean} true: Error occudded
*
* @see CeL.data.fit_filter()
*/
function verify_arg(key, value) {
if (!(key in this.import_arg_hash)) {
return true;
}
var type = typeof value, arg_type_data = this.import_arg_hash[key];
// console.log(arg_type_data);
if (!(type in arg_type_data)) {
library_namespace.warn([ 'verify_arg: ', {
// gettext_config:{"id":"the-allowed-data-type-for-$1-is-$4-but-it-was-set-to-{$2}-$3"}
T : [ '"%1" 這個值所允許的數值類型為 %4但現在被設定成 {%2} %3',
//
key, typeof value, value,
//
library_namespace.is_Object(arg_type_data)
//
? Object.keys(arg_type_data).map(function(type) {
return gettext(type);
}).join('|') : arg_type_data ]
} ]);
return true;
}
arg_type_data = arg_type_data[type];
if (Array.isArray(arg_type_data)) {
if (arg_type_data.length === 1
&& typeof arg_type_data[0] === 'string') {
var fso_type = arg_type_data[0]
.match(/^fso_(file|files|directory|directories)$/);
if (fso_type) {
fso_type = fso_type[1];
if (typeof value === 'string')
value = value.split('|');
// assert: Array.isArray(value)
var error_fso = undefined, checker = fso_type
.startsWith('file') ? library_namespace.storage.file_exists
: library_namespace.storage.directory_exists;
if (value.some(function(fso_path) {
if (!checker(fso_path)) {
error_fso = fso_path;
return true;
}
})) {
library_namespace.warn([ 'verify_arg: ', {
// gettext_config:{"id":"some-$2-path(s)-specified-by-$1-do-not-exist-$3"}
T : [ '至少一個由「%1」所指定的%2路徑不存在%3', key,
// gettext_config:{"id":"file","mark_type":"combination_message_id"}
// gettext_config:{"id":"files","mark_type":"combination_message_id"}
// gettext_config:{"id":"directory","mark_type":"combination_message_id"}
// gettext_config:{"id":"directories","mark_type":"combination_message_id"}
gettext(fso_type), error_fso ]
} ]);
return true;
}
return;
}
}
// e.g., "string:value1,value2"
if (arg_type_data.includes(value)) {
// verified
return;
}
} else if (arg_type_data && ('min' in arg_type_data)) {
// assert: type === 'number'
if ((!arg_type_data.min || arg_type_data.min <= value)
&& (!arg_type_data.max || value <= arg_type_data.max)) {
// verified
return;
}
} else if (typeof arg_type_data === 'function') {
if (arg_type_data(value))
return;
} else {
if (arg_type_data !== undefined) {
library_namespace.warn([ 'verify_arg: ', {
// gettext_config:{"id":"unable-to-process-$1-condition-with-value-type-$2"}
T : [ '無法處理 "%1" 在數值類型為 %2 時之條件!', key, arg_type_data ]
} ]);
}
// 應該修改審查條件式,而非數值本身的問題。
return;
}
library_namespace.warn([ 'verify_arg: ', {
// gettext_config:{"id":"$1-is-set-to-the-problematic-value-{$2}-$3"}
T : [ '"%1" 被設定成了有問題的值:{%2} %3', key, typeof value, value ]
} ]);
return true;
}
/**
* 設定 crawler 的參數。 normalize and setup value
*
* @example<code>
crawler.setup_value(key, value);
// 應該用:
this.setup_value(key, value);
// 不應用:
this[key] = value;
delete this[key];
</code>
*
* @param {any}
* key
* @param {any}
* value
*
* @return {String}has error
*/
function setup_value(key, value) {
if (!key)
// gettext_config:{"id":"key-value-not-given"}
return '未提供鍵值';
if (library_namespace.is_Object(key)) {
// assert: value === undefined
value = key;
for (key in value) {
this.setup_value(key, value[key]);
}
// TODO: return error
return;
}
// assert: typeof key === 'string'
switch (key) {
case 'proxy':
// 使用代理伺服器 proxy_server
// TODO: check .proxy
library_namespace.info({
// gettext_config:{"id":"using-proxy-server-$1"}
T : [ 'Using proxy server: %1', value ]
});
this.get_URL_options.proxy = this[key] = value;
return;
case 'cookie':
// set-cookie, document.cookie
if (this.get_URL_options.agent) {
library_namespace.merge_cookie(this.get_URL_options.agent,
value);
} else if (this.get_URL_options.cookie) {
if (!/;\s*$/.test(this.get_URL_options.cookie))
this.get_URL_options.cookie += ';';
this.get_URL_options.cookie += value;
} else {
this.get_URL_options.cookie = value;
}
// console.trace(this.get_URL_options);
return;
case 'timeout':
value = library_namespace.to_millisecond(value);
if (!(value >= 0)) {
// gettext_config:{"id":"failed-to-parse-time"}
return '無法解析的時間';
}
this.get_URL_options.timeout = this[key] = value;
break;
// case 'agent':
// @see function setup_agent(URL)
case 'user_agent':
if (!value) {
// gettext_config:{"id":"user-agent-is-not-set"}
return '未設定 User-Agent。';
}
this.get_URL_options.headers['User-Agent'] = this[key] = value;
break;
case 'Referer':
if (!value
// value === '': Unset Referer
&& value !== '') {
// gettext_config:{"id":"referer-cannot-be-undefined"}
return 'Referer 不可為 undefined。';
}
library_namespace.debug({
// gettext_config:{"id":"configure-referer-$1"}
T : [ '設定 Referer%1', JSON.stringify(value) ]
}, 2);
this.get_URL_options.headers.Referer = value;
// console.log(this.get_URL_options);
return;
case 'allow_EOI_error':
if (this.using_default_MIN_LENGTH) {
this[key] = value;
// 因為 .allow_EOI_error 會影響到 .MIN_LENGTH
this.setup_value('MIN_LENGTH', 'default');
return;
}
break;
case 'MIN_LENGTH':
// 設定預設可容許的最小圖像大小。
if (!(value >= 0)) {
if (value === 'default') {
this.using_default_MIN_LENGTH = true;
value = this.allow_EOI_error ? 4e3 : 1e3;
} else
// gettext_config:{"id":"min-image-size-should-be-greater-than-0"}
return '最小圖片大小應大於等於零';
} else {
delete this.using_default_MIN_LENGTH;
}
break;
case 'main_directory':
if (!value || typeof value !== 'string')
return;
value = value.replace(/[\\\/]/g,
// 正規化成當前作業系統使用的目錄分隔符號。
library_namespace.env.path_separator);
// main_directory 必須以 path separator 作結。
value = library_namespace.append_path_separator(value);
break;
}
if (key in this.import_arg_hash) {
this.verify_arg(key, value);
}
if (value === undefined) {
// delete this[key];
}
this[key] = value;
}
// import command line arguments 以命令行參數為準
// 從命令列引數來的設定,優先等級比起作品預設設定更高。
function import_args() {
// console.log(library_namespace.env.arg_hash);
if (!library_namespace.env.arg_hash) {
return;
}
for ( var key in library_namespace.env.arg_hash) {
if (!(key in this.import_arg_hash) && !(key in this)) {
continue;
}
var value = library_namespace.env.arg_hash[key];
if (this.import_arg_hash[key] === 'number') {
try {
// value = +value;
// 這樣可以處理如"1e3"
value = JSON.parse(value);
} catch (e) {
library_namespace.error('import_args: '
// gettext_config:{"id":"cannot-parse-$1"}
+ gettext('無法解析 %1', key + '=' + value));
continue;
}
}
var old_value = this[key], error = this.setup_value(key, value);
if (error) {
library_namespace.error('import_args: '
// gettext_config:{"id":"unable-to-set-$1-$2"}
+ gettext('無法設定 %1%2', key + '=' + old_value, error));
} else {
library_namespace.log(library_namespace.display_align([
[ key + ': ', old_value ],
// + ' ': 增加間隙。
// gettext_config:{"id":"from-command-line"}
[ gettext('由命令列') + ' → ', value ] ]));
}
}
}
// --------------------------------------------------------------------------------------------
// export 導出.
// @instance
Object.assign(Work_crawler.prototype, {
verify_arg : verify_arg,
setup_value : setup_value,
import_args : import_args,
// 數值規範。命令列可以設定的選項之型態資料集。通常僅做測試微調用。
// GUI 選項於 work_crawler/gui_electron/gui_electron_functions.js 設定。
// 以純量為主,例如邏輯真假、數字、字串。無法處理函數!
// 現在 import_arg_hash 之說明已與 I18n 統合在一起。
// work_crawler/work_crawler_loader.js與gui_electron_functions.js各參考了import_arg_hash的可選參數。
// @see work_crawler/gui_electron/gui_electron_functions.js
// @see work_crawler/resource/locale of work_crawler - locale.csv
// gettext_config:{"id":"number","mark_type":"combination_message_id"}
// gettext_config:{"id":"function","mark_type":"combination_message_id"}
// gettext_config:{"id":"boolean","mark_type":"combination_message_id"}
// gettext_config:{"id":"string","mark_type":"combination_message_id"}
// gettext_config:{"id":"fso_file","mark_type":"combination_message_id"}
// gettext_config:{"id":"fso_files","mark_type":"combination_message_id"}
// gettext_config:{"id":"fso_directory","mark_type":"combination_message_id"}
// gettext_config:{"id":"fso_directories","mark_type":"combination_message_id"}
import_arg_hash : {
// 預設值設定於 Work_crawler_prototype @ CeL.application.net.work_crawler
// set download directory, fso:directory
main_directory : 'string:fso_directory',
// crawler.show_work_data(work_data);
show_information_only : 'boolean',
one_by_one : 'boolean',
// 篩選想要下載的章節標題關鍵字。例如"單行本"。
chapter_filter : 'string',
// 開始/接續下載的章節。將依類型轉成 .start_chapter_title 或
// .start_chapter_NO。對已下載過的章節必須配合 .recheck。
start_chapter : 'number:natural|string',
// 開始/接續下載的章節編號。
start_chapter_NO : 'number:natural',
// 下載此章節編號範圍。例如 "20-30,50-60"。
chapter_NO_range : 'string',
// 開始/接續下載的章節標題。
start_chapter_title : 'string',
// 指定了要開始下載的列表序號。將會跳過這個訊號之前的作品。
// 一般僅使用於命令列設定。default:1
start_list_serial : 'number:natural|string',
// 重新整理列表檔案 rearrange list file
rearrange_list_file : 'boolean',
// string: 如 "3s"
chapter_time_interval : 'number:natural+0|string|function',
MIN_LENGTH : 'number:natural+0',
timeout : 'number:natural+0|string',
// 容許錯誤用的相關操作設定。
MAX_ERROR_RETRY : 'number:natural+0',
allow_EOI_error : 'boolean',
skip_error : 'boolean',
skip_chapter_data_error : 'boolean',
directory_name_pattern : 'string',
preserve_work_page : 'boolean',
preserve_chapter_page : 'boolean',
remove_ebook_directory : 'boolean',
// 當新獲取的檔案比較大時,覆寫舊的檔案。
overwrite_old_file : 'boolean',
vertical_writing : 'boolean|string',
// RTL_writing : 'boolean',
convert_to_language : 'string:TW;CN',
// 不解開原電子書的選項: 就算存在舊電子書檔案,也不解壓縮、利用舊資料。
discard_old_ebook_file : 'boolean',
user_agent : 'string',
// 代理伺服器 proxy_server: "username:password@hostname:port"
proxy : 'string',
// 設定下載時要添加的 cookie。 document.cookie: "key=value"
cookie : 'string',
// 可接受的圖片類別(延伸檔名)。以 "|" 字元作分隔,如 "webp|jpg|png"。未設定將不作檢查。
// 輸入 "images" 表示接受所有圖片。
acceptable_types : 'string',
// 漫畫下載完畢後壓縮圖片檔案。
archive_images : 'boolean',
// 完全沒有出現錯誤才壓縮圖片檔案。
archive_all_good_images_only : 'boolean',
// 壓縮圖片檔案之後,刪掉原先的圖片檔案。
remove_images_after_archive : 'boolean',
images_archive_extension : 'string',
// 重新擷取用的相關操作設定。
regenerate : 'boolean',
reget_chapter : 'boolean',
recheck : 'boolean|string:changed;multi_parts_changed',
search_again : 'boolean',
cache_title_to_id : 'boolean',
write_chapter_metadata : 'boolean',
write_image_metadata : 'boolean',
// 封存舊作品。
archive_old_works : 'boolean|string',
// 以作品完結時間為分界來封存舊作品。預設為最後一次下載時間。
use_finished_date_to_archive_old_works : 'boolean',
// 同時自作品列表中刪除將封存之作品。
modify_work_list_when_archive_old_works : 'boolean',
// 儲存偏好選項 save_options。
save_preference : 'boolean'
}
});
setup_argument_conditions();
// 不設定(hook)本 module 之 namespace僅執行 module code。
return library_namespace.env.not_to_extend_keyword;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,665 @@
/**
* @name WWW work crawler sub-functions
*
* @fileoverview WWW work crawler functions: part of image
*
* @since 2019/10/13 拆分自 CeL.application.net.work_crawler
*/
'use strict';
// --------------------------------------------------------------------------------------------
if (typeof CeL === 'function') {
// 忽略沒有 Windows Component Object Model 的錯誤。
CeL.env.ignore_COM_error = true;
CeL.run({
// module name
name : 'application.net.work_crawler.image',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : 'this,*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
}
function module_code(library_namespace) {
// requiring
var Work_crawler = library_namespace.net.work_crawler, crawler_namespace = Work_crawler.crawler_namespace;
var gettext = library_namespace.locale.gettext,
/** node.js file system module */
node_fs = library_namespace.platform.nodejs && require('fs');
// --------------------------------------------------------------------------------------------
function image_path_to_url(path, server) {
if (path.includes('://')) {
return path;
}
if (!server.includes('://')) {
// this.get_URL_options.headers.Host = server;
server = 'http://' + server;
}
return server + path;
}
function EOI_error_path(path, XMLHttp) {
return path.replace(/(\.[^.]*)$/, this.EOI_error_postfix
// + (XMLHttp && XMLHttp.status ? ' ' + XMLHttp.status : '')
+ '$1');
}
// 下載單一個圖片。
// callback(image_data, status)
function get_image(image_data, callback, images_archive) {
// console.log(image_data);
if (!image_data || !image_data.file || !image_data.url) {
if (image_data) {
image_data.has_error = true;
image_data.done = true;
}
// 注意: 此時 image_data 可能是 undefined
if (this.skip_error) {
// gettext_config:{"id":"unspecified-image-data"}
this.onwarning(gettext('未指定圖片資料'), image_data);
} else {
// gettext_config:{"id":"unspecified-image-data"}
this.onerror(gettext('未指定圖片資料'), image_data);
}
if (typeof callback === 'function')
callback(image_data, 'invalid_data');
return;
}
/**
* 每張圖片都要檢查實際存在的圖片檔案。當之前已存在完整的圖片時,就不再嘗試下載圖片。<br />
* 工作機制:<br />
* 檢核`image_data.file`是否存在。`image_data.file`由圖片的網址URL來判別可能的延伸檔名。猜不出的會採用預設的圖片延伸檔名/副檔名.default_image_extension。
*
* @see function process_images() @ CeL.application.net.work_crawler.chapter
*
* 若`image_data.file`不存在,將會檢核所有可接受的圖片類別副檔名(.acceptable_types)。
* 每張圖片都要檢核所有可接受的圖片類別,會加大硬碟讀取負擔。 會用到 .overwrite_old_file 這個選項的,應該都是需要提報
* issue 的,因此這個選項不會列出來。麻煩請在個別網站遇到此情況時提報 issue列出作品名稱以及圖片類別以供這邊確認圖片類別。
* 只要存在完整無損害的預設圖片類別或是可接受的圖片類別,就直接跳出,不再嘗試下載這張圖片。否則會重新下載圖片。
* 當下載的圖片以之前的圖片更大時,就會覆蓋原先的圖片。
* 若下載的圖片類別並非預設的圖片類別(.default_image_extension),例如預設 JPG 但得到 PNG
* 檔案時,會將副檔名改為實際得到的圖像格式。因此下一次下載時,需要設定 .acceptable_types 才能找得到圖片。
*/
var image_downloaded = node_fs.existsSync(image_data.file)
|| this.skip_existed_bad_file
// 檢查是否已有上次下載失敗,例如 server 上本身就已經出錯的檔案。
&& node_fs.existsSync(this.EOI_error_path(image_data.file)), acceptable_types;
if (!image_downloaded) {
// 正規化 acceptable_types
acceptable_types = image_data.acceptable_types
|| this.acceptable_types;
if (!acceptable_types) {
// 未設定將不作檢查。
} else if (typeof acceptable_types === 'string') {
acceptable_types = acceptable_types.trim();
if (acceptable_types === 'images') {
// 將會測試是否已經下載過一切可接受的檔案類別。
acceptable_types = Object.keys(this.image_types);
} else {
acceptable_types = acceptable_types.split('|');
}
} else if (!Array.isArray(acceptable_types)) {
library_namespace.warn({
// gettext_config:{"id":"invalid-acceptable_types-$1"}
T : [ 'Invalid acceptable_types: %1', acceptable_types ]
});
acceptable_types = null;
}
if (acceptable_types) {
// 檢核所有可接受的圖片類別(.acceptable_types)。
image_downloaded = acceptable_types.some(function(extension) {
var alternative_filename = image_data.file.replace(
/\.[a-z\d]+$/, '.' + extension);
if (node_fs.existsSync(alternative_filename)) {
image_data.file = alternative_filename;
return true;
}
});
}
}
// 檢查壓縮檔裡面的圖片檔案。
var image_archived, bad_image_archived;
if (images_archive && images_archive.fso_path_hash
// 檢查壓縮檔,看是否已經存在圖像檔案。
&& image_data.file.startsWith(images_archive.work_directory)) {
image_archived = image_data.file
.slice(images_archive.work_directory.length);
bad_image_archived = images_archive.fso_path_hash[this
.EOI_error_path(image_archived)];
if (image_archived && bad_image_archived) {
images_archive.to_remove.push(bad_image_archived);
}
if (false) {
console.log([ images_archive.fso_path_hash, acceptable_types,
image_archived,
images_archive.fso_path_hash[image_archived] ]);
}
image_downloaded = image_downloaded
|| images_archive.fso_path_hash[image_archived]
|| this.skip_existed_bad_file
// 檢查是否已有上次下載失敗,例如 server 上本身就已經出錯的檔案。
&& bad_image_archived;
if (!image_downloaded
// 可以接受的圖片類別/圖片延伸檔名/副檔名/檔案類別 acceptable file extensions
&& acceptable_types) {
image_downloaded = acceptable_types.some(function(extension) {
var alternative_filename = image_archived.replace(
/\.[a-z\d]+$/, '.' + extension);
return images_archive.fso_path_hash[alternative_filename];
});
}
}
if (image_downloaded) {
// console.log('get_image: Skip ' + image_data.file);
image_data.done = true;
if (typeof callback === 'function')
callback(image_data, 'image_downloaded');
return;
}
// --------------------------------------
var _this = this,
// 漫畫圖片的 URL。
image_url = image_data.url, server = this.server_list;
if (server) {
server = server[server.length * Math.random() | 0];
image_url = this.image_path_to_url(image_url, server, image_data);
} else {
image_url = this.full_URL(image_url);
}
image_data.parsed_url = image_url;
if (!crawler_namespace.PATTERN_non_CJK.test(image_url)) {
// 工具檔應先編碼URL。
library_namespace.warn({
// gettext_config:{"id":"invalid-url-must-encode-first-$1"}
T : [ '必須先將URL編碼%1', image_url ]
});
// console.trace(image_url);
if (!/%[\dA-F]{2}/i.test(image_url))
image_url = encodeURI(image_url);
}
if (!image_data.file_length) {
image_data.file_length = [];
}
// console.log('get_image: image_url: ' + image_url);
// library_namespace.set_debug(3);
this.get_URL(image_url, function(XMLHttp) {
// console.trace(XMLHttp.status);
// console.log(image_data);
if (image_data.url !== XMLHttp.responseURL) {
// 紀錄最後實際下載的圖片網址。
image_data.responseURL = XMLHttp.responseURL;
}
/** {Buffer}圖片數據的內容。 */
var contents = XMLHttp.buffer;
// 修正圖片結尾 tail 非正規格式之情況。
// TODO: 應該檢測刪掉後是正確的圖片檔,才刪掉 trailing new line。
if (_this.trim_trailing_newline && contents && contents.length > 4
// 去掉最後的換行符號:有些圖片在檔案最後會添加上換行符號 "\r\n",因此被判別為非正規圖片檔。
&& contents.at(-2) === 0x0D && contents.at(-1) === 0x0A) {
contents = contents.slice(0, -2);
}
if (_this.image_preprocessor) {
// 圖片前處理程序 預處理器 image pre-processing
// 例如修正圖片結尾非正規格式之情況。
// 必須自行確保不會 throw需檢查 contents 是否非 {Buffer}。
try {
contents = _this.image_preprocessor(contents, image_data);
} catch (e) {
has_error = has_error || e;
}
// if _this.image_preprocessor() returns `false`,
// will treat as bad file.
if (contents === undefined)
contents = XMLHttp.buffer;
}
var has_error = !contents || !(contents.length >= _this.MIN_LENGTH)
|| (XMLHttp.status / 100 | 0) !== 2, verified_image;
// console.trace([ image_url, XMLHttp.responseURL ]);
if (!image_data.is_bad
// image_data.is_bad may be set by _this.image_preprocessor()
&& typeof _this.is_limited_image_url === 'function') {
// 處理特殊圖片: 檢查是否下載到 padding 用的 404 檔案。
image_data.is_bad = _this.is_limited_image_url(
XMLHttp.responseURL, image_data);
if (!image_data.is_bad)
delete image_data.is_bad;
else if (image_data.is_bad === true)
image_data.is_bad = 'is limited image';
}
if (!has_error) {
image_data.file_length.push(contents.length);
library_namespace.debug({
// gettext_config:{"id":"completed-image-testing-$1"}
T : [ '測試圖片是否完整:%1', image_data.file ]
}, 2, 'get_image');
var file_type = library_namespace.file_type(contents);
verified_image = file_type && !file_type.damaged;
if (verified_image) {
// console.log(_this.image_types);
if (!file_type.extensions
//
|| !file_type.extensions.some(function(extension) {
return extension in _this.image_types;
})) {
verified_image = false;
library_namespace
.warn({
T : [
// gettext_config:{"id":"unable-to-process-image-file-of-type-$2-$1"}
file_type.type ? '無法處理類型為 %2 之圖片檔:%1'
// gettext_config:{"id":"unable-to-determine-the-type-of-image-file-$1"}
: '無法判別圖片檔之類型:%1', image_data.file,
file_type.type ]
});
}
var original_extension
//
= image_data.file.match(/[^.]*$/)[0].toLowerCase();
if (file_type.extensions ?
//
!image_data.file.endsWith('.' + file_type.extension)
// accept '.jpeg' as alias of '.jpg'
&& !file_type.extensions.includes(original_extension)
// 無法判別圖片檔類型時,若原副檔名為圖片檔案類別則採用之。
: !(original_extension in _this.image_types)) {
// 依照所驗證的檔案格式改變副檔名。
image_data.file = image_data.file.replace(/[^.]+$/,
// e.g. .png
file_type.extension
// 若是沒有辦法判別延伸檔名,那麼就採用預設的圖片延伸檔名。
|| _this.default_image_extension);
}
}
}
// verified_image===true 則必然(!!has_error===false)
// has_error表示下載過程發生錯誤光是檔案損毀不會被當作has_error!
// has_error則必然(!!verified_image===false)
if (false) {
console.log([ _this.skip_error, _this.MAX_ERROR_RETRY,
//
_this.MIN_LENGTH, has_error, _this.skip_error
//
&& image_data.error_count === _this.MAX_ERROR_RETRY ]);
// 出錯次數
library_namespace.log({
// gettext_config:{"id":"number-of-errors-$1"}
T : [ '{{PLURAL:%1|%1}} 次錯誤', image_data.error_count ]
});
}
if (verified_image || image_data.is_bad || _this.skip_error
// 有出問題的話最起碼都需retry足夠次數。
&& image_data.error_count === _this.MAX_ERROR_RETRY
//
|| _this.allow_EOI_error
//
&& image_data.file_length.length > _this.MAX_EOI_ERROR) {
// console.log(image_data.file_length);
if (verified_image || image_data.is_bad || _this.skip_error
// skip error 的話,不管有沒有下載/獲取過檔案(包括404圖像不存在),依然 pass。
// && image_data.file_length.length === 0
//
|| image_data.file_length.cardinal_1()
// ↑ 若是每次都得到相同的檔案長度,那就當作來源檔案本來就有問題。
&& (_this.skip_error || _this.allow_EOI_error
//
&& image_data.file_length.length > _this.MAX_EOI_ERROR)) {
// 圖片下載過程結束,不再嘗試下載圖片:要不是過關,要不就是錯誤太多次了。
var bad_file_path = _this.EOI_error_path(image_data.file,
XMLHttp);
if (has_error || image_data.is_bad
|| verified_image === false) {
image_data.file = bad_file_path;
image_data.has_error = true;
if (_this.preserve_bad_image) {
library_namespace.warn([ {
T : has_error ? contents
// gettext_config:{"id":"force-non-image-files-to-be-saved-as-images"}
? '強制將非圖片檔儲存為圖片。'
// gettext_config:{"id":"force-empty-content-to-be-saved-as-an-image"}
: '強制將空內容儲存為圖片。'
// assert: (!!verified_image===false)
// 圖檔損壞: e.g., Do not has EOI
// gettext_config:{"id":"force-storage-of-damaged-image"}
: '強制儲存損壞的圖片。'
}, XMLHttp.status
// 狀態碼正常就不顯示。
&& (XMLHttp.status / 100 | 0) !== 2 ? {
// gettext_config:{"id":"http-status-code-$1"}
T : [ 'HTTP status code %1.', XMLHttp.status ]
} : '',
// 顯示 crawler 程式指定的錯誤。
image_data.is_bad ? {
// gettext_config:{"id":"error-$1"}
T : [ 'Error: %1.', image_data.is_bad ]
} : '', contents ? {
// gettext_config:{"id":"file-size-$1"}
T : [ 'File size: %1.',
//
CeL.to_KiB(contents.length) ]
} : '',
//
': ' + image_data.file + '\n← ' + image_url ]);
}
if (!contents
// 404之類就算有內容也不過是錯誤訊息頁面。
|| (XMLHttp.status / 100 | 0) === 4) {
contents = '';
}
} else {
// pass, 過關了。
if (node_fs.existsSync(bad_file_path)) {
library_namespace.info({
// gettext_config:{"id":"delete-corrupted-old-image-file-$1"}
T : [ '刪除損壞的舊圖片檔:%1', bad_file_path ]
});
library_namespace.fs_remove(bad_file_path);
}
if (bad_image_archived) {
// 登記壓縮檔內可以刪除的損壞圖檔。
images_archive.to_remove.push(bad_image_archived);
}
}
var old_file_status, old_archived_file =
// image_data.has_error?bad_image_archived:image_archived
image_archived || bad_image_archived;
try {
old_file_status = node_fs.statSync(image_data.file);
} catch (e) {
// old/bad file not exist
}
if (old_archived_file && (!old_file_status
//
|| old_archived_file.size > old_file_status.size)) {
// 壓縮檔內的圖像質量更好的情況,那就採用壓縮檔的。
if (old_file_status
&& old_archived_file.size < contents.length) {
library_namespace.warn({
T : [ _this.archive_images
// gettext_config:{"id":"the-quality-of-the-image-in-the-archive-is-better-than-in-the-directory-but-will-be-overwritten-after-downloading-$1"}
? '壓縮檔內的圖片品質比目錄中的更好,但在下載完後將可能在壓縮時被覆蓋:%1'
// gettext_config:{"id":"the-quality-of-the-image-in-the-archive-is-better-than-in-the-directory-$1"}
: '壓縮檔內的圖片品質比目錄中的更好:%1',
//
old_archived_file.path ]
});
}
old_file_status = old_archived_file;
}
if (!old_file_status || _this.overwrite_old_file
// 得到更大的檔案,寫入更大的檔案。
&& !(old_file_status.size >= contents.length)) {
if (_this.image_post_processor) {
// 圖片後處理程序 image post-processing
contents = _this.image_post_processor(contents,
image_data
// , images_archive
)
|| contents;
}
if (!image_data.has_error || _this.preserve_bad_image) {
library_namespace.debug({
// gettext_config:{"id":"save-image-data-to-your-hard-drive-$1"}
T : [ '保存圖片數據到硬碟上:%1', image_data.file ]
}, 1, 'get_image');
// TODO: 檢查舊的檔案是不是文字檔。例如有沒有包含 HTML 標籤。
try {
node_fs
.writeFileSync(image_data.file,
contents);
} catch (e) {
library_namespace.error(e);
// gettext_config:{"id":"unable-to-write-to-image-file-$1"}
var message = [ gettext('無法寫入圖片檔案 [%1]。',
image_data.file) ];
if (e.code === 'ENOENT') {
message.push(gettext(
// TODO: show chapter_directory 當前作品章節目錄:
// gettext_config:{"id":"it-may-be-because-the-download-directory-of-the-work-has-changed-and-the-cache-data-points-to-the-old-location-that-does-not-exist"}
'可能因為作品下載目錄改變了,而快取資料指向不存在的舊位置。'));
} else {
message.push(gettext(
// gettext_config:{"id":"it-may-be-because-the-work-information-cache-is-different-from-the-structure-of-the-work-chapter-on-the-current-website"}
'可能因為作品資訊快取與當前網站上之作品章節結構不同。'));
}
message.push(gettext(
// https://github.com/kanasimi/work_crawler/issues/278
// gettext_config:{"id":"if-you-have-downloaded-this-work-before-please-save-the-original-work-catalog-or-rename-the-work-cache-file-(the-work-id.json-under-the-work-directory)-and-try-the-new-download"}
'若您之前曾經下載過本作品的話,請封存原有作品目錄,或將作品資訊快取檔(作品目錄下的 作品id.json改名之後嘗試全新下載。'
//
));
_this.onerror(message.join('\n'), image_data);
if (typeof callback === 'function') {
callback(image_data,
'image_file_write_error');
}
return Work_crawler.THROWED;
}
}
} else if (old_file_status
&& old_file_status.size > contents.length) {
library_namespace.log({
T : [
// gettext_config:{"id":"there-is-a-large-old-file-($2)-that-will-not-be-overwritten-$1"}
'存在較大的舊檔 (%2),將不覆蓋:%1',
image_data.file,
old_file_status.size + '>'
+ contents.length ]
});
}
image_data.done = true;
if (typeof callback === 'function')
callback(image_data/* , 'OK' */);
return;
}
}
// 有錯誤。下載圖像錯誤時報錯。
var message;
if (verified_image === false) {
// 圖檔損壞: e.g., Do not has EOI
message = [ {
// gettext_config:{"id":"image-damaged"}
T : '圖檔損壞:'
} ];
} else {
// 圖檔沒資格驗證。
message = [ {
// gettext_config:{"id":"failed-to-get-image"}
T : '無法取得圖片。'
}, XMLHttp.status ? {
// gettext_config:{"id":"http-status-code-$1"}
T : [ 'HTTP status code %1.', XMLHttp.status ]
} : '', {
// gettext_config:{"id":"image-without-content"}
T : !contents ? '圖片無內容:' : [
//
contents.length < _this.MIN_LENGTH
// gettext_config:{"id":"$1-bytes-too-small"}
? '檔案過小,僅 %1 {{PLURAL:%1|位元組}}'
// gettext_config:{"id":"$1-bytes"}
: '檔案僅 %1 {{PLURAL:%1|位元組}}', contents.length ]
} ];
}
message.push(image_url + '\n→ ' + image_data.file);
library_namespace.warn(message);
// Release memory. 釋放被占用的記憶體。
message = null;
if (image_data.error_count === _this.MAX_ERROR_RETRY) {
image_data.has_error = true;
// throw new Error(_this.id + ': ' +
// gettext('MESSAGE_NEED_RE_DOWNLOAD'));
library_namespace.log(_this.id + ': '
// gettext_config:{"id":"message_need_re_download"}
+ gettext('MESSAGE_NEED_RE_DOWNLOAD'));
// console.log('error count: ' + image_data.error_count);
if (contents && contents.length > 10
//
&& contents.length < _this.MIN_LENGTH
// 檔案有驗證過,只是太小時,應該不是 false。
&& verified_image !== false
// 就算圖像是完整的只是比較小HTTP status code 也應該是 2xx。
&& (XMLHttp.status / 100 | 0) === 2) {
library_namespace.warn([ {
// gettext_config:{"id":"perhaps-the-image-is-complete-just-too-small-and-not-up-to-standard-such-as-an-almost-blank-image"}
T : '或許圖片是完整的,只是過小而未達標,例如幾乎為空白之圖片。'
}, {
// gettext_config:{"id":"work_crawler-skip-image-error-prompt"}
T : [ 'work_crawler-skip-image-error-prompt',
//
contents.length,
//
JSON.stringify(_this.EOI_error_postfix) ]
} ]);
} else if (image_data.file_length.length > 1
&& !image_data.file_length.cardinal_1()) {
library_namespace.warn([ {
// gettext_config:{"id":"the-downloaded-image-is-different-in-size-$1"}
T : [ '下載所得的圖片大小不同:%1。', image_data.file_length ]
}, {
// gettext_config:{"id":"if-it-is-not-because-the-website-cuts-off-the-connection-early-then-you-may-need-to-increase-the-time-limit-to-provide-enough-time-to-download-the-image"}
T : '若非因網站提早截斷連線,那麼您或許需要增長時限來提供足夠的時間下載圖片?'
} ]);
// TODO: 提供續傳功能。
// e.g., for 9mdm.js→dagu.js 魔剑王 第59话 4392-59-011.jpg
} else if (!_this.skip_error) {
library_namespace.info([ {
// gettext_config:{"id":"if-the-error-persists-you-can-set-skip_error=true-to-ignore-the-image-error"}
T : '若錯誤持續發生,您可以設定 skip_error=true 來忽略圖片錯誤。'
}, {
// gettext_config:{"id":"you-must-set-the-skip_error-or-allow_eoi_error-option-to-store-corrupted-files"}
T : '您必須設定 skip_error 或 allow_EOI_error 選項,才會儲存損壞的檔案。'
}, {
// gettext_config:{"id":"if-you-need-to-re-download-the-section-that-failed-to-download-before-turn-on-the-recheck-option"}
T : '若您需要重新下載之前下載失敗的章節,請開啟 recheck 選項。'
} ]);
}
// gettext_config:{"id":"failed-to-download-image"}
_this.onerror(gettext('圖片下載錯誤'), image_data);
// image_data.done = false;
if (typeof callback === 'function')
callback(image_data, 'image_download_error');
return Work_crawler.THROWED;
// 網頁介面不可使用process.exit(),會造成白屏
// process.exit(1);
}
image_data.error_count = (image_data.error_count | 0) + 1;
library_namespace.log([ 'get_image: ', {
// gettext_config:{"id":"retry-$1-$2"}
T : [ 'Retry %1/%2',
//
image_data.error_count, _this.MAX_ERROR_RETRY ]
}, '...' ]);
var get_image_again = function() {
_this.get_image(image_data, callback, images_archive);
}
if (image_data.time_interval > 0) {
library_namespace.log_temporary([ 'get_image: ', {
// gettext_config:{"id":"waiting-for-$2-and-retake-the-image-$1"}
T : [ '等待 %2 之後再重新取得圖片:%1', image_data.url,
//
library_namespace.age_of(0, image_data.time_interval, {
digits : 1
}) ]
} ]);
setTimeout(get_image_again, image_data.time_interval);
} else
get_image_again();
}, null, Object.assign({
/**
* 最多平行下載/獲取檔案(圖片)的數量。
*
* <code>
incase "MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 connect listeners added. Use emitter.setMaxListeners() to increase limit"
</code>
*/
max_listeners : 300,
fetch_type : 'image'
}, this.get_URL_options, image_data.get_URL_options), 'buffer');
}
// --------------------------------------------------------------------------------------------
// export 導出.
// @instance
Object.assign(Work_crawler.prototype, {
// 可接受的圖片類別(延伸檔名)。以 "|" 字元作分隔,如 "webp|jpg|png"。未設定將不作檢查。輸入 "images"
// 表示接受所有圖片。
// 若下載的圖片不包含在指定類型中,則會視為錯誤。本工具只能下載特定幾種圖片類型。本選項僅供檢查圖片,非用來挑選想下載的圖片類型。
// {Array|String}可以接受的圖片類別/圖片延伸檔名/副檔名/檔案類別 acceptable file extensions。
// acceptable_types : 'images',
// acceptable_types : 'png',
// acceptable_types : 'webp|png',
// acceptable_types : ['webp', 'png'],
// 當圖片不存在 EOI (end of image) 標記,或是被偵測出非圖片時,依舊強制儲存檔案。
// allow image without EOI (end of image) mark. default:false
// allow_EOI_error : true,
// 圖片檔案下載失敗處理方式:忽略/跳過圖片錯誤。當404圖片不存在、檔案過小或是被偵測出非圖片(如不具有EOI)時依舊強制儲存檔案。default:false
// skip_error : true,
//
// 若已經存在壞掉的圖片就不再嘗試下載圖片。default:false
// skip_existed_bad_file : true,
//
// 循序逐個、一個個下載圖片。僅對漫畫有用,對小說無用。小說章節皆為逐個下載。 Download images one by one.
// default: 同時下載本章節中所有圖片。 Download ALL images at the same time.
// 若設成{Natural}大於零的數字(ms)或{String}時間長度,那會當成下載每張圖片之時間間隔 time_interval。
// cf. .chapter_time_interval
// one_by_one : true,
//
// e.g., '2-1.jpg' → '2-1 bad.jpg'
EOI_error_postfix : ' bad',
// 加上有錯誤檔案之註記。
EOI_error_path : EOI_error_path,
image_path_to_url : image_path_to_url,
get_image : get_image
});
// 不設定(hook)本 module 之 namespace僅執行 module code。
return library_namespace.env.not_to_extend_keyword;
}

View File

@@ -0,0 +1,206 @@
/**
* @name WWW work crawler sub-functions
*
* @fileoverview WWW work crawler functions: part of search
*
* @since 2019/10/13 拆分自 CeL.application.net.work_crawler
*/
'use strict';
// --------------------------------------------------------------------------------------------
if (typeof CeL === 'function') {
// 忽略沒有 Windows Component Object Model 的錯誤。
CeL.env.ignore_COM_error = true;
CeL.run({
// module name
name : 'application.net.work_crawler.search',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : 'this,*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
}
function module_code(library_namespace) {
// requiring
var Work_crawler = library_namespace.net.work_crawler, crawler_namespace = Work_crawler.crawler_namespace;
var gettext = library_namespace.locale.gettext;
// --------------------------------------------------------------------------------------------
// @see luoxia.js, dmzj.js
function parse_search_result_token(id_list, id_data, token_parser, token) {
var matched = token.match(/<a\s([^<>]+)>([\s\S]+?)<\/a>/i);
if (library_namespace.is_RegExp(token_parser)) {
matched = token.match(token_parser);
} else {
// matched: [ link, attributes, inner HTML ]
matched = token.match(/<a\s([^<>]+)>([\s\S]+?)<\/a>/i);
}
if (!matched)
return;
var id = matched[1]
// dmzj.js: title=""href="" 中間沒有空格。
.match(/href=["'][^"'<>]+?\/([a-z\d\-_]+)(?:\/|\.html)?["']/i);
if (!id)
return;
id = id[1];
if (false && !isNaN(id)) {
id = +id;
}
id_list.push(id);
var title = matched[1].match(/title=["']([^"'<>]+)["']/);
id_data.push(crawler_namespace.get_label(title && title[1]
|| matched[2]));
}
// only for .parse_search_result() !!
function extract_work_id_from_search_result_link(PATTERN_item_token, html,
token_parser) {
// console.log(html);
var matched,
// {Array}id_list = [ id, id, ... ]
id_list = [],
// {Array}id_data = [ title, title, ... ]
id_data = [],
//
search_result_parser = typeof token_parser === 'function'
//
&& function(token) {
// function parser(token, id_list, id_data){console.log(token);}
var result = token_parser.call(this, token, id_list, id_data);
if (Array.isArray(result) && result.length === 2) {
id_list.push(result[0]);
id_data.push(result[1]);
}
};
// PATTERN_item_token 會分離出每個作品的欄位。
if (library_namespace.is_RegExp(PATTERN_item_token)) {
// assert: PATTERN_item_token.global === true
// matched: [ , HTML token to check ]
while (matched = PATTERN_item_token.exec(html)) {
if (search_result_parser) {
search_result_parser(matched[1]);
} else {
parse_search_result_token(id_list, id_data, token_parser,
matched[1]);
}
}
} else if (Array.isArray(PATTERN_item_token)) {
html.each_between(PATTERN_item_token[0], PATTERN_item_token[1],
search_result_parser
|| parse_search_result_token.bind(null, id_list,
id_data, token_parser));
} else {
throw new TypeError('extract_work_id_from_search_result_link: '
// gettext_config:{"id":"invalid-token-pattern-{$1}-$2"}
+ gettext('Invalid token pattern: {%1} %2',
typeof PATTERN_item_token, JSON
.stringify(PATTERN_item_token)));
}
// console.log([ id_list, id_data ]);
// throw 'extract_work_id_from_search_result_link';
return [ id_list, id_data ];
}
// CeL.work_crawler.extract_work_id_from_search_result_link()
Work_crawler.extract_work_id_from_search_result_link = extract_work_id_from_search_result_link;
// --------------------------------------------------------------------------------------------
var PATTERN_url_for_baidu = /([\d_]+)(?:\.html|\/(?:index\.html)?)?$/;
if (library_namespace.is_debug()) {
[ 'http://www.host/0/123/', 'http://www.host/123/index.html',
'http://www.host/123.html' ].forEach(function(url) {
console.assert('123' === 'http://www.host/123/'
.match(PATTERN_url_for_baidu)[1]);
});
}
crawler_namespace.parse_search_result_set = {
// baidu cse
baidu : function(html, get_label) {
// console.log(html);
var id_data = [],
// {Array}id_list = [id,id,...]
id_list = [], get_next_between = html.find_between(
' cpos="title" href="', '</a>'), text;
while ((text = get_next_between()) !== undefined) {
// console.log(text);
// 從URL網址中解析出作品id。
var matched = text.between(null, '"').match(
PATTERN_url_for_baidu);
// console.log(matched);
if (!matched)
continue;
id_list.push(matched[1]);
// 從URL網址中解析出作品title。
matched = text.match(/ title="([^"]+)"/);
if (matched) {
matched = matched[1];
} else {
// e.g., omanhua.js: <em>择</em><em>天</em><em>记</em>
matched = text.between('<em>', {
tail : '</em>'
});
}
// console.log(matched);
if (matched && (matched = get_label(matched))
// 只取第一個符合的。
// 避免如 http://host/123/, http://host/123/456.htm
&& !id_data.includes(matched)) {
id_data.push(matched);
} else {
id_list.pop();
}
}
// console.log([ id_list, id_data ]);
return [ id_list, id_data ];
}
};
// --------------------------------------------------------------------------------------------
// export 導出.
// @instance
Object.assign(Work_crawler.prototype, {
search_result_file_name : 'search.json',
cache_title_to_id : true,
get_search_result_file : function() {
var search_result_file = this.main_directory
+ this.search_result_file_name;
return search_result_file;
},
get_search_result : function() {
var search_result_file = this.get_search_result_file(),
// search cache
// 檢查看看之前是否有獲取過。
search_result = library_namespace.get_JSON(search_result_file);
return search_result;
}
});
// 不設定(hook)本 module 之 namespace僅執行 module code。
return library_namespace.env.not_to_extend_keyword;
}

View File

@@ -0,0 +1,233 @@
/**
* @name CeL module for downloading AlphaPolis user novels / comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載 アルファポリス - 電網浮遊都市 - 小説 / 漫画 的工具。
*
* <code>
CeL.AlphaPolis({
// configuration
site : '' || CeL.get_script_name()
}).start(work_id);
</code>
*
* @since 2020/7/18 5:31:55 模組化。
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/novel.ja-JP/AlphaPolis.js
// https://github.com/kanasimi/work_crawler/blob/master/comic.ja-JP/AlphaPolis_user_manga.js
// https://github.com/kanasimi/work_crawler/blob/master/comic.ja-JP/AlphaPolis_official_manga.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.AlphaPolis',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
var default_configuration = {
// 當網站不允許太過頻繁的訪問/access時可以設定下載之前的等待時間(ms)。
// 模仿實際人工請求。
// chapter_time_interval : '5s',
base_URL : 'https://www.alphapolis.co.jp/',
// @deprecated: novel
// till 20170619, use POST. 20170620 AlphaPolis 改版, use UTF-8.
search_URL_2016_to_20170619 : function(work_title) {
return [ 'top/search/', {
// 2: 小説
'data[tab]' : 2,
'data[refer]' : work_title
} ];
},
// 解析 作品名稱 → 作品id get_work()
search_URL : function(work_title) {
return 'search?category='
+ this.work_type.split('/').reverse().join('_') + '&query='
+ encodeURIComponent(work_title.replace(/\s+\d+$/, ''));
},
// for AlphaPolis_user_manga.js , AlphaPolis_official_manga.js
parse_search_result : function(html, get_label) {
// console.log(html);
var _this = this, id_data = [],
// {Array}id_list = [id,id,...]
id_list = [];
html.each_between(' class="title">', '</a>', function(text) {
// console.log(text);
var id = text.between(' href="/' + _this.work_type + '/', '"');
if (id) {
id_list.push(id.replace(/\//, '-'));
id_data.push(get_label(text.between('>')));
}
});
// console.log([ id_list, id_data ]);
return [ id_list, id_data ];
},
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
return this.work_type + '/' + work_id.replace('-', '/');
},
// for AlphaPolis.js , AlphaPolis_user_manga.js
parse_work_data : function(html, get_label, extract_work_data) {
var work_data = {
// 必要屬性:須配合網站平台更改。
// 2019/8/16 19:0 改版。
title : get_label(html.between('<h1 class="title">', '</h1>')
|| html.between('<h2 class="title">', '</h1>')),
// 選擇性屬性:須配合網站平台更改。
// e.g., 连载中, 連載中
status : [],
// get the first <div class="author">...</div>
author : get_label(html.between('<div class="author">', '</a>')),
last_update : get_label(html.between('<th>更新日時</th>', '</td>')),
description : get_label(
html.between('<div class="abstract">', '</div>'))
.replace(/\n{2}/g, '\n'),
ranking : get_label(html.between('<div class="ranking">',
// also category
'</div>')).replace(/\t/g, '').replace(/\n{3}/g, '\0').replace(
/\n/g, ' ').split('\0'),
// site_name : 'アルファポリス'
language : html.between('data-lang="', '"') || 'ja-JP'
}, PATTERN, matched;
if (work_data.title === 'Not Found' && !work_data.author) {
// 對於已經失效的作品,直接中斷下載。
throw work_data.title;
}
if (work_data.site_name) {
// "アルファポリス - 電網浮遊都市 - " → "アルファポリス"
work_data.site_name = work_data.site_name.replace(/ +- .+/, '');
}
// 2019/1 才發現改 pattern 了。
PATTERN = /<span class="tag">([\s\S]+?)<\/span>/g;
while (matched = PATTERN.exec(html.between(
'<div class="content-tags">', '</div>'))) {
work_data.status.push(get_label(matched[1]));
}
html.between('<div class="content-statuses">', '</div>')
// additional tags
.each_between(' class="content-status', '<',
//
function(text) {
work_data.status.push(get_label(text.between('>')));
});
work_data.status = work_data.status.unique();
// <h2>作品の情報</h2>
extract_work_data(work_data, html,
/<th>([\s\S]+?)<\/th>[\s\n]*<td[^<>]*>([\s\S]+?)<\/td>/g);
extract_work_data(work_data, html);
if (work_data.image
// ignore site default image
&& work_data.image.endsWith('\/ogp.png')) {
delete work_data.image;
}
// console.log(work_data);
return work_data;
},
// for AlphaPolis.js , AlphaPolis_user_manga.js
get_chapter_list : function(work_data, html) {
work_data.chapter_list = [];
var _this = this;
html = html.between('<div class="nav">', '<div class="freespace">')
//
.each_between('<a href="/'
//
+ _this.work_type + '/', '</a>', function(text) {
work_data.chapter_list.push({
url : '/' + _this.work_type + '/'
//
+ text.between(null, '"'),
date : text.between('<span class="open-date">', '</span>')
//
.to_Date({
zone : work_data.time_zone
}),
title : text.between(' class="title">',
// '<span class="title"><span
// class="bookmark-dummy"></span>',
// '</span>'
'<span class="open-date">')
});
});
},
// for AlphaPolis_user_manga.js , AlphaPolis_official_manga.js
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
// console.log(html);
var chapter_data = work_data.chapter_list[chapter_NO - 1];
Object.assign(chapter_data, {
// 設定必要的屬性。
title : get_label(html.between('<h2>', '</h2>')),
image_list : []
});
html.each_between('_pages.push("', '"', function(url) {
if (url.includes('://'))
chapter_data.image_list.push(url);
});
return chapter_data;
}
};
// --------------------------------------------------------------------------------------------
function new_AlphaPolis_crawler(configuration, callback, initializer) {
// library_namespace.set_debug(9);
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
if (configuration.need_create_ebook) {
library_namespace.run([ 'application.storage.EPUB'
// CeL.detect_HTML_language()
, 'application.locale' ]);
}
// 每次呼叫皆創建一個新的實體。
var crawler = new library_namespace.work_crawler(configuration);
// for 年齢確認 eternityConfirm()
crawler.setup_value('cookie', [ 'confirm='
+ Math.floor(Date.now() / 1000)
// location.hostname
+ ';domain=' + crawler.base_URL.match(/\/\/([^\/]+)/)[1]
+ ';path=/;' ]);
return crawler;
}
return new_AlphaPolis_crawler;
}

View File

@@ -0,0 +1,375 @@
/**
* @name CeL module for downloading PTCMS novels.
*
* @fileoverview 本檔案包含了解析並處理、批量下載中國大陸常見小說管理系統: PT小说聚合程序 (PTCMS系统) 各個版本的工具。
*
* <code>
CeL.PTCMS(configuration).start(work_id);
</code>
*
* TODO: 去掉前後網站廣告。
*
* @see https://www.ptcms.com/
* @see http://down.chinaz.com/test/201210/2252_1.htm 杰奇小说连载系统 杰奇原创文学系统,
* https://zhidao.baidu.com/question/518711125119801445.html 奇文网络小说管理系统
* 终点小说网站管理系统 露天中文小说网站管理系统 https://zhidao.baidu.com/question/474414436.html
* https://www.ptcms.com/ 关关采集器
*
* @see https://github.com/LZ0211/Wedge/tree/master/lib/Sites/plugins
* https://github.com/lufengfan/NovelDownloader
* https://github.com/unclezs/NovelHarvester
* @see http://www.sodu.cc/default.html
* https://kknews.cc/zh-tw/culture/oqyx5.html https://tw.hjwzw.com/
* @see http://www.76wx.com/ http://www.xssk.net/
*
* @since 2017/6/19 21:15:40 模組化。
*/
// More examples:
// @see https://github.com/kanasimi/work_crawler/blob/master/81xsw.js
// @see https://github.com/kanasimi/work_crawler/blob/master/23us.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.PTCMS',
require : 'application.net.work_crawler.'
//
+ '|application.storage.EPUB.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
function is_server_error(result) {
// TODO: 81xsw 有時會 403需要重新再擷取一次。
return result in {
// 88dus 有時會 502需要重新再擷取一次。
'502 Bad Gateway' : true,
// 630book 有時會 503需要重新再擷取一次。
'503 Service Unavailable' : true,
// 630book 有時會 "500 - 内部服务器错误。"
'服务器错误' : true
};
}
var default_configuration = {
// auto_create_ebook, automatic create ebook
// MUST includes CeL.application.locale!
need_create_ebook : true,
// recheck:從頭檢測所有作品之所有章節。
// 'changed': 若是已變更,例如有新的章節,則重新下載/檢查所有章節內容。
recheck : 'changed',
// base_URL : 'http://www.*.com/',
// charset : 'gbk',
// 解析 作品名稱 → 作品id get_work()
// search_URL : '',
// for 笔趣阁
parse_search_result_biquge : function(html, get_label) {
// console.log(html);
var matched = html
.match(/og:url" content="[^<>"]+?\/(?:\d+_)?(\d+)\/"/);
if (matched) {
return [ [ +matched[1] ],
[ get_label(html.between('og:title" content="', '"')) ] ];
}
matched = html.match(/blockcontent">([\s\S]+)<\/div>/);
if (matched) {
/**
* <code>
// xbiquge.cc.js:
<div class="blocktitle">出现错误!</div><div class="blockcontent"><div style="padding: 10px"><br /> 错误原因:对不起,两次搜索的间隔时间不得少于 30 秒<br /><br /> 请 <a href="javascript:history.back(1)">返 回</a> 并修正<br /><br /></div><div style="width: 100%; text-align: right; line-height: 200%; padding-right: 10px;">[<a href="javascript:window.close()">关闭本窗口</a>]</div></div>
</code>
*/
matched = get_label(matched[1]).replace(/\n[\s\S]*/, '');
library_namespace.error(matched);
}
// console.trace(html);
var id_list = [], id_data = [];
html.each_between('<li>', '</li>', function(text) {
matched = text.match(
/**
* <code>
// biquge.js:
<span class="s2"><a href="https://www.xs.la/211_211278/" target="_blank">
万古剑神</a>
</span>
// xbiquge.js:
<span class="s2"><a href="http://www.xbiquge.cc/book/24276/">元尊</a></span>
// xbiquke.js
<span class="s1">1</span>
<span class="s2">
<a href="/29_29775/" target="_blank">
我真不是邪神走狗
</a>
</span>
<span class="s4">
<a href="/author/29775/">
万劫火
</a>
</span>
<span class="s3">
<a style="color: Red;" href="/29_29775/22709468.html" target="_blank" title="番外·童年(一)">
番外·童年(一)</a>
</span>
<span class="s6">2021-11-26 02:30:39</span>
</code>
*/
/<a href="[^<>"]*?\/(?:\d+_)?(\d+)\/"[^<>]*>([\s\S]+?)<\/a>/);
// console.log([ text, matched ]);
if (matched) {
id_list.push(+matched[1]);
id_data.push(get_label(matched[2]));
}
});
return [ id_list, id_data ];
},
// 取得作品的章節資料。 get_work_data()
// work_URL : function(work_id) {
// /** @see this.work_URL in CeL.application.net.work_crawler */ },
parse_work_data : function(html, get_label, extract_work_data) {
// console.log(html);
// 由 meta data 取得作品資訊。
var work_data = {
// 必要屬性:須配合網站平台更改。
title : html.between('og:novel:title" content="', '"')
// e.g., 88dushu.js
|| html.between('og:title" content="', '"')
// 通常與og:title相同
|| html.between('og:novel:book_name" content="', '"'),
// 選擇性屬性:須配合網站平台更改。
author : html.between('og:novel:author" content="', '"'),
// e.g., 连载[中], [已]完成
status : html.between('og:novel:status" content="', '"'),
category : html.between('og:novel:category" content="', '"'),
// https://www.booktxt.net/: '<div id="fmimg">'
image : html.between('<div id="fmimg">', '</div>').between(
'<img src="', '"')
// general
|| html.between('og:image" content="', '"'),
last_update :
//
html.between('og:novel:update_time" content="', '"')
// e.g., 630book
|| html.between('og:novel:update_time" content=\'', "'"),
latest_chapter : html.between(
'og:novel:latest_chapter_name" content="', '"'),
description : get_label(
//
html.between('og:description" content="', '"')
// e.g., 630book
|| html.between('<div id="intro">', '</div>'))
// 偶爾會有沒填上描述的書籍。
|| '',
language : 'cmn-Hans-CN',
site_name : get_label(
//
html.between('<div class="logo">', '</div>')
//
|| html.between('<div class="header_logo">', '</div>')
// e.g., 630book
|| html.between('<strong class="logo">', '</strong>'))
};
// 由 meta data 取得作品資訊。
extract_work_data(work_data, html);
if (is_server_error(work_data.title)) {
return this.REGET_PAGE;
}
if (this.extract_work_data) {
// e.g., xbiquke.js
this.extract_work_data(work_data, html, get_label,
extract_work_data);
}
if (/^\d{1,2}-\d{1,2}$/.test(work_data.last_update)) {
// e.g., 07-01 → 2017-07-01
work_data.last_update = (new Date).getFullYear() + '-'
+ work_data.last_update;
}
if (work_data.site_name.includes('?$')) {
// e.g., 88dushu.js
work_data.site_name = html.between("AddFavorite('", "'");
}
// console.log(work_data);
return work_data;
},
// 取得包含章節列表的文字範圍。
// get_chapter_list_contents : function(html) {return html.between();},
get_chapter_list : function(work_data, html, get_label) {
// determine base directory of work
work_data.base_url = work_data.url.endsWith('/') ? work_data.url
: work_data.url.replace(/\.[^.]+$/, '/');
if (work_data.base_url.startsWith(this.base_URL)) {
work_data.base_url = work_data.base_url
.slice(this.base_URL.length - 1);
}
if (this.get_chapter_list_contents) {
html = this.get_chapter_list_contents(html);
}
// console.log(html);
work_data.chapter_list = [];
var part_title, matched,
// 章節以及篇章連結的模式。
// [ all, tag name, attributes, 連結內容 HTML ]
PATTERN_chapter = /<(li|dd|dt)([^<>]*)>([\s\S]*?)<\/\1>/g;
while (matched = PATTERN_chapter.exec(html)) {
if (false) {
delete matched.input;
console.log(matched);
}
if (matched[1] === 'dt' ||
// e.g., 88dushu.js
matched[1] === 'li' && matched[2].includes('class="fj"')) {
part_title = get_label(matched[3]);
if (part_title.includes('最新章节') && part_title.length > 20) {
// e.g., 《...》最新章节(提示:已启用缓存技术,最新章节可能会延时显示,登录书架即可实时查看。)
// e.g., ...最新章节列表 (本页已经缓存,请加入书架查看...最新章节)
part_title = 'pass';
} else if (part_title.includes('正文')) {
// e.g., 《...》正文卷, 《...》正文
part_title = '';
}
// console.log(part_title);
} else if (part_title !== 'pass'
// 取得連結內容。
&& (matched = matched[3].between('<a ', '</a>'))) {
var chapter_data = {
// 從href取得章節的網址。
url : matched.between('href="', '"')
// xbiquge.js: 交錯使用 "", ''
|| matched.between("href='", "'")
// booktxt.js: 交錯使用 "", ''
|| matched.between('href ="', '"'),
part_title : part_title,
// 從title/顯示的文字取得章節的標題。
title : matched.between('title="', '"')
|| get_label(matched.between('>'))
};
work_data.chapter_list.push(chapter_data);
}
}
// console.log(work_data.chapter_list);
},
// 取得每一個章節的內容與各個影像資料。 get_chapter_data()
chapter_URL : function(work_data, chapter_NO) {
var url = work_data.chapter_list[chapter_NO - 1].url;
// console.trace(url);
url = url.startsWith('/') || url.includes('://') ? url
: work_data.base_url + url;
// console.trace(url);
return url;
},
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
if (!html && this.skip_error === true) {
// Skip empty chapter
return;
}
// 在取得小說章節內容的時候,若發現有章節被目錄漏掉,則將之補上。
this.check_next_chapter(work_data, chapter_NO, html,
this.PATTERN_next_chapter);
var chapter_data = work_data.chapter_list[chapter_NO - 1],
//
sub_title = get_label(html.between('<h1>', '</h1>'))
// || get_label(html.between('<H1>', '</H1>'))
// || chapter_data.title
, text = (html
// general: <div id="content">
// xbiquge.js: <div id="content" name="content">
.between('<div id="content"', '</div>').between('>')
// 去除掉廣告。
// e.g., 88dushu.js
|| html.between('<div class="yd_text2">', '</div>')).replace(
/<script[^<>]*>[^<>]*<\/script>/g, ''),
//
KEY_interval_cache = 'original_chapter_time_interval';
if (is_server_error(sub_title) && text.length < 2000) {
this[KEY_interval_cache] = this.chapter_time_interval;
// 當網站不允許太過頻繁的訪問/access時可以設定下載之前的等待時間(ms)。
this.chapter_time_interval = 10 * 1000;
return this.REGET_PAGE;
}
if (KEY_interval_cache in this) {
// recover time interval
if (this[KEY_interval_cache] > 0) {
this.chapter_time_interval = this[KEY_interval_cache];
} else {
delete this.chapter_time_interval;
}
delete this[KEY_interval_cache];
}
if (this.remove_ads) {
text = this.remove_ads(text);
}
// console.log(text);
this.add_ebook_chapter(work_data, chapter_NO, {
title : chapter_data.part_title,
sub_title : sub_title,
text : text
});
}
};
// --------------------------------------------------------------------------------------------
function new_PTCMS_novels_crawler(configuration) {
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
if (configuration.parse_search_result === 'biquge') {
configuration.parse_search_result = configuration.parse_search_result_biquge;
}
// 每次呼叫皆創建一個新的實體。
return new library_namespace.work_crawler(configuration);
}
return new_PTCMS_novels_crawler;
}

View File

@@ -0,0 +1,634 @@
/**
* @name CeL module for downloading SinMH CMS comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載中國大陸常見漫畫管理系統: 圣樱漫画管理系统 (圣樱CMS) MHD模板 PC端 的工具。
*
* <code>
CeL.SinMH(configuration).start(work_id);
</code>
*
* TODO: ONE漫画 https://www.onemanhua.com/ 可能是比 930mh.js 更舊的版本?
*
* @see https://cms.shenl.com/sinmh/
* @see https://www.manhuadui.com/js/common.js "Created by Shen.L on 2016/1/28."
*
* @since 2018/7/26 11:9:53 模組化 MHD模板。<br />
* 2019/2/4 add 930mh.js 使用 CryptoJS採用 DMZJ模板。<br />
* 2019/7/2 50mh.js 使用 CryptoJS採用 DMZJ模板。
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/36mh.js
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/gufengmh.js
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/930mh.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.SinMH',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
var default_configuration = {
// 嘗試取得被屏蔽的作品。
// 對於被屏蔽的作品,將會每次都從頭檢查。
try_to_get_blocked_work : true,
// 當有多個分部的時候才重新檢查。
recheck : 'multi_parts_changed',
// allow .jpg without EOI mark.
// allow_EOI_error : true,
// 當圖像檔案過小,或是被偵測出非圖像(如不具有EOI)時,依舊強制儲存檔案。
// skip_error : true,
// 提取出引數(如 URL中的作品ID 以回傳。
extract_work_id : function(work_information) {
// e.g,
// https://www.36mh.com/manhua/IDOLiSH7ouxiangxingyuanxiangliuxingxuyuan/
return /^[a-z\d]+$/i.test(work_information) && work_information;
},
// 取得伺服器列表。
// use_server_cache : true,
server_URL : 'js/config.js',
parse_server_list : function(html) {
var server_list = [], SinConf;
// console.trace(html);
if (/^\s*var cars/.test(html)) {
// for manhuaniu.js 2021/1/19 改版
html = html.replace('var SinConf', 'SinConf').replace(
/\n}\(\);[\s\S]*/, '}();SinConf.cars=cars;');
} else {
html = html.replace('var ', '').replace(/(}\(\))[\s\S]*/, '$1');
}
// console.trace(html);
eval(html);
function append_path(host) {
return host.endsWith('/') ? host : host + '/';
}
SinConf.resHost.map(function(data) {
server_list.append(data.domain.map(append_path));
});
if (SinConf.cars) {
server_list.append(SinConf.cars.map(append_path));
}
server_list = server_list.unique();
// for manhuaniu.js 2021/1/19 改版
server_list = server_list.filter(function(server) {
return !server.includes('restp.dongqiniqin');
});
server_list.conf = SinConf;
// console.log(SinConf);
// console.log(server_list);
return server_list;
},
// 解析 作品名稱 → 作品id get_work()
// 1. 使用 PC端 網頁取得搜尋所得的作品資料。 (default)
search_URL : 'search/?keywords=',
// 2. 使用API取得搜尋所得的作品資料。 (set search_URL:'API')
search_URL_API : function(work_title) {
// SinConf.apiHost
var apiHost = this.api_base_URL
|| this.base_URL.replace(/\/\/[a-z]+/, '//api');
return [ apiHost + 'comic/search', {
keywords : work_title
} ];
},
parse_search_result : function(html, get_label) {
// console.log(html);
if (html.startsWith('{')) {
// 2. 使用API取得搜尋所得的作品資料。
/**
* e.g.,<code>
{"items":[{"id":3542,"status":1,"commend":0,"is_original":0,"is_vip":0,"name":"军阀霸宠:纯情妖女火辣辣","title":"民国妖闻录","alias":"","original_name":"","letter":"j","slug":"junfabachongchunqingyaonuhuolala","coverUrl":"http://res.gufengmh.com/images/cover/201711/1509877682Xreq-5mrrSsDm82P.jpg","uri":"/manhua/junfabachongchunqingyaonuhuolala/","last_chapter_name":"040纯良少年的堕落","last_chapter_id":235075,"author":"逐浪动漫","author_id":3901,"serialise":1}],"_links":{"self":{"href":"http://api.gufengmh.com/comic/search?page=1"}},"_meta":{"totalCount":1,"pageCount":1,"currentPage":1,"perPage":20},"status":0}
</code>
*/
var id_data = html ? JSON.parse(html).items : [];
// console.log(id_data);
return [ id_data, id_data ];
}
// 1. 使用 PC端 網頁取得搜尋所得的作品資料。
// e.g., 36mh.js
var id_list = [], id_data = [], matched,
// matched: [ all, url, inner (title) ]
PATTERN_search = /<p class="ell"><a href="([^<>"]+)">([^<>]+)/g;
if (matched = html.between('<h4 class="fl">')) {
html = matched;
// matched: [ all, url, inner (title) ]
// PATTERN_search = /<p class="ell"><a
// href="([^<>"]+)">([^<>]+)/g;
} else if (matched = html.between('<div id="update_list">')) {
// 行動版 mobile version
// e.g., <div id="update_list"><div class='UpdateList'><div
// class="itemBox" data-key="10992">
html = matched;
// e.g., <a class="title"
// href="https://m.36mh.com/manhua/dushizhixiuzhenguilai/"
// target="_blank">都市之修真归来</a>
// matched: [ all, url, inner (title) ]
PATTERN_search = /<a class="title" href="([^<>"]+)"[^<>]*>([^<>]+)/g;
} else {
// throw new Error('Unknown site!');
}
while (matched = PATTERN_search.exec(html)) {
// .html: mh1234.js
matched[1] = matched[1].match(/([^\/]+)(?:\/|\.html)$/);
id_list.push(matched[1][1]);
id_data.push(get_label(matched[2]));
}
return [ id_list, id_data ];
},
// e.g., 50mh.js
// id_of_search_result : 'slug',
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
// console.log(work_id);
return 'manhua/' + work_id + '/';
},
parse_work_data : function(html, get_label, extract_work_data) {
// console.log(html);
var work_data = {
// 必要屬性:須配合網站平台更改。
title : get_label(html.between('<h1>', '</h1>')),
// 選擇性屬性:須配合網站平台更改。
description : get_label(html.between('intro-all', '</div>')
.between('>')
// 930mh.js
|| html.between('<p class="comic_deCon_d">', '</p>')
// copy from 733mh.js: for mh1234.js
|| html.between(
'<div class="introduction" id="intro1">',
'</div>'))
};
// <div class="book-detail pr fr">
// <ul class="detail-list cf">
// ...
// </ul>
// <a class="intro-act" id="intro-act" href="javascript:;">展開詳情</a>
extract_work_data(work_data, html.between('detail-list', '</ul>'),
// e.g., "<strong>漫画别名:</strong>暂无</span>"
// gufengmh.js:<li><span><strong>漫画类型:</strong>...</span><span><strong>漫画作者:</strong>...</span></li>
/<strong[^<>]*>([^<>]+)<\/strong>([\s\S]+?)<\/(?:li|span)>/g);
// console.log(html.between('detail-list', '</ul>'));
// console.log(work_data);
// 930mh.js
extract_work_data(work_data, html
.between('<ul class="comic_deCon_liT">',
'<p class="comic_deCon_d">')
// <li>时间2019-02-04 <li>最新:<a
// href="/manhua/17884/668443.html">第6话</a></li>
.replace(/<li>/g, '</li><li>'),
// e.g., "<li>类别:<a href="/list/shaonian/">少年</a></li>"
/<li>([^]+)([\s\S]+?)<\/li>/g);
// copy from 733mh.js: for mh1234.js
extract_work_data(work_data, html.between('<div class="info">',
'<div class="info_cover">'),
/<em>([^<>]+?)<\/em>([\s\S]*?)<\/p>/g);
// 由 meta data 取得作品資訊。
extract_work_data(work_data, html);
Object.assign(work_data, {
author : work_data.漫画作者 || work_data.漫畫作者 || work_data.作者
|| work_data.原著作者,
status : work_data.漫画状态 || work_data.漫畫狀態 || work_data.状态,
last_update : work_data.更新时间 || work_data.时间,
latest_chapter : work_data.最新 || work_data.更新至
|| get_label(html.between('<span class="text">更新至',
// for 36mh.js: "更新至:", 999comics.js: "更新至:"
'</span>').replace(/^[:]/, '')),
latest_chapter_url : html.between('最新:<a href="', '"')
// for 36mh.js
|| html.between('更新至 [ <a href="', '"')
// gufengmh.js
|| html.between('更新至:</strong><a href="', '"')
});
// console.log(work_data);
if (!work_data.last_update && work_data.status) {
// for 36mh.js
var matched = work_data.status
.match(/^([\s\S]+?)最近[于於]([\s\S]+?)$/);
if (matched) {
Object.assign(work_data, {
status : matched[1],
last_update : matched[2].replace(
/^[\s\n]*\[|\][\s\n]*$/g, '').trim()
});
}
}
if (!work_data.last_update) {
// for 999comics.js
var matched = html.match(/最近[于於]([\s\S]+?)<\//);
// console.log(matched);
if (matched) {
work_data.last_update = get_label(matched[1].replace(
/^[\s\n]*\[|\][\s\n]*$/g, ''));
}
}
// console.log(work_data);
return work_data;
},
get_chapter_list : function(work_data, html, get_label) {
// console.log(work_data);
var chapter_block, PATTERN_chapter_block = html
.includes('class="chapter-body')
// <div class="chapter-category clearfix">
// <div class="chapter-body clearfix">
? /class="chapter-(body|category)[^<>]+>([\s\S]+?)<\/div>/g
// 930mh.js
// <div class="zj_list_head">...<h2>章节<em class="c_3">列表</em></h2>
// <div class="zj_list_head_px" data-key="6"><span>排序 :...</div>
// <div class="zj_list_con autoHeight">...</div>
: /class="zj_list_(con|head)[^<>]+>([\s\S]+?)<\/div>/g,
//
latest_chapter_list = work_data.chapter_list;
// reset work_data.chapter_list
work_data.chapter_list = [];
// 漫畫目錄名稱不須包含分部號碼。使章節目錄名稱不包含 part_NO。
// 將會在 function get_chapter_directory_name() 自動設定。
// work_data.chapter_list.add_part_NO = false;
while (chapter_block = PATTERN_chapter_block.exec(html)) {
// delete chapter_block.input;
// console.log(chapter_block);
if (chapter_block[1] === 'category') {
// console.log(chapter_block[2]);
// e.g., 决断地 @ gufengmh
chapter_block = chapter_block[2]
// <div class="caption pull-left"><span>章节</span></div>
// <div class="caption pull-left"><span>单话</span></div>
.match(/class="caption[^<>]+>([\s\S]+)/);
// console.log(chapter_block);
if (chapter_block) {
this.set_part(work_data, chapter_block[1]);
}
continue;
}
if (chapter_block[1] === 'head') {
// console.log(chapter_block[2]);
// 930mh.js
// e.g., http://www.duzhez.com/manhua/269/
chapter_block = chapter_block[2]
// <h2>章节<em class="c_3">列表</em></h2>
// <h2>番外篇<em class="c_3">列表</em></h2>
.between('<h2>', '<em class="c_3">列表</em>');
// console.log(chapter_block);
if (chapter_block) {
this.set_part(work_data, chapter_block);
}
continue;
}
chapter_block = chapter_block[2];
var link, PATTERN_chapter_link =
//
/<a href="([^<>"]+)"[^<>]*>([\s\S]+?)<\/a>/g;
while (link = PATTERN_chapter_link.exec(chapter_block)) {
if (link[1].startsWith('javascript:')) {
// 本站应《 》版权方要求现已屏蔽删除本漫画所有章节链接,只保留作品文字信息简介以及章节目录
continue;
}
var chapter_data = {
url : link[1],
title : get_label(link[2])
};
this.add_chapter(work_data, chapter_data);
// console.log(work_data.chapter_list);
// console.log(chapter_data);
}
}
this.check_filtered(work_data, html, get_label,
//
latest_chapter_list);
work_data.inverted_order = this.chapter_inverted_order;
// console.log(work_data.chapter_list);
// throw work_data.chapter_list.length;
},
// 注意:在呼叫本函數之前,不可改變 html
check_filtered : function(work_data, html, get_label,
latest_chapter_list) {
// console.log(work_data);
// console.log(work_data.chapter_list);
var text = work_data.chapter_list.length === 0 && get_label(
/**
* 已屏蔽删除本漫画所有章节链接 e.g., <code>
// 930mh.js 一人之下
<div class="zj_list_con autoHeight">
<p class="ip-notice" style="padding:10px;color: red;background:snow;font-size:14px;width:875px;">
尊敬的各位喜爱一人之下漫画的用户,本站应《一人之下》版权方要求现已屏蔽删除本漫画所有章节链接,只保留作品文字信息简介以及章节目录,请喜欢一人之下的漫友购买杂志或到官网付费欣赏。为此给各位漫友带来的不便,敬请谅解!
</p>
</div>
// mh1234.js
<div class="ip-body">
<p class="ip-notice">
尊敬的各位喜爱妖精种植手册漫画的用户,本站应《妖精种植手册 》版权方要求现已屏蔽删除本漫画所有章节链接,只保留作品文字信息简介以及章节目录,请喜欢妖精种植手册 的漫友购买杂志或到官网付费欣赏。为此给各位漫友带来的不便,敬请谅解!
</p>
<p>
版权方在线阅读地址: <span><a href="http://www.mh1234.com" rel="nofollow">http://www.mh1234.com</a></span>
</p>
</div>
</code>
*/
html.between('<p class="ip-notice"', '</p>').between('>')
//
|| html.between('class="ip-body">', '</div>'));
// console.log(text);
if (!text) {
return;
}
work_data.removed = text;
var chapter_id = html.between('href="/comic/read/?id=', '"')
|| html.between('SinMH.initComic(', ')')
|| html.between('SinTheme.initComic(', ')')
|| html.between('var pageId = "comic.', '"');
if (this.try_to_get_blocked_work && chapter_id) {
library_namespace.info([ work_data.title || work_data.id, ': ',
{
// gettext_config:{"id":"trying-to-get-the-blocked-work"}
T : '嘗試取得被屏蔽的作品。'
} ]);
if (Array.isArray(latest_chapter_list)
// e.g., 全职法师, 一人之下 http://www.duzhez.com/manhua/1532/
&& latest_chapter_list.length > 1
//
&& (!this.recheck || this.recheck in {
changed : true,
multi_parts_changed : true
})) {
library_namespace.info({
// gettext_config:{"id":"using-the-previous-cache-to-download-§$1"}
T : [ '使用之前的快取,自 §%1 接續下載。',
latest_chapter_list.length ]
});
// 這可以保留 work_data.chapter_list 先前的屬性。
work_data.chapter_list = Object.assign(latest_chapter_list,
work_data.chapter_list);
work_data.last_download.chapter = latest_chapter_list.length;
} else {
this.add_chapter(work_data,
//
'/comic/read/?id=' + chapter_id);
}
} else {
library_namespace.warn(text);
}
},
pre_parse_chapter_data
// 執行在解析章節資料 process_chapter_data() 之前的作業 (async)。
// 必須自行保證執行 callback(),不丟出異常、中斷。
: function(XMLHttp, work_data, callback, chapter_NO) {
var html = XMLHttp.responseText;
if (work_data.removed && chapter_NO === 1) {
var first_chapter_id = html.between('SinMH.initChapter(', ',')
|| html.between('SinTheme.initChapter(', ',');
// console.log(html);
if (first_chapter_id) {
library_namespace.debug('add first chapter: '
+ first_chapter_id);
var url = this.work_URL(work_data.id) + first_chapter_id
+ '.html';
work_data.chapter_list[chapter_NO - 1].url = url;
this.get_URL(url, callback, null, {
error_retry : this.MAX_ERROR_RETRY,
no_warning : true
});
return;
}
}
var crypto_url = html
// 930mh.js: Error on http://www.duzhez.com/manhua/449/245193.html
&& html
// https://www.manhuadui.com/manhua/haizeiwang/296660.html :
// <script
// src="https://cdn.staticfile.org/crypto-js/3.1.9-1/crypto-js.js"></script>
.match(/<script src="([^"]+\/crypto(?:-js)?\.js)"><\/script>/);
// console.log(crypto_url);
if (crypto_url) {
var file_name = this.main_directory + 'crypto.js';
// TODO: this is a workaround to pass to require()
if (!library_namespace.is_absolute_path(file_name)) {
file_name = process.cwd()
+ library_namespace.env.path_separator + file_name;
}
// console.log(file_name);
library_namespace.get_URL_cache(this.full_URL(crypto_url[1]),
// @see function cops201921() @
// http://www.duzhez.com/js/cops201921.js
function(data, error, XMLHttp) {
// data = data.toString();
// @see https://code.google.com/archive/p/crypto-js/
// 懶得自己寫,直接 including。
global.CryptoJS = require(file_name);
callback();
}, {
file_name : file_name,
get_URL_options : this.get_URL_options
});
return;
}
callback();
},
// 取得每一個章節的各個影像內容資料。 get_chapter_data()
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
// console.log(html);
if (work_data.removed && !work_data.chapter_filtered) {
var next_chapter_data = html.between('nextChapterData =', ';');
// console.log(next_chapter_data || html);
if (next_chapter_data
// next_chapter_data==='' @
// https://www.mh1234.com/comic/9384.html
&& (next_chapter_data = JSON.parse(next_chapter_data))
&& next_chapter_data.id > 0) {
library_namespace.debug('add chapter: '
+ next_chapter_data.id);
next_chapter_data.url = this.work_URL(work_data.id)
+ next_chapter_data.id + '.html';
// 動態增加章節。
work_data.chapter_count++;
work_data.chapter_list.push(next_chapter_data);
} else {
// console.log(html);
}
}
// console.log(work_data.chapter_list);
var chapter_data = work_data.chapter_list[chapter_NO - 1],
// <!--全站头部导航 结束-->\n<script>
chapter_data_code = html
// 930mh.js: Error on http://www.duzhez.com/manhua/449/245193.html
&& (html.match(/<script>(;var [\s\S]+?)<\/script>/)
// for manhuaniu.js 2021/1/19 改版
|| html.match(/<script>(var siteName = "";[\s\S]+?)<\/script>/));
// console.trace(chapter_data_code);
if (!chapter_data_code) {
library_namespace.warn({
// gettext_config:{"id":"unable-to-parse-chapter-data-for-«$1»-§$2"}
T : [ '無法解析《%1》§%2 之章節資料!', work_data.title, chapter_NO ]
});
return;
}
// console.trace(chapter_data_code[1]);
// eval(chapter_data_code[1].replace(/;var /g, ';chapter_data.'));
chapter_data_code[1].split(';var ').forEach(function(token) {
if (!token.includes('='))
return;
token = token.replace(/^\s*var\s/, '');
// console.trace(token);
try {
eval('chapter_data.' + token);
} catch (e) {
console.error(new SyntaxError(
// Ignore SyntaxError. e.g.,
// https://www.gufengmh8.com/manhua/wodeshashounanyou/742494.html
// ;var pageTitle = "我的杀手男友第65、66话 "肉偿在线观看";
'parse_chapter_data: ' + token));
}
});
// console.log(chapter_data);
// 設定必要的屬性。
chapter_data.title = get_label(html.between('<h2>', '</h2>'))
// e.g., mh1234.js has no <h2>...</h2>'
|| chapter_data.title;
// e.g., 'images/comic/4/7592/'
var path = encodeURI(chapter_data.chapterPath);
// console.log(chapter_data.chapterImages);
if (global.CryptoJS
&& typeof chapter_data.chapterImages === 'string') {
// console.log(chapter_data.chapterImages);
// console.log(this.crypto);
/**
* <code>
JSON.parse(CryptoJS.AES.decrypt(chapterImages,CryptoJS.enc.Utf8.parse("6133AFVvxas55841"),{iv:CryptoJS.enc.Utf8.parse("A25vcxQQrpmbV51t"),mode:CryptoJS.mode.CBC,padding:CryptoJS.pad.Pkcs7}).toString(CryptoJS.enc.Utf8))
</code>
*
* @see https://segmentfault.com/q/1010000011225051
*/
chapter_data.chapterImages =
// 使用 CryptoJS https://code.google.com/archive/p/crypto-js/
// https://github.com/brix/crypto-js
JSON.parse(CryptoJS.AES.decrypt(chapter_data.chapterImages,
// 930mh.js key 密鑰 "十六位字符作为密钥"
CryptoJS.enc.Utf8.parse(this.crypto.key), {
iv : CryptoJS.enc.Utf8.parse(this.crypto.iv),
mode : CryptoJS.mode.CBC,
padding : CryptoJS.pad.Pkcs7
}).toString(CryptoJS.enc.Utf8));
}
// assert: Array.isArray(chapter_data.chapterImages)
chapter_data.image_list = chapter_data.chapterImages.map(function(
url) {
return {
// e.g., 外挂仙尊 184 第76话
// 但是這還是沒辦法取得圖片...
url : encodeURI(/^https?:\/\//.test(url) ? url
//
: path + url)
}
});
if (chapter_data.image_list.length === 0
&& (html = html.between('class="ip-notice">', '<'))) {
// 避免若連內容被屏蔽,會從頭檢查到尾都沒有成果。
work_data.chapter_filtered = true;
if (work_data.removed) {
library_namespace.info({
// gettext_config:{"id":"§$1-has-been-blocked-and-no-longer-attempts-to-resolve-other-chapters"}
T : [ '§%1 已被屏蔽,不再嘗試解析其他章節。', chapter_NO ]
});
} else {
library_namespace.warn(get_label(html));
}
}
// console.log(chapter_data);
return chapter_data;
}
};
// --------------------------------------------------------------------------------------------
function new_SinMH_comics_crawler(configuration) {
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
if (configuration.search_URL === 'API') {
configuration.search_URL = default_configuration.search_URL_API;
// 因為不見得會執行到 parse_search_result(),不可放在 parse_search_result() 裡面。
if (!configuration.id_of_search_result) {
// gufengmh.js: using 'slug'
configuration.id_of_search_result = 'id';
}
configuration.title_of_search_result = 'title';
}
// 每次呼叫皆創建一個新的實體。
return new library_namespace.work_crawler(configuration);
}
// for CeL.application.net.work_crawler.sites.SinMH2013
new_SinMH_comics_crawler.default_configuration = default_configuration;
return new_SinMH_comics_crawler;
}

View File

@@ -0,0 +1,288 @@
/**
* @name CeL module for downloading SinMH CMS? comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載中國大陸常見漫畫管理系統: 可能是 圣樱漫画管理系统 (圣樱CMS) 2013年版 的工具。
*
* TODO: https://m.999comics.com/
*
* <code>
CeL.SinMH2013(configuration).start(work_id);
</code>
*
* modify from archive/2manhua.js
*
* 57mh 介面程式碼類似於 999comics。manhuagui 似乎是在這基礎上經過修改? 57mh 這一批介面外觀與
* CeL.application.net.work_crawler.sites.SinMH 類似,但介面程式碼有些差距。或可稱為
* CeL.application.net.work_crawler.sites.SinMH2013 或
* CeL.application.net.work_crawler.sites.SMH。
*
* @see https://www.999comics.com/static/scripts/main.js?v=1.0 MHD (MHD: 漫画岛
* http://www.manhuadao.com/book/baiqianjiadeyaoguaiwangzi/)
* http://www.wuqimh.com/templates/wuqi/default/scripts/main.js?v=1.0.3 MHW
* https://cf.hamreus.com/scripts_tw/main_EB87BCACAD66FA68CA738D0C925DC508.js
* main_EB87BCACAD66FA68CA738D0C925DC508.js 末: SMH = { update: "2013/4/1" }
*
* @since 2019/6/18 6:13:11 模組化 MHD模板?
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/57mh.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.SinMH2013',
require : 'application.net.work_crawler.'
//
+ '|application.net.work_crawler.sites.SinMH.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
var default_configuration = {
// 所有的子檔案要修訂註解說明時應該都要順便更改在CeL.application.net.comic中Comic_site.prototype內的母comments並以其為主體。
// 本站常常無法取得圖片,因此得多重新檢查。
// recheck:從頭檢測所有作品之所有章節與所有圖片。不會重新擷取圖片。對漫畫應該僅在偶爾需要從頭檢查時開啟此選項。
// recheck : true,
// 當無法取得chapter資料時直接嘗試下一章節。在手動+監視下recheck時可併用此項。
// skip_chapter_data_error : true,
// 36145 前任攻略 19话 057.jpg
// {Natural}MIN_LENGTH:最小容許圖案檔案大小 (bytes)。
MIN_LENGTH : 400,
// 當圖像檔案過小,或是被偵測出非圖像(如不具有EOI)時,依舊強制儲存檔案。
skip_error : true,
// one_by_one : true,
// base_URL : '',
// 取得伺服器列表。
// use_server_cache : true,
// http://www.5qmh.com/templates/wuqi/default/scripts/configs.js?v=1.0.3
server_URL : 'templates/wuqi/default/scripts/configs.js',
parse_server_list : function(html) {
// console.log(html);
var server_list = [];
Object.values(JSON.parse(
// var pageConfig = { 'host': { ...
html.replace(/^[^{]+/, '').replace(/[^}]+$/, '')
//
.replace(/'/g, '"')).host)
//
.forEach(function(_server_list) {
_server_list.forEach(function(server) {
if (server) {
if (!server.endsWith('/'))
server += '/';
server_list.push(server);
}
});
});
return server_list;
},
// 解析 作品名稱 → 作品id get_work()
search_URL : 'handler/suggest?cb=_&key=',
parse_search_result : function(html) {
// console.log(html);
// e.g.,
// _([{"id":"28015","t":"民工勇者","u":"/comic/28015/","cid":"/comic/28015/0208","ct":"207话","s":"0"},{"id":"28093","t":"无敌勇者王(民工勇者)","u":"/comic/28093/","cid":"/comic/28093/02","ct":"199话","s":"0"}])
var id_data = html ? JSON.parse(html.between('(').replace(
/\)[^)]*$/, '')) : [];
return [ id_data, id_data ];
},
id_of_search_result : function(cached_data) {
return cached_data.id | 0;
},
title_of_search_result : 't',
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
// e.g., http://www.5qmh.com/28437/
return work_id + '/';
},
parse_work_data : library_namespace.SinMH.default_configuration.parse_work_data,
get_chapter_list : function(work_data, html, get_label) {
// console.log(html);
work_data.chapter_list = [];
var matched, part_title, part_NO = 0, page,
// 2017/7/22: 57mh.js 章節編號順序為 21 43 65.
// e.g., 银魂 http://www.wuqimh.com/8/
// matched: [ all, part_title, page inner ]
PATTERN_page = /<h4><span>(.+?)<\/span><\/h4>|<ul (?:style="display:block;")?>([\s\S]+?)<\/ul>/g,
/**
* e.g., 57mh.js 只發現過多page頁面沒有發現過多part <code>
<h4><span>单话</span></h4><div class="chapter-page cf mt10" id="chpater-page-1"><ul>
<li ><a href="javascript:;" title="第1页">第1页<i></i></a></li>
...
<li class="on"><a href="javascript:;" title="第7页">第7页<i></i></a></li>
</ul></div>
<div class="chapter-list cf mt10" id="chpater-list-1"><ul >
<li><a href="/8/065.html" title="155话" class="status0" target="_blank"><span>155话<i>21p</i></span></a></li>
...
</ul></div>
</code>
*
* e.g., 999comics.js 只發現過多part沒有發現過多page頁面 <code>
<h4><span>單話</span></h4>
<div class="chapter-list cf mt10">
<ul style="display:block;">
<li><a href="/comic/26060/72192e0511125993c37cbd5264c971b6.html" title="第371回" class="status0" target="_blank"><span>371回</span></a></li>
...
</ul>
</div><h4><span>番外篇</span></h4>
<div class="chapter-list cf mt10">
<ul style="display:block;">
<li><a href="/comic/26060/b0da0b7c0767f1332684a0ae111b3696.html" title="Jump next出張篇" class="status0" target="_blank"><span>Jump</span></a></li>
...
</ul>
</div>
</code> 單話 番外篇 單行本
*
* <code>
<li><a href="/comic/25652/072.html" title="72回 碧霞坠" class="status0" target="_blank"><span>72回<i>14p</i></span></a></li>
</code>
*/
PATTERN_chapter =
// matched: [ all, href, title, inner ]
/<li><a href="([^"<>]+)" title="([^"<>]+)"[^<>]*>(.+?)<\/a><\/li>/g;
while (page = PATTERN_page.exec(html)) {
if (page[1]) {
part_title = get_label(page[1]);
part_NO++;
// library_namespace.info('part_title: ' + part_title);
continue;
}
page = page[2];
// console.log(page);
var chapter_list = [];
while (matched = PATTERN_chapter.exec(page)) {
matched[2] = matched[2].trim();
if (matched[3] = matched[3].between('<i>', '</i>')) {
// add page count
matched[2] = matched[2] + ' ' + matched[3];
}
chapter_list.push({
part : part_title,
part_NO : part_NO,
title : get_label(matched[2]),
url : encodeURI(matched[1])
});
}
if (!this.no_need_to_revert)
chapter_list = chapter_list.reverse();
work_data.chapter_list.append(chapter_list);
}
work_data.chapter_list.part_NO = part_NO;
return;
// 已被棄置的排序方法。
work_data.chapter_list
.sort(function(chapter_data_1, chapter_data_2) {
var matched_1 = chapter_data_1.url.match(/(\d+)\.htm/),
// 依照.url排序。
matched_2 = chapter_data_2.url.match(/(\d+)\.htm/);
if (matched_1 && matched_2) {
return matched_1[1] - matched_2[1];
}
return chapter_data_1.url < chapter_data_2.url ? -1 : 1;
// 依照.title排序。
return chapter_data_1.title < chapter_data_2.title ? -1
: 1;
});
},
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
// decode chapter data
function decode(code) {
code = eval(code).replace(/^[^=]+/, 'code');
return eval(code);
}
var chapter_data = html
.match(/[>;\n\s]var\s+cInfo\s*=\s*(\{[\s\S]+?\})[;\n]/);
if (chapter_data) {
// 999comics.js
// console.log(chapter_data[1]);
eval('chapter_data=' + chapter_data[1]);
// https://www.999comics.com/static/scripts/core2.js?v=20180206
// preload: function(t) {...}
// r("<img />")[0].src = "//www.999comics.com/g.php?"+o.cid+'/'+
// o.fs[i - 1]
chapter_data.fs = chapter_data.fs.map(function(i) {
return this.full_URL('g.php?'
//
+ chapter_data.cid + '/' + i);
}, this);
} else if (chapter_data = html.between(
'<script type="text/javascript">eval', '\n')) {
// 57mh.js
chapter_data = decode(chapter_data);
}
if (!chapter_data) {
library_namespace.warn({
// gettext_config:{"id":"unable-to-parse-chapter-data-for-«$1»-§$2"}
T : [ '無法解析《%1》§%2 之章節資料!', work_data.title, chapter_NO ]
});
return;
}
// console.log(chapter_data);
// 設定必要的屬性。
chapter_data.title = get_label(html.between('<h2>', '</h2>'));
chapter_data.image_count = chapter_data.fc;
chapter_data.image_list = chapter_data.fs;
if (!chapter_data.fs.at(-1)) {
// for http://www.5qmh.com/6908/0296.html?p=9
chapter_data.fs.pop();
chapter_data.image_count--;
}
return chapter_data;
}
};
// --------------------------------------------------------------------------------------------
function new_SinMH2013_comics_crawler(configuration) {
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
// 每次呼叫皆創建一個新的實體。
return new library_namespace.work_crawler(configuration);
}
return new_SinMH2013_comics_crawler;
}

View File

@@ -0,0 +1,168 @@
/**
* @name CeL module for downloading YOUNG ACE UP, TYPE-MOON comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載 KADOKAWA CORPORATION webエース ヤングエースUPアップ
* Webコミック、TYPE-MOONコミックエース 漫画 的工具。
*
* <code>
CeL.ace({
// configuration
site : '' || CeL.get_script_name()
}).start(work_id);
</code>
*
* @since 2020/4/26 5:58:57 模組化。
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/comic.ja-JP/youngaceup.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.ace',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
var default_configuration = {
// 日本的網路漫畫網站習慣刪掉舊章節,因此每一次都必須從頭檢查。
// e.g., ヱデンズボゥイ
recheck : true,
// one_by_one : true,
base_URL : 'https://web-ace.jp/',
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
return 'contents/' + work_id + '/';
},
parse_work_data : function(html, get_label, extract_work_data) {
var work_data = JSON.parse(html.between(
'<script type="application/ld+json">', '</script>'));
extract_work_data(work_data, html);
// 放在這裡以預防被extract_work_data()覆蓋。
Object.assign(work_data, {
// 必要屬性:須配合網站平台更改。
title : get_label(html.between('<h1>', '</h1>')),
authors : html.all_between('<p class="author">', '</p>').map(
get_label),
// 選擇性屬性:須配合網站平台更改。
subtitle : get_label(html.between('<p class="subtitle">',
'</p>')),
description : get_label(html.between(
'<div class="description">', '</div>')),
status : html.between('<p class="genre">', '</p>').replace(
'ジャンル:', '').split(' / ').map(get_label),
last_update : get_label(html.between(
'<span class="updated-date">', '</span>'))
|| (new Date).toISOString(),
next_update : html.all_between(
'<span class="label_day-of-the-week">', '</span>').map(
get_label)
// 隔週火曜日更新 次回更新予定日2018年11月27日
.map(function(token) {
return token.replace('次回更新予定日:', '');
})
});
work_data.author = work_data.authors.map(function(name) {
// 原作: 漫画: キャラクター原案:
return name.replace(/^[^]+/, '').trim();
});
// console.log(work_data);
return work_data;
},
chapter_list_URL : function(work_id, work_data) {
return this.work_URL(work_id) + 'episode/';
},
get_chapter_list : function(work_data, html, get_label) {
// <div class="container" id="read">
html = html.between(' id="read">', '</section>')
// <ul class="table-view">
.between('<ul', '</ul>');
work_data.chapter_list = [];
var some_skipped;
html.each_between('<li', '</li>', function(token) {
var matched = token.between('<p class="yudo_wa">', '</div>');
if (matched) {
library_namespace.info(work_data.title + ': '
+ get_label(matched).replace(/\s{2,}/g, ' '));
some_skipped = true;
return;
}
matched = token
.match(/<a [^<>]*?href=["']([^"'<>]+)["'][^<>]*>/);
var chapter_data = {
title : get_label(token
//
.between('<p class="text-bold">', '</p>')),
date : token.between('<span class="updated-date">',
'</span>'),
// 直接取得圖片網址資訊。
url : matched[1] + 'json/'
};
work_data.chapter_list.push(chapter_data);
});
work_data.chapter_list.reverse();
if (some_skipped) {
// 因為中間的章節可能已經被下架,因此依章節標題來定章節編號。
this.set_chapter_NO_via_title(work_data);
}
},
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
// console.log(html);
var chapter_data = work_data.chapter_list[chapter_NO - 1];
Object.assign(chapter_data, {
// 設定必要的屬性。
image_list : JSON.parse(html)
});
return chapter_data;
}
};
// --------------------------------------------------------------------------------------------
function new_ace_comics_crawler(configuration, callback, initializer) {
// library_namespace.set_debug(9);
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
configuration.base_URL += (configuration.site
// || library_namespace.get_script_name()
) + '/';
// 每次呼叫皆創建一個新的實體。
var crawler = new library_namespace.work_crawler(configuration);
return crawler;
}
return new_ace_comics_crawler;
}

View File

@@ -0,0 +1,346 @@
/**
* @name CeL module for downloading baozimh comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載 包子漫畫 的工具。
*
* <code>
CeL.baozimh({
// configuration
site : '' || CeL.get_script_name()
}).start(work_id);
</code>
*
* @since 2022/11/3 5:55:24
* @since 2022/11/3 5:55:24 模組化。
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hant-TW/baozimh.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.baozimh',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
// ----------------------------------------------------------------------------
function module_code(library_namespace) {
// requiring
/**
* <code>
<a href="/user/page_direct?comic_id=yuanlaiwoshixiuxiandalao-luanshijiaren&amp;section_slot=0&amp;chapter_slot=0" rel="noopener" class="comics-chapters__item" data-v-0c0802bc><div style="flex: 1;" data-v-0c0802bc><span data-v-0c0802bc>預告</span></div></a>
<code>
*/
var PATTERN_chapter_link = /<a [^<>]*?href="([^<>"]+?)" [^<>]* class="comics-chapters__item"[^<>]*>([\s\S]+?)<\/a>/g;
// --------------------------------------------------------------------------------------------
var default_configuration = {
base_URL : 'https://www.baozimh.com/',
// 最小容許圖案檔案大小 (bytes)。
// 對於極少出現錯誤的網站,可以設定一個比較小的數值,並且設定.allow_EOI_error=false。因為這類型的網站要不是無法取得檔案要不就是能夠取得完整的檔案要取得破損檔案並且已通過EOI測試的機會比較少。
// 對於有些圖片只有一條細橫桿的情況。
MIN_LENGTH : 50,
// e.g., 都是黑丝惹的祸2 0409 第二季 第409话 因为我喜欢他
// allow .jpg without EOI mark.
// allow_EOI_error : true,
// 當圖像檔案過小,或是被偵測出非圖像(如不具有EOI)時,依舊強制儲存檔案。
skip_error : true,
// e.g., woshenshangyoutiaolong-pikapi 我身上有条龙/
// 0590 第590话 父母过往/woshenshangyoutiaolong-pikapi-590-017.jpg
// one_by_one : true,
acceptable_types : 'jpg|webp',
// 解析 作品名稱 → 作品id get_work()
search_URL : 'search?q=',
parse_search_result : function(html, get_label) {
html = html.between('<div class="pure-g classify-items">');
// console.log(html);
var id_list = [], id_data = [];
html.each_between('<a href="/comic/', '</a>', function(text) {
id_list.push(text.between(null, '"'));
id_data.push(get_label(text.between('title="', '"')));
});
return [ id_list, id_data ];
},
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
return 'comic/' + work_id;
},
parse_work_data : function(html, get_label, extract_work_data) {
var work_data = {
// 必要屬性:須配合網站平台更改。
title : get_label(html.between(
// <h1 class="comics-detail__title"
// data-v-6f225890>原來我是修仙大佬</h1>
'<h1 class="comics-detail__title"', '</h1>').between('>')),
author : get_label(html.between(
// <h2 class="comics-detail__author" data-v-6f225890>亂室佳人</h2>
'<h2 class="comics-detail__author"', '</h2>').between('>')),
// 選擇性屬性:須配合網站平台更改。
tags : html.all_between('<span class="tag"', '</span>')
//
.map(function(tag) {
return get_label(tag.between('>'));
}),
last_update : get_label(html.between('最新:').between('<em',
'</em>').between('>').replace(/\((.+) 更新\)/, '$1')),
latest_chapter : get_label(html.between('最新:', '</a>')),
description : get_label(html.between('<p class="comics-detail',
'</p>').between('>')),
/**
* cover image<code>
<amp-img alt="原來我是修仙大佬" width="180" height="240" layout="responsive" src="https://static-tw.baozimh.com/cover/yuanlaiwoshixiuxiandalao-luanshijiaren.jpg" data-v-6f225890>
<code>
*/
cover_image : html.between('layout="responsive" src="', '"')
};
// 由 meta data 取得作品資訊。
extract_work_data(work_data, html);
// console.log(work_data);
return work_data;
},
get_chapter_list : function(work_data, html, get_label) {
var _this = this;
// reset chapter list
work_data.chapter_list = [];
var part_count = html.all_between('<div class="section-title"',
'</div>').length;
var skip_latest_chapters = true;
html.each_between('<div class="section-title"', null,
//
function(text) {
/**
* <code>
<div class="section-title" data-v-6f225890>章節目錄</div>
<code>
*/
var part_title = text.between('>', '</div>');
// 最新章節 最新章节
if (/^最新章[節节]$/.test(part_title)
// 假如只有一個 part那就必須留下最新章節。 e.g., 妖精种植手册黑白轮回篇
&& (skip_latest_chapters = part_count > 1)) {
return;
}
_this.set_part(work_data, part_title);
// console.log(text);
var matched;
while (matched = PATTERN_chapter_link.exec(text)) {
var chapter_data = {
// reset sub_chapter_list
sub_chapter_list : null,
title : get_label(matched[2]),
// TODO: fix "&amp;"
url : matched[1].replace(/&amp;/g, '&')
};
_this.add_chapter(work_data, chapter_data);
}
});
// console.trace([ part_count, skip_latest_chapters ]);
if (!skip_latest_chapters) {
// 最新章節 為倒序。
// e.g., 妖精种植手册黑白轮回篇
// https://cn.baozimh.com/comic/yaojingchongzhishouceheibailunhuipian-dazui
// 我独自升级
// https://www.baozimh.com/comic/woduzishengji-duburedicestudio_n6ip31
work_data.inverted_order = true;
}
// console.log(work_data.chapter_list);
},
pre_parse_chapter_data
// 執行在解析章節資料 process_chapter_data() 之前的作業 (async)。
// 必須自行保證執行 callback(),不丟出異常、中斷。
: function(XMLHttp, work_data, callback, chapter_NO) {
// console.log(XMLHttp);
// console.log(work_data);
// 模擬歸一化
// https://www.webmota.com/comic/chapter/wangyouzhijinzhanfashi-samanhua/0_188.html
// https://cn.webmota.com/comic/chapter/wangyouzhijinzhanfashi-samanhua/0_188.html
function simulated_normalized_url(url) {
return url.replace(/\/\/[a-z]+\./, '//www.');
}
var chapter_data = work_data.chapter_list[chapter_NO - 1];
// console.trace(chapter_data);
if (!chapter_data.sub_chapter_list) {
// get_chapter_list() 獲得的是動態的 url會轉成靜態的。
chapter_data.sub_chapter_list = [ XMLHttp.responseURL ];
}
if (chapter_data.static_url) {
if (simulated_normalized_url(chapter_data.static_url) !== simulated_normalized_url(chapter_data.sub_chapter_list[0])) {
// console.log(chapter_data);
library_namespace.warn('§' + chapter_NO + '《'
+ chapter_data.title
+ '》: 從上一章的章節內容頁面獲得的 URL 不同於從章節列表獲得的 URL\n '
+ chapter_data.static_url + '\n '
+ chapter_data.sub_chapter_list[0]);
}
// free
delete chapter_data.static_url;
}
var html = XMLHttp.responseText, matched, next_chapter_url = html;
// console.log(html);
/**
* <code>
// 可能有上下兩個 `<div class="next_chapter">`,取後一個。
<div class="chapter-main scroll-mode"><div class="next_chapter"><a href="https://cn.webmota.com/comic/chapter/dubuxiaoyao-zhangyuewenhua/0_35.html#bottom">
点击进入上一页
</a></div>
<div class="next_chapter"><a href="https://www.webmota.com/comic/chapter/shenzhita-siu/0_738_2.html">
點擊進入下一頁
<span class="iconfont icon-xiangxia"></span></a></div>
<div class="next_chapter"><a href="https://cn.webmota.com/comic/chapter/shenzhita-siu/0_738.html">
点击进入下一话
<span class="iconfont icon-xiayibu"></span></a></div>
<code>
*/
while (matched = next_chapter_url.between(' class="next_chapter">'))
next_chapter_url = matched;
next_chapter_url = next_chapter_url
// 去掉網頁錨點。
&& next_chapter_url.between(' href="', '"').replace(/#.*$/, '');
if (!next_chapter_url
//
|| !/^_\d/.test(simulated_normalized_url(next_chapter_url)
// 確定 url 以本章節 url 開頭。
.between(simulated_normalized_url(
//
chapter_data.sub_chapter_list[0]).replace(/\.html$/, '')))) {
// console.trace('下一話');
// assert: next_chapter_url 為下一話的靜態 url。
var next_chapter_data = work_data.chapter_list[chapter_NO];
if (next_chapter_url && next_chapter_data) {
// 做個記錄。
if (false) {
console.trace([ chapter_NO, next_chapter_url,
next_chapter_data ]);
}
next_chapter_data.static_url = next_chapter_url;
// 直接從靜態網頁獲取章節內容,避免採用 CGI。
if (!next_chapter_data.sub_chapter_list)
next_chapter_data.sub_chapter_list = [ next_chapter_url ];
}
// console.trace(chapter_data);
callback();
return;
}
// assert: next_chapter_url 為下一頁的靜態 url。
// console.trace('下一頁');
// 做個記錄。
chapter_data.sub_chapter_list.push(next_chapter_url);
// console.trace(next_chapter_url);
this.get_URL(next_chapter_url, function(XMLHttp, error) {
if (!chapter_data.next_chapter_HTMLs)
chapter_data.next_chapter_HTMLs = [];
chapter_data.next_chapter_HTMLs.push(XMLHttp.responseText);
this.pre_parse_chapter_data(XMLHttp, work_data, callback,
chapter_NO);
});
},
// 取得每一個章節的各個影像內容資料。 get_chapter_data()
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
var chapter_data = work_data.chapter_list[chapter_NO - 1];
var image_list = chapter_data.image_list = [], url_Set = new Set;
function handle_html(html) {
html = html
// <div class="chapter-main scroll-mode">
.between('<div class="chapter-main scroll-mode">');
// console.trace(html);
/**
* <code>
<img src="https://s1.baozimh.com/scomic/yuanlaiwoshixiuxiandalao-luanshijiaren/0/0-vmac/1.jpg" alt="原來我是修仙大佬 - 預告 - 1" width="1200" height="3484" data-v-25d25a4e>
<code>
*/
html.each_between('<img src="', '>', function(text) {
var url = encodeURI(text.between(null, '"'));
if (url_Set.has(url)) {
// 前面的部分會重複3張圖片。
return;
}
url_Set.add(url);
image_list.push({
title : get_label(text.between('alt="', '"')),
url : url
});
});
}
handle_html(html);
if (chapter_data.next_chapter_HTMLs) {
chapter_data.next_chapter_HTMLs.forEach(handle_html);
// free
delete chapter_data.next_chapter_HTMLs;
}
// console.log(image_list);
return chapter_data;
},
is_limited_image_url : function(image_url, image_data) {
// e.g.,
// https://cn.webmota.com/comic/chapter/zunshang-mankewenhua_d/0_220.html
if (typeof image_url === 'string'
// https://static-tw.baozimh.com/cover/404.jpg
&& image_url.endsWith('/cover/404.jpg')) {
return '404 Not Found';
}
}
};
// --------------------------------------------------------------------------------------------
function new_baozimh_comics_crawler(configuration, callback, initializer) {
// library_namespace.set_debug(9);
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
// 每次呼叫皆創建一個新的實體。
var crawler = new library_namespace.work_crawler(configuration);
return crawler;
}
return new_baozimh_comics_crawler;
}

View File

@@ -0,0 +1,744 @@
/**
* @name CeL module for downloading comico comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載 韓國 NHN comico Corp. 漫畫 的工具。
*
* 2021/12/22 改版
*
* <code>
CeL.comico(configuration, function(crawler) {
start_crawler(crawler, typeof module === 'object' && module);
}, function(crawler) {
setup_crawler(crawler, typeof module === 'object' && module);
});
</code>
*
* @see http://comico.kr/
*
* @since 2018/8/19 5:49:8 模組化。
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/comico.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.comico',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
function add_navigation_data(data, html) {
var navigation_data;
try {
navigation_data = JSON.parse(html.between(
// コミコ 日文版有時 json 結構有問題。
// e.g., http://www.comico.jp/articleList.nhn?titleNo=3410
'<script type="application/ld+json">', '</script>')
// e.g., http://www.comico.com.tw/2870/17/
.replace(/\t+"/g, '"'));
} catch (e) {
// TODO: handle exception
}
return Object.assign(data, navigation_data);
}
var PATTERN_work_info = /<(p|div) class="[^<>"]+?__(author|(?:sub-)?description|meta)">([\s\S]+?)<\/\1>/g,
// assert: (NO_ticket_notified>=0) === false
// gettext_config:{"id":"no-read-volumes-are-available"}
NO_ticket_notified = '已無閱讀卷可用。', auto_use_ticket_notified,
// 可以用閱讀卷閱讀的章節。
READABLE_FLAG = 'W',
//
default_configuration = {
// 所有的子檔案要修訂註解說明時應該都要順便更改在CeL.application.net.comic中Comic_site.prototype內的母comments並以其為主體。
// e.g., 20099 俺のメンタルは限界です\0003 3話 「マンガを描く原点」\20099-3-022.jpg
MIN_LENGTH : 180,
// one_by_one : true,
base_URL : '',
// have already read the chapter
set_downloaded_if_read : true,
convert_id : {
// switch
// 警告: 需要自行呼叫 insert_id_list(id_list);
adult : function(insert_id_list, get_label) {
// TW only: 此前被當作是一般作品。
library_namespace.info([ this.id + ': ', {
// gettext_config:{"id":"subsequent-titles-of-the-work-are-considered-to-be-web-limited-works"}
T : '此後的作品標題都被當作是網頁限定作品。'
} ]);
// webonly
this.adult = true;
insert_id_list();
}
},
// 解析 作品名稱 → 作品id get_work()
search_URL : function(work_title) {
var url = (this.adult ? 'webonly/' : '')
// ↑ webonly, オトナ限定: TW only
+ 'search/index.nhn?searchWord='
+ encodeURIComponent(work_title.replace(/\s+\d+$/, ''));
if (this.base_URL.includes('\/\/plus.comico')) {
url = this.base_URL
.replace('\/\/plus.comico', '\/\/www.comico')
+ url;
}
return url;
},
// 每個項目的<li>開頭。
search_head_token : '<li class="list-article02__item">',
// title 不能用 [^<>"]+ : for case of "薔薇的嘆息 <薔薇色的疑雲 I>"
PATTERN_search : /<a href="[^<>"]*?\/(\d+)\/"[^<>]*? title="([^"]+)"/,
parse_search_result : function(html, get_label) {
// console.log(html);
html = html.between(' id="officialList">') || html;
// console.log(html);
var _this = this, id_list = [], id_data = [];
html.each_between(this.search_head_token, '</li>', function(token) {
// console.log(token);
var matched = token.match(_this.PATTERN_search);
// console.log(matched);
if (matched) {
// コミコ有些整本賣的作品,而非一話一話。
id_list.push(matched[1]);
id_data.push(get_label(matched[2]));
}
});
return [ id_list, id_data ];
},
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
return work_id + '/';
},
parse_work_data : function(html, get_label, extract_work_data) {
// console.log(html);
if (this.archive_old_works) {
// 因為不會遍歷所有章節檔案,得到的是錯誤的 `work_data.last_file_modified_date`。
// 因此必須避免執行 check_and_archive_old_work()。
library_namespace.warn([ this.id + ': ', {
// gettext_config:{"id":"this-website-does-not-support-the-function-of-archiving-old-works-(.archive_old_works)"}
T : '本網站不支援封存舊作品功能 (.archive_old_works)'
} ]);
this.archive_old_works = false;
}
var cmnData = html.between('var cmnData =', '</script>'), matched;
if (!cmnData) {
// 公式作品の「掲載終了日」について、お知らせいたします。
// 出版社の都合により、以下2作品を掲載終了とさせていただきます。
// 更新中の掲載終了につき、大変ご迷惑をおかけし申し訳ございません。
// 下記の公式作品は既に掲載終了しています。
matched = get_label(html.between(
// <p class="m-section-error__heading">お探しのページは存在しません</p>
'<p class="m-section-error__heading">', '</p>'));
if (matched) {
throw new Error(matched);
}
}
eval('cmnData=' + cmnData);
var title = (html.between('<h1 class="article', '</h1>') || html
.between('<h1', '</h1>')).between('>'), tags = [];
/**
* e.g., http://www.comico.com.tw/challenge/3263/ 去除label
*
* <code>
<h1 class="article-hero03__ttl"><i class="i-label i-label--fill article-hero03__ttl-icon">精選</i><span class="o-hidden _challengeTitle">小惡魔與草莓男友</span></h1>
</code>
*/
if (title.includes('</i>')) {
tags.push(get_label(title.between(null, '</i>')));
title = title.between('</i>');
}
var work_data = {
// 必要屬性:須配合網站平台更改。
// <h1 class="article-hero05__ttl">美麗的代價</h1>
title : get_label(title),
// 選擇性屬性:須配合網站平台更改。
tags : tags
};
extract_work_data(work_data, html);
// e.g., '<li class="article-hero03__list-tag-item">中篇故事</li>'
html.each_between('list-tag-item">', '</li>', function(text) {
tags.push(get_label(text));
});
// JavaScript Object Notation for Linked Data 關聯的資料
matched = html.between('<script type="application/ld+json">',
'</script>');
if (matched) {
// Structured Data 結構化資料
// https://search.google.com/structured-data/testing-tool
try {
matched = JSON.parse(matched);
work_data.linked_data = matched;
} catch (e) {
// library_namespace.error(matched);
}
}
while (matched = PATTERN_work_info.exec(html)) {
if (matched[3] = get_label(matched[3]).replace(/\t/g, ''))
work_data[matched[2]] = matched[3];
}
Object.assign(add_navigation_data(work_data, html),
// 警告: 這會留下個人資訊!
cmnData, {
// 更新日期:每週連載時間/是否為完結作品。 e.g., 完結作品, 每週六, 隔週週日
status : get_label(html.between('__info-value">', '</dd>')
// コミコ e.g., 完結作品, 毎週金曜日
|| html.between('<span class="o-txt-bold">', '</span>'))
.replace(/[\s\n]{2,}/g, ' '),
// 可用的閱讀券數量。
ticket_left : (cmnData.eventRentalTicket || 0)
// 若是不用等的話,表示已收到閱讀券,還有一張可用。
+ (cmnData.time && cmnData.time.leftTime === 0 ? 1 : 0),
last_checked : null
});
if (cmnData.time && cmnData.time.leftTime > 0) {
library_namespace.info({
// gettext_config:{"id":"the-next-time-you-receive-a-reading-voucher-you-will-need-$1"}
T : [ '下次收到閱讀券還要 %1。',
// レンタル券で無料 レンタル券が届きました1日で回復
// 作品を1話レンタルできます
library_namespace.age_of(0, 1000 * cmnData.time.leftTime) ]
});
}
// console.log(work_data);
return work_data;
},
chapter_list_URL : function(work_id, work_data) {
// console.log(work_data);
// library_namespace.set_debug(9);
var api = work_data.api && work_data.api.articleListAPI;
if (!api) {
api = work_data.isOfficial === false
// 2019/10: 'api/getArticleListAll.nhn' 沒有 .freeFlg
// 標記,無法自動使用閱讀卷。
// 但是對新手村作品如 '3729 神光拜達摩' 來說,
// 用 'api/getArticleListAll.nhn' 才能取得所有作品之列表。
//
// https://github.com/kanasimi/work_crawler/issues/384
// 第一次執行時,尚未取得 .isOfficial 標記,必須先採用 api/getArticleList.nhn
? 'api/getArticleListAll.nhn'
// 2019/9: 'api/getArticleList.nhn'
: 'api/getArticleList.nhn'
}
return [ api, {
titleNo : work_id
} ];
},
get_chapter_list : function(work_data, html, get_label) {
if (!Array.isArray(work_data.downloaded_chapter_list))
work_data.downloaded_chapter_list = [];
// console.log(html);
var recerse_count = 0;
html = JSON.parse(html);
html = html.result;
// for 'api/getArticleList.nhn', there is no .totalPageCnt
if (('totalPageCnt' in html) && html.totalPageCnt !== 1) {
console.log(html);
throw new Error(work_data.title + ': ' + 'Total page is '
+ html.totalPageCnt + ', not 1!');
}
// 作品改變 titleNo 時,舊 id 可能會回傳 `{"result":{}}`
html.list.forEach(function(chapter_data, index) {
chapter_data.url = chapter_data.articleDetailUrl;
// 原先都將標題設在 subtitletitle 沒東西。
chapter_data.title = get_label(chapter_data.subtitle);
if (this.set_downloaded_if_read
&& !work_data.downloaded_chapter_list[index]) {
work_data.downloaded_chapter_list[index]
// 記錄是否已經下載過本章節。
= chapter_data.read;
}
if (index > 0) {
recerse_count += Math.sign(html.list[index - 1].articleNo
- chapter_data.articleNo);
}
}, this);
work_data.chapter_list = html.list;
// 預防尾大不掉。
delete html.list;
// console.log(recerse_count);
if (recerse_count > 0) {
library_namespace.info([ work_data.title + ': ', {
// gettext_config:{"id":"change-the-list-of-reversed-chapters-to-positive-order"}
T : '將倒序章節列表轉為正序。'
} ]);
work_data.chapter_list.reverse();
}
Object.assign(work_data, html);
// 先檢查是不是還有還有沒讀過的章節。
if (work_data.ticket_left > 0) {
if (work_data.last_download) {
work_data.chapter_list.some(function(chapter_data, index) {
if (++index >= work_data.last_download.chapter) {
return true;
}
if (!chapter_data.freeFlg) {
throw new Error(this.id + ': '
+ '網站改版?未發現 .freeFlg');
}
if (!work_data.downloaded_chapter_list[index]
&& chapter_data.freeFlg === READABLE_FLAG) {
library_namespace.info([ work_data.title + ': ', {
// gettext_config:{"id":"there-are-still-$1-reading-volume-but-$2-$3-chapter-has-not-been-downloaded-yet.-so-checking-from-this-chapter"}
T : [ '還有%1張{{PLURAL:%1|閱讀卷}},且第 %2/%3 章還有沒下載過,從此章開始檢查。',
//
work_data.ticket_left, index,
//
work_data.chapter_list.length ]
} ]);
work_data.last_checked
// 記錄最後檢查過的章節。
= work_data.last_download.chapter;
work_data.last_download.chapter = index;
return true;
}
});
} else {
work_data.recheck = true;
}
}
},
consume_url : 'consume/index.nhn',
pre_parse_chapter_data
// 執行在解析章節資料 process_chapter_data() 之前的作業 (async)。
// 必須自行保證執行 callback(),不丟出異常、中斷。
: function(XMLHttp, work_data, callback, chapter_NO) {
// console.log(work_data);
var chapter_data = work_data.chapter_list[chapter_NO - 1],
//
skip_chapter = !chapter_data.price || chapter_data.isPurchased && {
// gettext_config:{"id":"«$1»-has-been-purchased"}
T : [ '已購買章節《%1》。', chapter_data.title ]
};
// console.log(chapter_data);
if (!skip_chapter && chapter_data.expireDate > 0) {
skip_chapter = {
// gettext_config:{"id":"you-can-read-«$3»-in-this-section-before-$1-(and-$2)"}
T : [ '在 %1 之前(還有 %2可以閱讀本章節《%3》。',
//
new Date(chapter_data.expireDate).format('%m/%d %H:%M'),
//
library_namespace.age_of(Date.now(),
//
chapter_data.expireDate), chapter_data.title ]
};
}
if (!skip_chapter
&& work_data.downloaded_chapter_list[chapter_NO - 1]) {
// TODO: 應該檢查是不是真的有圖片檔案存在。若有檔案不見,
// 或者有損壞檔案,
// 那麼就把 work_data.downloaded_chapter_list[index] 設成 false。
skip_chapter = {
// gettext_config:{"id":"the-section-«$1»-has-been-downloaded-before-and-will-not-be-re-purchased"}
T : [ '之前已下載過章節《%1》不再重新購買。', chapter_data.title ]
};
}
if (!skip_chapter && chapter_data.freeFlg !== READABLE_FLAG) {
// N: TW: 本章節需要錢(coin)來閱讀。
if (chapter_data.freeFlg === 'N'
// P: JP: 本章節需要錢(30コイン) or point(15ポイント)來閱讀。
|| chapter_data.freeFlg === 'P') {
skip_chapter = true;
} else {
skip_chapter = {
// gettext_config:{"id":"the-status-of-this-chapter-is-unknown-($1).-skipping-$1-does-not-use-reading-volumes"}
T : [ '本章節狀況不明(%1)。跳過《%1》不採用閱讀卷。',
chapter_data.freeFlg, chapter_data.title ]
};
}
}
if (!skip_chapter && !(work_data.ticket_left > 0)) {
if (work_data.ticket_left !== NO_ticket_notified) {
work_data.ticket_left = NO_ticket_notified;
skip_chapter = [ {
T : NO_ticket_notified
}, {
T : [ '跳過《%1》不使用閱讀券。', chapter_data.title ]
} ];
} else
skip_chapter = true;
}
if (!skip_chapter && !this.auto_use_ticket) {
skip_chapter = auto_use_ticket_notified ? true
// @see https://github.com/kanasimi/work_crawler
: {
// gettext_config:{"id":"the-tool-is-not-set-to-automatically-use-the-reading-volume.-if-you-are-not-using-the-installation-package-and-want-to-have-the-tool-automatically-use-the-reading-volume-please-open-the-file-manager"}
T : '未設定讓本工具自動使用閱讀卷。若您並非使用安裝包並想要讓本工具自動使用閱讀卷請打開檔案總管到安裝本工具的目錄下若是您使用安裝包就不能夠設定帳號密碼了。在「work_crawler.configuration.js」這個 .js 組態檔案中設定好帳號密碼資料並設定「auto_use_ticket:true」。您可以參考 work_crawler.default_configuration.js 這個檔案來做設定。'
};
auto_use_ticket_notified = true;
}
if (skip_chapter) {
if (skip_chapter !== true) {
if (!Array.isArray(skip_chapter)) {
skip_chapter = [ skip_chapter ];
}
skip_chapter.unshift(work_data.title, ': ');
library_namespace.info(skip_chapter);
}
callback();
return;
}
// http://www.comico.com.tw/notice/detail.nhn?no=751
library_namespace.info([ work_data.title + ': ', {
// gettext_config:{"id":"reading-«$1»-with-a-reading-voucher"}
T : [ '用閱讀券閱讀《%1》。', chapter_data.title ]
} ]);
var _this = this, html = XMLHttp.responseText;
this.get_URL(this.consume_url, function(XMLHttp) {
// console.log(XMLHttp.responseText);
var matched = XMLHttp.responseText && XMLHttp.responseText
// var msg = '很抱歉,帳號需要通過電話認證才可購買。';
.match(/[\s;]msg *= *'([^']+)/);
if (matched) {
library_namespace
.error(work_data.title + ': ' + matched[1]);
// 歸零。
work_data.ticket_left = 1;
}
if (--work_data.ticket_left === 0
// 僅僅下載有閱讀券的章節,然後就回到最後讀取的章節。
&& work_data.last_checked > 0) {
// 回到原先應該檢查的章節號碼。
work_data.jump_to_chapter = work_data.last_checked;
delete work_data.last_checked;
}
// XMLHttp 只是一個轉址網頁,必須重新擷取網頁。
_this.get_URL(chapter_data.url, callback);
}, {
titleNo : work_data.id,
articleNo : chapter_data.articleNo,
// K: 專用閱讀券, P: 用point, C: 用coin購買
paymentCode : 'K',
// 使用coin時才需要
// https://github.com/zhongfly/comico/blob/master/comico.py
// ['http://www.comico.com.tw/consume/coin/publish.nhn',{paymentCode:'C'}]
// JSON.parse(result).result.coinUseToken
coinUseToken : '',
// 5, 6, コミコ:36, ...?
productNo : html
//
.between(' name="productNo" value="', '"') || 5,
// coin price
price : chapter_data.price,
// 用coin租用價格一般能租用的都可以用閱讀券。 コミコ: 20
rentalPrice : html.between(' name="rentalPrice" value="', '"')
|| '',
// point price, コミコ 漫畫作品無此項, TW only
pointRentalPrice : html.between(
' name="pointRentalPrice" value="', '"') || 120
});
},
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
// http://static.comico.com.tw/tw/syn/spn/js/manga/article/plusMangaDetailApp/app.1.12.0.js
var chapter_data = work_data.chapter_list[chapter_NO - 1],
//
cmnData = html.between('var cmnData =', '</script>');
// console.log(cmnData);
if (cmnData) {
eval('cmnData=' + cmnData);
} else if (cmnData = html.between(
// e.g., http://plus.comico.jp/manga/24517/8/
'<p class="m-section-error__heading">', '</p>')) {
var message = work_data.title + ' §' + chapter_NO + ' '
+ chapter_data.title + ': ' + cmnData;
if (cmnData !== 'お探しのページは存在しません') {
throw new Error(message);
}
library_namespace.error(message);
return chapter_data;
}
var matched, image_url_list = html.between(
// TW: <div class="locked-episode__kv _lockedEpisodeKv"
// コミコ: <div class="locked-episode locked-episode--show-kv">
'<div class="locked-episode', '</div>');
if (image_url_list) {
chapter_data.limited = true;
// TW: style="background-image: url('...');">
image_url_list = image_url_list.between("url('", "'")
// コミコ: <img src=".jpg" width="88" height="88" alt="" />
|| image_url_list.between(' src="', '"');
// 日文版plus.comico 此時雖然有 {Array}cmnData.imageData
// 但缺少hash而不能取得。
if (/\.jpg$/.test(image_url_list)) {
// 日文版plus.comico 此時僅有一個縮圖可用,跳過不取。
cmnData.imageData = [];
} else if (cmnData.imageData
&& cmnData.imageData.filter(function(url) {
return url && !/\.jpg$/.test(url);
}).length > 0) {
// 應該不會到這裡來了。
cmnData.imageData.unshift(image_url_list);
} else {
// 中文版的狀況。
cmnData.imageData = [ image_url_list ];
}
} else if (image_url_list = html
// comico_jp: <div class="comic-image _comicImage">
// e.g., 新手村
.between(' _comicImage">', '</div>')) {
// 一般正常可取得圖片的情況。
// 去除 placeholder。 <div class="comic-image__blank-layer">
image_url_list = image_url_list.between(null, '<div ')
|| image_url_list;
image_url_list = image_url_list.all_between(' src="', '"');
// assert: {Array}image_url_list
if (cmnData.imageData) {
// 中文版, 日文版plus.comico 將除第一張外所有圖片網址放在
// {Array}cmnData.imageData 裡面。
if (image_url_list.length === 1) {
cmnData.imageData.unshift(image_url_list[0]);
} else if (
/**
* e.g.,
* http://www.comico.jp/detail.nhn?titleNo=27605&articleNo=1
* <code>
<div class="swiper-wrapper _swiperWrapper _comicImage">
<!-- Slides -->
<div class="swiper-slide _swiperSlide o-hidden">
<div dir="ltr">
<div id="_popIn_video"></div>
</code>
*/
!html.includes(' class="swiper-slide _swiperSlide')) {
// gettext_config:{"id":"web-page-revision?-unable-to-parse"}
var message = '網頁改版?無法解析!';
if (library_namespace.gettext)
message = library_namespace.gettext(message);
throw new Error('parse_chapter_data: '
+ work_data.title + ' §' + chapter_NO + ': '
+ message);
Array.prototype.unshift.apply(cmnData.imageData,
image_url_list);
}
} else {
// コミコ 日文版一般漫畫將所有圖片放在這之間,無 cmnData.imageData。
cmnData.imageData = image_url_list;
}
} else if (matched = html
// e.g., https://www.comico.jp/detail.nhn?titleNo=4235&articleNo=219
.match(/<h2 class="[^"<>]*o-txt-error[^"<>]*">([\s\S]+?)<\/h2>/)) {
var message = get_label(matched[1]);
throw new Error(work_data.title + ' §' + chapter_NO + ': '
+ message);
} else if (html.includes('<p class="error-section__ttl">')) {
var message = get_label(html.between(
/**
* <code>
<div class="error-section o-mt100 o-mb100">
<p class="error-section__ttl">無法閱讀</p>
<p class="error-section__description">本話正在審查當中。<br/>審查完成後將會開放閱讀</p>
<p class="o-mt30"><a href="" class="btn03" onclick="javascript:history.back();">離開</a></p>
<!-- /.m-section-error --></div>
</code>
*/
'<p class="error-section__ttl">', '</p>'))
//
+ ': ' + get_label(html.between(
//
'<p class="error-section__description">', '</p>'));
throw new Error(work_data.title + ' §' + chapter_NO + ': '
+ message);
} else {
console.log(html);
// gettext_config:{"id":"web-page-revision?-unable-to-parse"}
var message = '網頁改版?無法解析!';
if (library_namespace.gettext)
message = library_namespace.gettext(message);
throw new Error(work_data.title + ' §' + chapter_NO + ': '
+ message);
}
if (cmnData.url) {
// e.g., http://plus.comico.jp/manga/24529/13/
// 預防 chapter_data.url 被污染。
cmnData._url = cmnData.url;
delete cmnData.url;
}
// console.log(work_data);
// console.log(chapter_data);
// console.log(cmnData);
Object.assign(add_navigation_data(chapter_data, html), {
// 設定必要的屬性。
image_list : cmnData.imageData.map(function(url) {
if (chapter_data.limited
// http://comicimg.comico.jp/tmb/00000/1/hexhex_hexhexhex.jpg"
&& url.includes('.jp/tmb/') && /\.jpg$/.test(url))
return;
// chapter_data.isOfficial ? 官方作品 : 新手村
if (chapter_data.isOfficial && chapter_data.freeFlg !== 'Y'
// 付費章節: 中文版提供第一張的完整版,日文版只提供縮圖。
// 圖片都應該要有hash且不該是縮圖。
&& (url.includes('.jp/tmb/') || /\.jpg$/.test(url))) {
var message = library_namespace.gettext
// gettext_config:{"id":"invalid-image-url-$1"}
? library_namespace.gettext('Invalid image url: %1',
url) : 'Invalid image url: ' + url;
throw new Error(work_data.title + ' §' + chapter_NO
+ ' ' + chapter_data.title + ': ' + message);
}
return {
url : url
};
})
}, cmnData);
return chapter_data;
},
// @see work_crawler_loader.js
after_download_list : function() {
// logout
}
};
// --------------------------------------------------------------------------------------------
/**
* full module name.
*
* @type {String}
*/
var module_name = this.id;
function new_comico_comics_crawler(configuration, callback, initializer) {
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
// 每次呼叫皆創建一個新的實體。
var crawler = new library_namespace.work_crawler(configuration);
if (typeof initializer === 'function') {
initializer(crawler);
}
// for 年齡確認您是否已滿18歲
crawler.setup_value('cookie', 'islt18age=' + Date.now());
// https://github.com/nodejs/node/issues/27384
// node.js v12 disable TLS v1.0 and v1.1 by default
require('tls').DEFAULT_MIN_VERSION = 'TLSv1';
if (crawler.password && crawler.loginid) {
library_namespace.log([ (crawler.id || module_name) + ': ', {
// gettext_config:{"id":"login-as-$1"}
T : [ 'Login as [%1]', crawler.loginid ]
} ]);
var account_api_host = crawler.base_URL.replace(/^.+?[a-z]+\./,
// https://id.comico.com.tw/login/login.nhn
// https://id.comico.jp/login/login.nhn
'https://id.');
crawler.get_URL(account_api_host + 'login/login.nhn', after_login,
//
{
autoLoginChk : 'Y',
loginid : crawler.loginid,
password : crawler.password,
nexturl : ''
});
} else {
callback(crawler);
}
function after_login(XMLHttp) {
// XMLHttp 只是一個轉址網頁。
// 必須先進入收件箱才能取得所有"有期限的物品"
crawler.get_URL(
// https://id.comico.com.tw/settings/inbox/
account_api_host + 'settings/inbox/', function to_inbox(XMLHttp) {
// 收件箱: 全部接收 有期限的物品
// 受け取りBOX: すべて受け取る
crawler.get_URL(
// https://id.comico.com.tw/api/incentiveall/index.nhn
account_api_host + 'api/incentiveall/index.nhn', get_ticket);
});
}
function get_ticket(XMLHttp) {
// e.g., XMLHttp.responseText ===
// '{"result":[121703625,121703626,121703627,121703628]}'
// console.log(XMLHttp.responseText);
var item_list = JSON.parse(XMLHttp.responseText).result;
if (item_list.length > 0) {
// TODO: 顯示物品的資訊。
library_namespace.info({
// gettext_config:{"id":"$1-items-with-a-time-limit-have-been-received"}
T : [ '已收到 %1 個有期限的{{PLURAL:%1|項目}}。', item_list.length ]
});
}
// 最新消息
// http://www.comico.com.tw/notice/
callback(crawler);
}
}
return new_comico_comics_crawler;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,317 @@
/**
* @name CeL module for downloading hhcool comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載 汗汗酷漫 漫畫 的工具。
*
* <code>
CeL.hhcool(configuration, function(crawler) {
start_crawler(crawler, typeof module === 'object' && module);
}, function(crawler) {
setup_crawler(crawler, typeof module === 'object' && module);
});
</code>
*
* using zh-cmn-Hant-CN .aspx
*
* TODO: http://coco.hhxxee.com/ http://99.hhxxee.com/ http://99770.hhxxee.com/
*
* @since 2019/4/25 5:6:7 模組化。
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/hhcool.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.hhcool',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
/**
* e.g., <code>
// 汗汗酷漫
<li><a title='野生的最终BOSS出现了' href='/manhua/32449.html'><img src='http://img.94201314.net/comicui/32449.JPG'><br>野生的最终BOSS出现了</a></li>
// 動漫伊甸園
<li><a title='樹鶯吟' target=_blank href='/comicinfo/37621.html'><img src='http://img.94201314.net/comicui/37621.JPG'><br>樹鶯吟</a></li>
</code>
*/
var PATTERN_search = /<li><a title='([^<>'"]+)'[^<>]*? href='\/[^\/]+\/(\d+).html'>.+?<\/li>/g;
/**
* e.g., <code>
<div class='cVolTag'>周刊杂志每周每月连载单集</div><ul class='cVolUl'><li>...</a></li></ul>
<div class='cVolTag'>漫画正片外的剧情之番外篇</div><ul class='cVolUl'><li>...</a></li></ul>
<li><a class='l_s' href='/cool282192/1.html?s=7' target='_blank' title='双星之阴阳师09卷'>双星之阴阳师09卷</a></li>
</code>
*/
// matched: [all, part_title, url, title, inner]
var PATTERN_chapter = /<div class='cVolTag'>([^<>]+)|<li><a [^<>]*?href='([^'<>]+)'[^<>]*? title='([^'<>]+)'[^<>]*>(.+?)<\/a>/g;
var PATTERN_image = /<img (?:.*?) name="([^<>"]+)" (?:.*?)hdNextImg" value="([^<>"]+)"/;
var default_configuration = {
// 所有的子檔案要修訂註解說明時應該都要順便更改在CeL.application.net.comic中Comic_site.prototype內的母comments並以其為主體。
// 本站常常無法取得圖片,因此得多重新檢查。
// 當有多個分部的時候才重新檢查。
recheck : 'multi_parts_changed',
// 當無法取得chapter資料時直接嘗試下一章節。在手動+監視下recheck時可併用此項。
// skip_chapter_data_error : true,
// 當圖像不存在 EOI (end of image) 標記,或是被偵測出非圖像時,依舊強制儲存檔案。
// allow image without EOI (end of image) mark. default:false
allow_EOI_error : true,
// 當圖像檔案過小,或是被偵測出非圖像(如不具有EOI)時,依舊強制儲存檔案。
// skip_error : true,
// 最小容許圖案檔案大小 (bytes)。
// 對於極少出現錯誤的網站,可以設定一個比較小的數值,並且設定.allow_EOI_error=false。因為這類型的網站要不是無法取得檔案要不就是能夠取得完整的檔案要取得破損檔案並且已通過EOI測試的機會比較少。
// 對於有些圖片只有一條細橫桿的情況。
MIN_LENGTH : 400,
// one_by_one : true,
// base_URL : '',
// /manhua/
base_comic_path : 'manhua',
// 解析 作品名稱 → 作品id get_work()
search_URL : 'comic/?act=search&st=',
parse_search_result : function(html) {
html = html.between('<div class="cComicList">', '</div>');
var id_list = [], id_data = [], matched;
while (matched = PATTERN_search.exec(html)) {
id_list.push(+matched[2]);
id_data.push(matched[1]);
}
return [ id_list, id_data ];
},
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
// e.g., http://www.hhcool.com/manhua/32449.html
return this.base_comic_path + '/' + work_id + '.html';
},
parse_work_data : function(html, get_label, extract_work_data) {
html = html.between('<div id="about_kit">',
'<div class="cVolList">');
html = html.between(null, '<div class="cInfoAct">') || html;
var work_data = {
// 必要屬性:須配合網站平台更改。
title : get_label(html.between('<h1>', '</h1>'))
// 選擇性屬性:須配合網站平台更改。
// <meta property="og:novel:status" content="已完结"/>
};
extract_work_data(work_data, html, /<li>([^:]+)(.+?)<\/li>/g);
work_data.status = work_data.状态;
work_data.last_update = work_data.更新;
return work_data;
},
get_chapter_list : function(work_data, html, get_label) {
html = html.between('<div class="cVolList">', '<div id="foot">');
work_data.chapter_list = [];
// 漫畫目錄名稱不須包含分部號碼。使章節目錄名稱不包含 part_NO。
// 將會在 function get_chapter_directory_name() 自動設定。
// work_data.chapter_list.add_part_NO = false;
// 轉成由舊至新之順序。
work_data.inverted_order = true;
var matched;
while (matched = PATTERN_chapter.exec(html)) {
// delete matched.input;
// console.log(matched);
if (matched[1]) {
this.set_part(work_data, get_label(matched[1]));
continue;
}
this.add_chapter(work_data, {
title : get_label(matched[3].replace(work_data.title, '')),
url : matched[2]
});
}
// console.log(work_data.chapter_list);
},
pre_parse_chapter_data
// 執行在解析章節資料 process_chapter_data() 之前的作業 (async)。
// 必須自行保證執行 callback(),不丟出異常、中斷。
: function(XMLHttp, work_data, callback, chapter_NO) {
var html = XMLHttp.responseText;
var chapter_list = [], URL = XMLHttp.responseURL,
// 每一張圖片都得要從載入的頁面獲得資訊。
matched, PATTERN = /csel2\((\d{1,3})\)/g;
while (matched = PATTERN.exec(html)) {
chapter_list.push(matched[1]);
}
work_data.cache_directory = work_data.directory
+ this.cache_directory_name;
library_namespace.create_directory(work_data.cache_directory);
if (!work_data.image_list) {
// image_list[chapter_NO] = [url, url, ...]
work_data.image_list = [];
}
var _this = this,
//
this_image_list = work_data.image_list[chapter_NO] = [];
function for_each_chapter(run_next, NO, index) {
var url = URL.replace(/\/\d{1,3}\.html/, '/' + NO + '.html'),
//
save_to = work_data.cache_directory
+ chapter_NO.pad(work_data.chapter_NO_pad_digits || 3)
+ '-' + NO.pad(work_data.chapter_NO_pad_digits || 3)
+ '.html';
function for_each_image_page(html, error) {
if (error) {
library_namespace.error({
// gettext_config:{"id":"an-error-occurred-while-downloading-and-the-file-contents-could-not-be-obtained-smoothly"}
T : '下載時發生錯誤,無法順利取得檔案內容!'
});
library_namespace.error(error);
_this.onerror(error);
return;
}
var image_data = html.match(PATTERN_image);
// decode chapter image url data
image_data = [ unsuan(image_data[1]), unsuan(image_data[2]) ];
if (image_data[0] !== '\x00') {
if (!this_image_list[index]) {
this_image_list[index] = image_data[0];
} else if (this_image_list[index] !== image_data[0]) {
_this.onerror([ {
// gettext_config:{"id":"different-url-$1-≠-$2"}
T : [ 'Different url: %1 ≠ %2',
//
this_image_list[index], image_data[0] ]
}, '\n', {
// gettext_config:{"id":"maybe-the-downloaded-file-has-an-error?-you-can-try-to-download-it-later-or-use-the-.recheck-option-to-ignore-the-cache-and-re-download-the-page-for-each-image"}
T : '或許是下載的檔案出現錯誤?您可嘗試過段時間再下載,或選用 .recheck 選項來忽略快取、重新下載每個圖片的頁面。'
} ]);
run_next();
return;
}
}
if (image_data[1] !== '\x00') {
this_image_list[index + 1] = image_data[1];
}
// console.log([ index, image_data ])
run_next();
}
// 沒 cache 的話,每一次都要重新取得每個圖片的頁面,速度比較慢。
library_namespace.get_URL_cache(url, for_each_image_page, {
get_URL_options : _this.get_URL_options,
no_write_info : true,
file_name : save_to,
reget : _this.recheck
});
}
chapter_list.run_serial(for_each_chapter, callback);
},
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
var PATTERN = / id="hdDomain"(?:.*?) value="([^<>"]+)"/,
// 不同作品放在不同的location。
matched = html.match(PATTERN);
this.server_list = matched[1].split('|');
var chapter_data = work_data.chapter_list[chapter_NO - 1];
// console.log(work_data.image_list[chapter_NO]);
chapter_data.image_list = work_data.image_list[chapter_NO]
.map(function(url) {
return encodeURI(library_namespace.HTML_to_Unicode(url));
});
return chapter_data;
}
};
function unsuan(s) {
var x = s.substring(s.length - 1);
var w = "abcdefghijklmnopqrstuvwxyz";
var xi = w.indexOf(x) + 1;
var sk = s.substring(s.length - xi - 12, s.length - xi - 1);
s = s.substring(0, s.length - xi - 12);
var k = sk.substring(0, sk.length - 1);
var f = sk.substring(sk.length - 1);
for (var i = 0; i < k.length; i++) {
eval("s=s.replace(/" + k.substring(i, i + 1) + "/g,'" + i + "')");
}
var ss = s.split(f);
s = "";
for (i = 0; i < ss.length; i++) {
s += String.fromCharCode(ss[i]);
}
return s;
}
// --------------------------------------------------------------------------------------------
function new_hhcool_comics_crawler(configuration, callback, initializer) {
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
// 每次呼叫皆創建一個新的實體。
var crawler = new library_namespace.work_crawler(configuration);
if (typeof initializer === 'function') {
initializer(crawler);
}
var decode_filename = 'script/view.js', unsuan;
library_namespace.get_URL_cache(crawler.base_URL + decode_filename,
//
function(contents, error) {
if (false) {
eval('unsuan=function'
+ contents.between('function unsuan', '\nvar'));
}
callback(crawler);
}, crawler.main_directory + decode_filename.match(/[^\\\/]+$/)[0]);
}
return new_hhcool_comics_crawler;
}

View File

@@ -0,0 +1,201 @@
/**
* @name CeL module for downloading jieqi article novels.
*
* @fileoverview 本檔案包含了解析並處理、批量下載中國大陸常見小說管理系統: 杰奇小说连载系统 的工具。
*
* <code>
CeL.jieqi_article(configuration).start(work_id);
</code>
*
* @see http://www.jieqi.com/files/page/html/product/article.html 杰奇网络 杰奇小说连载系统
* 2004-2015?,新版为杰奇原创文学系统)
*
* @since 2019/2/20 16:58:20 模組化。
*/
// More examples:
// @see https://github.com/kanasimi/work_crawler/blob/master/81xsw.js
// @see https://github.com/kanasimi/work_crawler/blob/master/23us.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.jieqi_article',
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',
// base_URL : '',
charset : 'gbk',
// 解析 作品名稱 → 作品id get_work()
search_URL : function(work_title) {
return [ 'modules/article/search.php', {
searchtype : 'articlename',
searchkey : work_title
} ];
},
parse_search_result : function(html, get_label) {
// console.log(html);
var id_list = [], id_data = [];
var matched = html.match(/og:novel:book_name" content="([^<>"]+)"/);
if (matched) {
// 直接進入作品資訊頁面。
id_data.push(get_label(matched[1]));
matched = html
// e.g., <meta property="og:novel:read_url"
// content="https://www.zhuishubang.com/120382/"/>
.match(/og:novel:read_url" content="[^<>"]*?\/(\d+)\/"/);
id_list.push(+matched[1]);
} else {
html.between('<div id="content">', '</table>')
//
.between('<table').each_between('<tr>', '</tr>',
//
function(text) {
var matched = text
// <td class="odd"><a
// href="https://www.kanshushenzhan.com/132800/">万古剑神</a></td>
.match(/ href="[^<>"]+\/(\d+)\/">(.+?)<\/a>/);
id_list.push(+matched[1]);
id_data.push(get_label(matched[2]));
});
}
return [ id_list, id_data ];
},
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
return work_id + '/';
},
parse_work_data : function(html, get_label, extract_work_data) {
var work_data = {
// 必要屬性:須配合網站平台更改。
// 選擇性屬性:須配合網站平台更改。
// <div class="routeLeft"><a href="/">看书神站</a> > <a
// href="/all/">书库</a>
site_name : get_label(html.between('<div class="routeLeft">',
'</a>'))
};
// overwrite .description
extract_work_data(work_data, html, null, true);
extract_work_data(work_data, html.between('<div class="bookPhr">',
'<div class="renew">'), /<dd>([^]+)(.+)<\/dd>/g);
var matched = html.between('<div class="renew">', '</div>').match(
// <div class="renew">最新章节:<a href="/111269/40729086.html">第535章
// 大结局</a><span>2018-08-01</span></div>
/<a href="([^<>"]+)">(.+?)<\/a>(?:<span>([^<>]+?)<\/span>)?/);
Object.assign(work_data, {
latest_chapter : work_data.latest_chapter_name,
latest_chapter_url : matched[1],
last_update : work_data.update_time || matched[3]
});
// console.log(work_data);
return work_data;
},
get_chapter_list : function(work_data, html, get_label) {
work_data.chapter_list = [];
html = html.between('<div class="chapterCon">', '</ul>');
var matched,
// <li><a href="/111269/40724950.html">第1章 甩你一脸</a></li>
PATTERN_chapter = /<a href="([^"<>]+)">([^<>]+)<\/a>/g;
while (matched = PATTERN_chapter.exec(html)) {
// console.log(matched);
var chapter_data = {
url : matched[1],
title : get_label(matched[2])
};
work_data.chapter_list.push(chapter_data);
}
if (this.inverted_order)
work_data.chapter_list.reverse();
// console.log(work_data);
},
// inverted_order : true,
// 取得每一個章節的內容與各個影像資料。 get_chapter_data()
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
// 在取得小說章節內容的時候,若發現有章節被目錄漏掉,則將之補上。
this.check_next_chapter(work_data, chapter_NO, html);
// var chapter_data = work_data.chapter_list[chapter_NO - 1];
var sub_title = work_data.previous_sub_title
|| get_label(html.between('<h2>', '</h1>')),
// e.g., https://www.huaxiangju.com/25087/6323179.html
text = html.between('<div class="articleCon">');
text = text.between(null, '<div class="page">')
|| html.between(null, '</div>');
text = text.between('<p>', {
tail : '</p>'
});
if (false && !html.includes('<div class="articleCon">')) {
// console.log(html);
console.log(html.between('<div class="articleCon">', '</div>'));
console.log(text);
}
if (this.remove_ads) {
text = this.remove_ads(text);
}
// console.log(text);
this.add_ebook_chapter(work_data, chapter_NO, {
sub_title : sub_title,
text : text
});
}
};
// --------------------------------------------------------------------------------------------
function new_jieqi_article_novels_crawler(configuration) {
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
// 每次呼叫皆創建一個新的實體。
return new library_namespace.work_crawler(configuration);
}
return new_jieqi_article_novels_crawler;
}

View File

@@ -0,0 +1,410 @@
/**
* @name CeL module for downloading manhuadb comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載中國大陸漫畫網站 漫画DB 平臺 的工具。
*
* modify from 9mdm.js→dagu.js
*
* 由於 漫画DB 系列網站下載機制較複雜,下載圖片功能為獨立撰寫出來,不支援 achive_images 功能。
*
* <code>
CeL.manhuadb(configuration).start(work_id);
</code>
*
* @since 2021/4/9 19:42:20 模組化。
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/manhuadb.js
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/manhuacat.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.manhuadb',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
// manhuadb.js:
// <a class="fixed-a-es" target="_blank" href="/manhua/000/....html"
// title="第01回">第01回</a>
//
// <div class="align-self-center pr-2"><h3 class="h4 mb-0 font-weight-normal
// comic_version_title">[辉夜姬想让人告白~天才们的恋爱头脑战~ 连载]</h3></div>
// manhuacat.js:
// <a class="fixed-a-es"
// href="https://www.manhuacat.com/manga/475/523727.html"
// title="第03话">第03话</a>
//
// <h2 class="h2 mb-0 font-weight-normal comic_version_title">单话</h2>
// [ all, chapter url, title, part title tag name, part title ]
var PATTERN_chapter = /<li[\s\S]+?<a [^<>]*?href="([^<>"]+)"[^<>]*? title="([^<>"]+)"|<(h[23])[^<>]*>(.+?)<\/\3>/g;
var default_configuration = {
// 本站常常無法取得圖片,因此得多重新檢查。
// recheck:從頭檢測所有作品之所有章節與所有圖片。不會重新擷取圖片。對漫畫應該僅在偶爾需要從頭檢查時開啟此選項。
// recheck : true,
// 當有多個分部的時候才重新檢查。
recheck : 'multi_parts_changed',
// 當無法取得chapter資料時直接嘗試下一章節。在手動+監視下recheck時可併用此項。
// skip_chapter_data_error : true,
// allow .jpg without EOI mark.
// allow_EOI_error : true,
// 當圖像檔案過小,或是被偵測出非圖像(如不具有EOI)時,依舊強制儲存檔案。
// e.g., 736 黄昏流星群/单行本 0005
// [黄昏流星群][弘兼宪史][尖端][volink]Vol_005/736-5-005.jpg
skip_error : true,
// 單行本圖片較多且大,因此採用一個圖一個圖取得的方式。
one_by_one : true,
// 下載圖片的逾時ms數。若逾時時間太小如10秒下載大檔案容易失敗。
timeout : 90 * 1000,
// reget_image_page : true,
// 解析 作品名稱 → 作品id get_work()
search_URL : 'search?q=',
PATTERN_search : /<a href="\/manhua\/(\d+)" title="([^<>"]+)"/,
parse_search_result : function(html, get_label) {
// console.log(html);
var PATTERN_search = this.PATTERN_search;
var id_list = [], id_data = [];
html.each_between('<div class="comicbook-index', '</div>',
function(token) {
// console.log(token);
var matched = token.match(PATTERN_search);
id_list.push(matched[1]);
id_data.push(get_label(matched[2]));
});
return [ id_list, id_data ];
},
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
return 'manhua/' + work_id;
},
parse_work_data : function(html, get_label, extract_work_data) {
// console.log(html);
var work_data = {
// 必要屬性:須配合網站平台更改。
// 選擇性屬性:須配合網站平台更改。
// 漫画出版信息
publish : get_label(html.between(
'<div class="comic-pub-data-section', '</div>')
.between('>')),
// 概要 synopsis
description : get_label(html.between(
// manhuadb.js:
'<div class="comic_detail_content">', '</div>')
// manhuacat.js:
|| html.between('<p class="comic_story">', '</p>')
//
.between('漫画简介:'))
};
extract_work_data(work_data, html);
extract_work_data(work_data, html,
/<th scope="row">([^<>]+)<\/th>([\s\S]*?)<\/td>/g);
Object.assign(work_data, {
title : work_data.book_name
});
// console.log(work_data);
return work_data;
},
add_part : true,
get_chapter_list : function(work_data, html, get_label) {
// <div class="comic-toc-section bg-white p-3">
// e.g., 一拳超人
var part_title_list = html.between('<div class="comic-toc-section',
'</ul>').all_between('<li', '</li>').map(function(token) {
return get_label(token.between('>')).replace(/列表$/, '');
});
// console.log(part_title_list);
// <div class="tab-content" id="comic-book-list">
html = html.between(' id="comic-book-list">', '<script ').between(
null, {
tail : '</ol>'
});
var matched, part_NO = 0, part_title, PATTERN_title = new RegExp(
work_data.title + '\\s*'), NO_in_part;
work_data.chapter_list = [];
while (matched = PATTERN_chapter.exec(html)) {
// delete matched.input;
// console.log(matched);
if (matched[4]) {
part_title = get_label(matched[3]).replace(PATTERN_title,
'').replace(/\[\]/g, '');
part_title = part_title_list[part_NO++];
NO_in_part = 0;
continue;
}
++NO_in_part;
var chapter_data = {
// 漫畫目錄名稱不須包含分部號碼。使章節目錄名稱不包含 part_NO。
// part_NO : part_NO,
part_title : part_title,
NO_in_part : NO_in_part,
chapter_NO : NO_in_part,
url : matched[1],
title : get_label(matched[2])
};
work_data.chapter_list.push(chapter_data);
continue;
// ----------------------------------
// 以下: 若是存在舊格式的檔案就把它移成新格式。
// @deprecated
// console.log(chapter_data);
// chapter_data.title = chapter_data.title.replace('文传', '文傳');
var old_directory = work_data.directory
+ work_data.chapter_list.length
// 4: @see chapter_directory_name
// @ CeL.application.net.work_crawler.chapter
.pad(work_data.chapter_NO_pad_digits || 4)
+ ' '
+ (chapter_data.title.includes('[') ? chapter_data.title
: '[' + chapter_data.title + ']'),
//
new_directory = work_data.directory + part_title + ' '
+ NO_in_part.pad(work_data.chapter_NO_pad_digits || 4)
+ ' ' + chapter_data.title;
if (library_namespace.directory_exists(old_directory)) {
library_namespace.move_fso(old_directory, new_directory);
}
var old_archive = old_directory + '.'
+ this.images_archive_extension;
if (library_namespace.file_exists(old_archive)) {
library_namespace.log(old_archive + '\n→ ' + new_directory);
var images_archive = new library_namespace.storage.archive(
old_archive);
images_archive.extract({
cwd : images_archive
});
library_namespace.move_fso(old_directory, new_directory);
library_namespace.remove_file(old_archive);
}
}
work_data.inverted_order = this.inverted_order;
// console.log(work_data.chapter_list);
// console.log(work_data);
},
decode_chapter_data : function(chapter_data) {
return JSON.parse(atob(chapter_data));
},
pre_parse_chapter_data
// 執行在解析章節資料 process_chapter_data() 之前的作業 (async)。
// 必須自行保證執行 callback(),不丟出異常、中斷。
: function(XMLHttp, work_data, callback, chapter_NO) {
// console.log(XMLHttp);
// console.log(work_data);
// console.log(work_data.chapter_list);
var chapter_data = work_data.chapter_list[chapter_NO - 1],
//
html = XMLHttp.responseText, _this = this, image_page_list = [];
// console.log(html);
chapter_data.title = html.between('<h2 class="h4 text-center">',
'</h2>')
|| chapter_data.title;
var matched = chapter_data.title.match(/^\[([^\[\]]+)\]$/);
if (matched)
chapter_data.title = matched[1];
// --------------------------------------
// 2019/9/17 漫画DB 網站改版
matched = html.between(" img_data = '", "';")
// manhuacat.js
|| html.between(' img_data = "', '"')
// 2019/9/17 5:0
|| html.between('localStorage.setItem("data:"', ');')
.between("'", {
tail : "'"
});
if (matched) {
// console.log(atob(matched));
// console.log(chapter_data);
// 2020/4 漫画DB 網站改版
// @see https://www.manhuadb.com/assets/js/vg-read.js
var image_prefix = this.image_prefix
|| html.between(' data-host="', '"')
+ html.between(' data-img_pre="', '"');
// console.log(image_prefix);
// img_data is base64 encoded, need to do base64 decode before
// json
// decode
chapter_data.image_list = this.decode_chapter_data(matched)
// assert: Array.isArray(chapter_data.image_list);
.map(function(image_data) {
return {
url : encodeURI(image_prefix
//
+ (image_data.img || image_data))
};
});
// console.log(chapter_data.image_list);
callback();
return;
}
// --------------------------------------
html.between('id="page-selector"', '</select>').each_between(
//
'<option value="', '</option>', function(token) {
image_page_list.push({
title : token.between('>'),
url : token.between(null, '"')
});
});
var image_count = image_page_list.length;
// console.log(image_page_list);
if (!(image_count >= 0)) {
throw work_data.title + ' #' + chapter_NO + ' '
+ chapter_data.title + ': Cannot get image count!';
}
// 將過去的 chapter_data.image_list cache 於 work_data.image_list。
if (work_data.image_list) {
chapter_data.image_list = work_data.image_list[chapter_NO - 1];
if (!this.reget_image_page && chapter_data.image_list
&& chapter_data.image_list.length === image_count) {
library_namespace.debug(work_data.title + ' #' + chapter_NO
+ ' ' + chapter_data.title + ': Already got '
+ image_count + ' images.');
chapter_data.image_list = chapter_data.image_list
// .slice() 重建以節省記憶體用量。
.slice().map(function(image_data) {
// 僅保留網址資訊,節省記憶體用量。
return typeof image_data === 'string' ? image_data
// else assert: library_namespace.is_Object(image_data)
: image_data.url;
});
callback();
return;
}
} else {
work_data.image_list = [];
}
function extract_image(XMLHttp) {
var html = XMLHttp.responseText,
//
url = html.between('<img class="img-fluid"', '>').between(
' src="', '"');
library_namespace.debug('Add image '
+ chapter_data.image_list.length + '/' + image_count
+ ': ' + url, 2, 'extract_image');
if (!url && !_this.skip_error) {
_this.onerror('No image url got: #'
+ chapter_data.image_list.length + '/'
+ image_count);
}
// 僅保留網址資訊,節省記憶體用量。
chapter_data.image_list.push(url);
}
chapter_data.image_list = [];
if (image_count > 0)
extract_image(XMLHttp);
library_namespace.run_serial(function(run_next, image_NO, index) {
var image_page_url
//
= _this.full_URL(image_page_list[index - 1].url);
// console.log('Get #' + index + ': ' + image_page_url);
library_namespace.log_temporary('Get image data page of §'
+ chapter_NO + ': ' + image_NO + '/' + image_count);
library_namespace.get_URL(image_page_url, function(XMLHttp) {
extract_image(XMLHttp);
run_next();
}, _this.charset, null, Object.assign({
error_retry : _this.MAX_ERROR_RETRY
}, _this.get_URL_options));
}, image_count, 2, function() {
work_data.image_list[chapter_NO - 1] = chapter_data.image_list
// .slice() 重建以節省記憶體用量。
.slice();
callback();
});
},
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
var chapter_data = work_data.chapter_list[chapter_NO - 1];
// console.log(chapter_data);
// 已在 pre_parse_chapter_data() 設定完 {Array}chapter_data.image_list
return chapter_data;
}
};
// --------------------------------------------------------------------------------------------
function new_manhuadb_comics_crawler(configuration, callback, initializer) {
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
// 每次呼叫皆創建一個新的實體。
var crawler = new library_namespace.work_crawler(configuration);
if (typeof initializer === 'function') {
initializer(crawler);
}
if (!crawler.decoder_URL) {
// e.g., comic.cmn-Hans-CN/manhuadb.js
return crawler;
}
// e.g., comic.cmn-Hans-CN/manhuacat.js
library_namespace.get_URL_cache(crawler.decoder_URL, function(contents,
error) {
var LZString;
contents = contents.replace(/var\s+(LZString)/, '$1');
eval(contents);
crawler.LZString = LZString;
callback(crawler);
}, {
directory : crawler.main_directory
});
}
return new_manhuadb_comics_crawler;
}

View File

@@ -0,0 +1,426 @@
/**
* @name CeL module for downloading manhuagui comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載中國大陸漫畫網站 漫画柜 的工具。
*
* 2017/10: 爱看漫/看漫画改名(DNS被導引到)漫画柜
*
* 57mh 介面程式碼類似於 999comics。manhuagui 似乎是在這基礎上經過修改?
* @see CeL.application.net.work_crawler.sites.SinMH2013
*
* @see http://www.manhua.demo.shenl.com/?theme=mhd
* @see qTcms 晴天漫画程序 晴天漫画系统 http://manhua3.qingtiancms.net/
*
* <code>
CeL.manhuagui(configuration, function(crawler) {
start_crawler(crawler, typeof module === 'object' && module);
}, function(crawler) {
setup_crawler(crawler, typeof module === 'object' && module);
});
</code>
*
* @since 2019/6/17 19:32:13 模組化。
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/manhuagui.js
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/manhuagui_tw.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.manhuagui',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
// core_9D227AD5A911B7758A332C9CA35C640C.js
// 2017/3/11 20:6:35: core_33A91659E79CDC4A0F31ED884877F3EF.js
// 2018/1/30與2/2之間改版:
// http://c.3qfm.com/scripts/core_E95EAFDD32D7F97E369526C7AD9A8837.js
// 2018/2/14:
// http://cf.hamreus.com/scripts/core_6B1519CED0A3FA5ED82E8FBDA8F1AB90.js
// 2018/3/16 00:17:25 GMT: add https: for image files
// https://cf.hamreus.com/scripts/core_ABBA2B6ADC1DABE325D505BE3314C273.js
// 2019/6/17 19:47:4
// https://cf.hamreus.com/scripts/core_C0683FDCDEE69940232A703BDEB0F64F.js
// https://cf.hamreus.com/scripts_tw/core_C0683FDCDEE69940232A703BDEB0F64F.js
var core_filename = 'core_C0683FDCDEE69940232A703BDEB0F64F.js',
// https://raw.githubusercontent.com/pieroxy/lz-string/master/libs/lz-string.js
// 2017: main_3A454149B2D2500411BC344B15DB58A4.js'
// 2018/2:
// http://c.3qfm.com/scripts/config_25855B4C08F7A6545A30D049ABD0F9EE.js
// 2018/2/14:
// http://cf.hamreus.com/scripts/config_25855B4C08F7A6545A30D049ABD0F9EE.js
// 2018/3/11 15:59:14 GMT:
// https://cf.hamreus.com/scripts/config_FAF1BF617BAF8A691A828F80672D3588.js
// https://cf.hamreus.com/scripts_tw/config_FAF1BF617BAF8A691A828F80672D3588.js
decode_filename = 'config_FAF1BF617BAF8A691A828F80672D3588.js',
/**
* e.g., <code>
<li><a href="/comic/17515/272218.html" title="第72话一虎进击" class="status0" target="_blank"><span>第72话一…<i>31p</i><em class="new"></em></span></a></li>
https://www.manhuagui.com/comic/4076/
<script type="text/javascript">$.Tabs('#chapter-page-1 li', '#chapter-list-1 ul',{'trigger':'click'});</script><h4><span>单行本</span></h4><div class="chapter-list cf mt10" id='chapter-list-1'><ul style="display:block"><li><a href="/comic/4076/390110.html" title="第67卷" class="status0" target="_blank"><span>第67卷<i>180p</i></span></a></li>
</code>
*
* [:]: incase href="javascript:;"
*
* matched: [ all, href, title, inner, part_title ]
*/
PATTERN_chapter = /<li><a href="([^"<>:]+)" title="([^"<>]+)"[^<>]*>(.+?)<\/a><\/li>|<h4>(.+?)<\/h4>/g;
var default_configuration = {
// 當有多個分部的時候才重新檢查。
recheck : 'multi_parts_changed',
one_by_one : true,
base_URL : 'https://www.manhuagui.com/',
script_base_URL : 'https://cf.hamreus.com/scripts/',
// {Natural}MIN_LENGTH:最小容許圖案檔案大小 (bytes)。
MIN_LENGTH : 400,
// 當網站不允許太過頻繁的訪問/access時可以設定下載之前的等待時間(ms)。
// 模仿實際人工請求。
// 2018/4: manhuagui 不允許過於頻繁的 access會直接 ban IP。
// 2018/7/12 22:29:18: 9s: NG, ban 2 hr.
// 10s, 15s 在下載過100章(1 hr)之後一樣會 ban 5hr。
// 20s, 30s 在下載過200章(~2 hr)之後一樣會 ban。
// 60s 大致OK
// 2019/2/6: 40s: NG, ban 1 day. 50s 在下載過50章後一樣會 ban。.5 day?
// 2019/3/1: 181s: NG. 3~4min 時,似乎會不固定時間檢查、平均每天被封鎖一次,每次封鎖一日?
chapter_time_interval : '4min',
// 2018/3/3 已經不再有常常出現錯誤的情況。
// allow .jpg without EOI mark.
// allow_EOI_error : true,
// 當圖像檔案過小,或是被偵測出非圖像(如不具有EOI)時,依舊強制儲存檔案。
// skip_error : true,
// 取得伺服器列表。
// use_server_cache : true,
server_URL : function() {
return this.script_base_URL + core_filename;
},
parse_server_list : function(html) {
// console.log(html);
var server_list = [];
eval(html.between('var servs=', ',pfuncs=')).forEach(
function(data) {
data.hosts.forEach(function(server_data) {
// @see SMH.utils.getPath() @ ((core_filename))
server_list.push(server_data.h + '.hamreus.com');
});
});
return server_list;
},
// 解析 作品名稱 → 作品id get_work()
search_URL : '/tools/word.ashx?key=',
parse_search_result : function(html) {
/**
* e.g.,<code>
[ { "t": "西游", "u": "/comic/17515/", "s": false, "cid": 272218, "ct": "第72话一虎进击", "a": "郑健和,邓志辉" } ]
</code>
*/
var id_data = html ? JSON.parse(html) : [];
return [ id_data, id_data ];
},
id_of_search_result : function(search_result) {
// e.g., "/comic/123/"
return +search_result.u.match(/^\/comic\/(\d+)\/$/)[1];
},
title_of_search_result : 't',
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
return 'comic/' + work_id + '/';
},
parse_work_data : function(html, get_label, extract_work_data) {
var work_data = {
// 必要屬性:須配合網站平台更改。
title : get_label(html.between('<h1>', '</h1>')),
// 選擇性屬性:須配合網站平台更改。
status : get_label(html.between('<li class="status">',
'</span>').between('</strong>')),
sub_title : get_label(html.between('<h1>', '</div>').between(
'</h1>')),
description : get_label(html.between('intro-all', '</div>')
.between('>'))
}, data = html.between('detail-list', '</ul>');
extract_work_data(work_data, data,
// e.g., "<strong>漫画别名:</strong>暂无</span>"
/<strong[^<>]*>([^<>]+)<\/strong>(.+?)<\/span>/g);
if (data = get_label(data.between('<li class="status">', '</li>'))) {
library_namespace.log(data);
}
return work_data;
},
get_chapter_list : function(work_data, html, get_label) {
var data, chapter_list = [], matched,
//
part_title, part_title_hash = Object.create(null), part_NO = 0;
// 有些尚使用舊模式。
// @see http://www.ikanman.com/comic/8004/
data = html.between('<div class="chapter-bar">',
// <div class="comment mt10" id="Comment">
'class="comment')
// 2017/3/3? ikanman 改版
|| LZString.decompressFromBase64(
//
html.between('id="__VIEWSTATE"', '>').between('value="', '"'));
while (matched = PATTERN_chapter.exec(data)) {
// delete matched.input;
// console.log(matched);
var chapter_data = get_label(matched[4]);
// console.log(chapter_data);
if (chapter_data) {
// console.log(chapter_data);
part_title = chapter_data;
part_title_hash[part_title]
// last part NO. part_NO starts from 1
= ++part_NO;
continue;
}
chapter_data = {
url : matched[1],
title : get_label(matched[2]
// .check_downloaded_chapters() 必須先確保已獲得最終之
// chapter_data.title。
// + ' ' + matched[3].between('<i>', '</i>')
)
};
if (matched = matched[1].match(/(\d+)\.html$/)) {
chapter_data.id = matched[1] | 0;
} else {
chapter_list.some_without_id = chapter_data;
}
if (part_title) {
chapter_data.part_title = part_title
}
chapter_list.push(chapter_data);
}
// console.log(chapter_list);
if (chapter_list.length === 0
// e.g., <div class="book-btn"><a href="/comic/8772/86612.html"
// target="_blank" title="1话" class="btn-read">开始阅读</a>
&& (data = html.between('book-btn', '</a>'))) {
// 尊敬的看漫画用户,应《》版权方的要求,现已删除屏蔽《》漫画所有卷和册,仅保留作品文字简介
this.pre_chapter_URL = this._pre_chapter_URL;
if (Array.isArray(work_data.chapter_list)
&& work_data.chapter_list.length > 1) {
work_data.last_download.chapter
// use cache (old data)
= work_data.chapter_list.length;
} else {
work_data.chapter_list = [ {
url : data.match(/ href="([^<>"]+)"/)[1],
title : data.match(/ title="([^<>"]+)"/)[1]
} ];
}
chapter_list = work_data.chapter_list;
} else {
if (chapter_list.length > 1) {
// 轉成由舊至新之順序。
if (chapter_list.some_without_id) {
library_namespace.warn({
// gettext_config:{"id":"some-chapter-url-names-are-not-numbers-$1"}
T : [ '有些篇章之URL檔名非數字%1',
//
JSON.stringify(chapter_list.some_without_id) ]
});
chapter_list.reverse();
} else {
// 按照章節添加時間排序。
chapter_list.sort(function(a, b) {
// 排序以.html檔案檔名(序號)為準。
// assert: 後來的檔名,序號會比較大。
// @see http://www.ikanman.com/comic/8928/
return a.id - b.id;
});
}
// console.log(chapter_list);
// set latest/max part_NO
chapter_list.part_NO = part_NO;
if (part_NO > 1) {
// rearrange part_NO
// 初始化 NO_in_part
var NO_in_part_hash = new Array(part_NO + 1).fill(0);
chapter_list.forEach(function(chapter_data) {
chapter_data.part_NO
// 在維持分部順序不動的情況下排序 NO_in_part
= part_title_hash[chapter_data.part_title];
chapter_data.NO_in_part
//
= ++NO_in_part_hash[chapter_data.part_NO];
});
// 重新按照 分部→章節順序 排序。
chapter_list.sort(function(a, b) {
// assert: max(NO_in_part) < 1e4
return (a.part_NO - b.part_NO) * 1e4 + a.NO_in_part
- b.NO_in_part;
});
}
}
// console.log(chapter_list);
// console.log(JSON.stringify(chapter_list));
// console.log(chapter_list.slice(0, 20));
// console.log(chapter_list.slice(-20));
// console.log(work_data.chapter_list);
work_data.chapter_list = chapter_list;
if (this.recheck === 'multi_parts_changed'
&& chapter_list.part_NO > 1) {
this.check_downloaded_chapters(work_data, chapter_list);
}
}
},
// 取得每一個章節的各個影像內容資料。 get_chapter_data()
_pre_chapter_URL : function(work_data, chapter_NO, callback) {
var chapter_data = work_data.chapter_list[chapter_NO - 1],
// e.g., "/comic/8772/86612.html"
chapter_id = +chapter_data.url.match(/^\/comic\/\d+\/(\d+)\.html$/)[1];
library_namespace.get_URL(this.base_URL
+ 'support/chapter.ashx?bid=' + work_data.id + '&cid='
+ chapter_id, function(XMLHttp) {
// console.log(XMLHttp.responseText);
chapter_data.sibling = JSON.parse(XMLHttp.responseText);
if (chapter_data.sibling.n > 0
&& work_data.chapter_count === chapter_NO) {
// 還有下一chapter。
work_data.chapter_list.push({
url : chapter_data.url.replace(/(\d+)\.html$/,
chapter_data.sibling.n + '.html')
});
work_data.chapter_count = work_data.chapter_list.length;
}
callback();
}, null, null, this.get_URL_options);
},
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
// decode chapter data
function decode_2016(code) {
code = eval(code);
eval(code.replace('eval', 'code='));
eval(code.replace(/^[^=]+/, 'code'));
return code;
}
// 2017/3/3? ikanman 改版
// String.prototype.splic: used in chapter
function decode(code) {
code = eval(code);
// 2018/3/16 改版
eval(code.between(null, {
tail : ').preInit()'
}).replace('SMH.imgData(', 'code='));
return code;
}
var chapter_data = html.between(
// window["eval"], window["\x65\x76\x61\x6c"]
'<script type="text/javascript">window["\\x65\\x76\\x61\\x6c"]',
'</script>');
if (!chapter_data || !(chapter_data = decode(chapter_data))) {
library_namespace.warn({
// gettext_config:{"id":"unable-to-parse-chapter-data-for-«$1»-§$2"}
T : [ '無法解析《%1》§%2 之章節資料!', work_data.title, chapter_NO ]
});
return;
}
chapter_data = Object.assign(
work_data.chapter_list[chapter_NO - 1], chapter_data);
// for debug
// console.log(chapter_data);
// throw this.id + ': debug throw';
// 設定必要的屬性。
chapter_data.title = chapter_data.cname;
chapter_data.image_count = chapter_data.len;
// e.g., "/ps3/q/qilingu_xmh/第01回上/"
var path = encodeURI(chapter_data.path),
// 令牌 @see SMH.utils.getPicUrl() @ ((core_filename))
token = '?cid=' + chapter_data.cid + '&'
//
+ new URLSearchParams(chapter_data.sl);
// 漫畫櫃的webp圖像檔案可能是即時生成的? 大小常常不一樣。
chapter_data.image_list = chapter_data.files.map(function(url) {
return {
url : path + url + token
}
});
// 當一次下載上百張相片的時候就會被封鎖IP。因此改成一個個下載圖像。
this.one_by_one = chapter_data.image_list.length > 30;
return chapter_data;
}
};
// --------------------------------------------------------------------------------------------
function new_manhuagui_comics_crawler(configuration, callback, initializer) {
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
// 每次呼叫皆創建一個新的實體。
var crawler = new library_namespace.work_crawler(configuration);
if (typeof initializer === 'function') {
initializer(crawler);
}
// 創建 main directory。
library_namespace.create_directory(crawler.main_directory);
var LZString;
library_namespace.get_URL_cache(crawler.script_base_URL
+ decode_filename,
// 2017/3/3? ikanman 改版
function(contents, error) {
contents = contents.between('\nwindow["\\x65\\x76\\x61\\x6c"]',
';\n')
//
.replace(/window\[([^\[\]]+)\]/g, function($0, key) {
return eval(key);
});
contents = eval(contents).replace(/^var /, '');
eval(contents);
callback(crawler);
}, crawler.main_directory + decode_filename);
}
return new_manhuagui_comics_crawler;
}

View File

@@ -0,0 +1,206 @@
/**
* @name CeL module for downloading **maybe** qTcms 2014 version comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載中國大陸常見漫畫管理系統: **可能為** 舊型晴天漫画CMS (晴天漫画系统 晴天漫画程序,
* NOT 晴天新漫画系统) 的工具。
*
* <code>
CeL.qTcms2014(configuration).start(work_id);
</code>
*
* @see http://manhua.qingtiancms.com/
*
* @since 2019/1/21 模組化。
*/
// More examples:
// @see archive/733dm.201811.js , archive/733dm.201808.js
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/katui.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.qTcms2014',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
var default_configuration = {
// 所有的子檔案要修訂註解說明時應該都要順便更改在CeL.application.net.comic中Comic_site.prototype內的母comments並以其為主體。
// 本站常常無法取得圖片,因此得多重新檢查。
// recheck:從頭檢測所有作品之所有章節與所有圖片。不會重新擷取圖片。對漫畫應該僅在偶爾需要從頭檢查時開啟此選項。
// recheck : true,
// 當無法取得chapter資料時直接嘗試下一章節。在手動+監視下recheck時可併用此項。
// skip_chapter_data_error : true,
// allow .jpg without EOI mark.
// allow_EOI_error : true,
// 當圖像檔案過小,或是被偵測出非圖像(如不具有EOI)時,依舊強制儲存檔案。
// skip_error : true,
// one_by_one : true,
// base_URL : 'http://www.___.net/',
charset : 'gb2312',
// 取得伺服器列表。
// use_server_cache : true,
// katui: /skin/2014mh/global.js
// pufei: /skin/2014mh/global.js?v=41
// taduo: /skin/2014mh/global.js?v=42
server_URL : 'skin/2014mh/global.js',
parse_server_list : function(html) {
var server_list = [],
// e.g., WebimgServerURL[0]="http://img.tsjjx.com/"
// WebimgServerURL[0]="http://www.733mh.com/fd.php?url=http://img.tsjjx.com/";
matched, PATTERN = /\nWebimgServerURL\[\d\]\s*=\s*"([^"]+)"/g;
while (matched = PATTERN.exec(html)) {
server_list.push(matched[1].between('url=') || matched[1]);
}
// console.log(server_list);
return server_list;
},
// 解析 作品名稱 → 作品id get_work()
search_URL : function(work_title) {
return [ 'e/search/index.php', {
// orderby : 1,
// myorder : 1,
tbname : 'mh',
// tempid:1 @ https://www.dagumanhua.com/
tempid : 3,
show : 'title,player,playadmin,bieming,pinyin',
keyboard : work_title
} ];
},
parse_search_result : function(html) {
// console.log(html);
html = html.between('id="dmList"', '</div>');
var id_list = [], id_data = [];
html.each_between('<li>', '</li>', function(token) {
var matched = token.match(
// pufei.js: <dt><a href="/manhua/32695/index.html"
// title="我靠美食来升级">我靠美食来升级</a></dt>
/<dt><a href="\/(?:mh|manhua)\/(\d+)(?:\/index\.html)?" title="([^"]+)">/
//
);
id_list.push(matched[1]);
id_data.push(matched[2]);
});
// console.log([ id_list, id_data ]);
return [ id_list, id_data ];
},
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
return 'manhua/' + work_id + '/';
},
parse_work_data : function(html, get_label) {
var text = html.between('<div class="detailInfo">',
'<div class="intro'),
// work_data={id,title,author,authors,chapters,last_update,last_download:{date,chapter}}
work_data = {
// 必要屬性:須配合網站平台更改。
title : get_label(
//
text.between('<div class="titleInfo">', '</h1>')),
// 選擇性屬性:須配合網站平台更改。
status : get_label(text.between('</h1><span>', '</span>')),
description : get_label(html.between(
'<div class="introduction" id="intro1">', '</div>'))
};
text.each_between('<li class="twoCol">', '</li>', function(token) {
work_data[get_label(token.between('<span>', '</span>'))
.replace(/$/, '')] = get_label(token
.between('</span>'));
});
Object.assign(work_data, {
author : work_data.作者,
last_update : work_data.更新时间
});
return work_data;
},
get_chapter_list : function(work_data, html, get_label) {
html = html.between('<div id="section">',
'<div class="description">');
work_data.chapter_list = [];
var matched,
// [ , chapter_url, chapter_title ]
PATTERN_chapter = /<a href="(\/manhua\/[^"]+)" title="([^"]+)"/g;
while (matched = PATTERN_chapter.exec(html)) {
work_data.chapter_list.push({
url : matched[1],
title : get_label(matched[2])
});
}
if (work_data.chapter_list.length > 1) {
// 轉成由舊至新之順序。
work_data.chapter_list.reverse();
}
},
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
function decode(packed) {
var photosr = [];
// decode chapter data @ every picture page
eval(eval(Buffer.from(packed, 'base64').toString().slice(4)));
// 通常[0]===undefined
return photosr.filter(function(url) {
return url;
});
}
var chapter_data = html && html.between('packed="', '"');
if (!chapter_data || !(chapter_data = decode(chapter_data))) {
return;
}
// console.log(JSON.stringify(chapter_data));
// console.log(chapter_data.length);
// library_namespace.set_debug(6);
if (typeof this.postfix_image_url === 'function')
chapter_data = chapter_data.map(this.postfix_image_url);
chapter_data = Object.assign(
// 設定必要的屬性。
work_data.chapter_list[chapter_NO - 1], {
image_list : chapter_data
});
// console.log(JSON.stringify(chapter_data));
return chapter_data;
}
};
// --------------------------------------------------------------------------------------------
function new_qTcms2014_comics_crawler(configuration) {
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
// 每次呼叫皆創建一個新的實體。
return new library_namespace.work_crawler(configuration);
}
return new_qTcms2014_comics_crawler;
}

View File

@@ -0,0 +1,553 @@
/**
* @name CeL module for downloading qTcms version 20170501-20190606010315
* comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載中國大陸常見漫畫管理系統: 晴天漫画CMS (晴天漫画系统 晴天漫画程序, 晴天新漫画系统)
* PC端网站 + 手机端网站(行動版 mobile version) 的工具。
*
* <code>
CeL.qTcms2017(configuration).start(work_id);
</code>
*
* modify from 9mdm.js→dagu.js, mh160.js
*
* @see qTcms 晴天漫画程序 晴天漫画系统 http://manhua.qingtiancms.com/
*
* @since 2019/2/3 模組化。
*/
// More examples:
// @see comic.cmn-Hans-CN/nokiacn.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.qTcms2017',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
var default_configuration = {
// 所有的子檔案要修訂註解說明時應該都要順便更改在CeL.application.net.comic中Comic_site.prototype內的母comments並以其為主體。
// 因為要經過轉址,所以一個圖一個圖來。
// one_by_one : true,
// base_URL : '',
// fs.readdirSync('.').forEach(function(d){if(/^\d+\s/.test(d))fs.renameSync(d,'manhua-'+d);})
// fs.readdirSync('.').forEach(function(d){if(/^manhua-/.test(d))fs.renameSync(d,d.replace(/^manhua-/,''));})
// 所有作品都使用這種作品類別catalog前綴。
// common_catalog : 'manhua',
// 規範 work id 的正規模式提取出引數中的作品id 以回傳。
extract_work_id : function(work_information) {
if ((this.common_catalog ? /^[a-z\-\d]+$/ : /^[a-z]+_[a-z\-\d]+$/)
.test(work_information))
return work_information;
},
// --------------------------------------
// search comic via web page
// 解析 作品名稱 → 作品id get_work()
search_URL_web : 'statics/search.aspx?key=',
parse_search_result_web : function(html, get_label) {
// console.log(html);
html = html.between('<div class="cy_list">', '</div>');
// console.log(html);
var id_list = [], id_data = [];
html.each_between('<li class="title">', '</li>', function(token) {
// console.log(token);
var matched = token.match(
// [ id, title ]
/<a href="\/([a-z]+\/[a-z\-\d]+)\/"[^<>]*?>([^<>]+)/);
// console.log(matched);
if (this.common_catalog
// 去掉所有不包含作品類別catalog前綴者。
&& !matched[1].startsWith(this.common_catalog + '/'))
return;
id_list.push(this.common_catalog
//
? matched[1].slice((this.common_catalog + '/').length)
// catalog/latin name
: matched[1].replace('/', '_'));
id_data.push(get_label(matched[2]));
}, this);
// console.log([ id_list, id_data ]);
return [ id_list, id_data ];
},
// --------------------------------------
// default: search comic via API
// copy from 360taofu.js
// 解析 作品名稱 → 作品id get_work()
search_URL : function(work_title) {
return [ 'statics/qingtiancms.ashx', {
cb : 'jQuery' + ('1.7.2' + Math.random()).replace(/\D/g, "")
// @see .expando
+ '_' + Date.now(),
key : work_title,
action : 'GetSear1',
_ : Date.now()
} ];
},
parse_search_result : function(html, get_label) {
// console.log(html);
var data = eval(html.between('(', {
tail : ')'
}));
// console.log(data);
return [ data, data ];
},
id_of_search_result : function(data) {
// console.log(data);
// PC version: .u: webdir + classid1pinyin + titlepinyin + "/"
// webdir: "/"
// classid1pinyin: latin + "/"
// titlepinyin: latin
var matched = data.u
// mobile version
|| data.url;
matched = matched.match(/(?:\/|^)([a-z]+)\/([a-z\-\d]+)\/$/);
// assert: !!matched === true
if (!this.common_catalog)
return matched[1] + '_' + matched[2];
// assert: this.common_catalog === matched[1]
return matched[2];
},
title_of_search_result : 't',
// --------------------------------------
// for mobile version
// 解析 作品名稱 → 作品id get_work()
search_URL_mobile : function(work_title) {
return [ 'statics/qingtiancms.ashx', {
action : 'GetWapSear1',
key : work_title
} ];
},
parse_search_result_mobile : function(html, get_label) {
/**
* @example <code>
{"result": 1000,"msg": "提交成功","data": [{name:'读书成圣',last_update_chapter_name:'014 禁忌十八式',last_updatetime:'',types:'',authors:'',url:'/rexue/dushuchengsheng/'}],"page_data": ""}
</code>
*/
// console.log(JSON.stringify(html));
var data;
try {
eval('data=' + html);
data = data.data;
} catch (e) {
// e.g., "{err!}"
data = [];
}
// console.log(data);
return [ data, data ];
},
title_of_search_result_mobile : 'name',
// --------------------------------------
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
return (this.common_catalog ? this.common_catalog + '/' + work_id
// replace only the first '_' to '/'
: work_id.replace('_', '/')) + '/';
},
parse_work_data : function(html, get_label, extract_work_data) {
// console.log(html);
var work_data = html.between('qingtiancms_Details=', ';var');
if (work_data) {
/**
* PC version:
*
* @example <code>
var qingtiancms_Details={G_mubanpage:".html",id:"6638",hits:"9454",webdir:"/",pinglunid:"10",pinglunid1:"",pinglunid2:"cytdbnhsU",pinglunid3:"prod_1368b8102b9177303c660debbbbd257c",title:"读书成圣",classid1pinyin:"rexue/",titlepinyin:"dushuchengsheng"};var uyan_config = {'su':'/6638/'};
</code>
*/
eval('work_data=' + work_data);
} else {
// dagu.js: has NO `qingtiancms_Details`
work_data = Object.create(null);
}
// PC version: nokiacn.js, iqg365.js, 733dm.js
extract_work_data(work_data, html.between(
// <div class="cy_title">\n <h1>相合之物</h1>
'<h1>', ' id="comic-description">'),
/<span>([^<>]+)([\s\S]*?)<\/span>/g);
// PC version: 360taofu.js
extract_work_data(work_data, html.between(
// <div class="mh-date-info fl">\n <div class="mh-date-info-name">
'<div class="mh-date-info', '<div class="work-author">'),
// <span class="one"> 作者: <em>... </span>
// <span> 人气: <em... </span>
// 人气: 收藏数: 吐槽: 状态:
/<span[^<>]*>([^<>]+)([\s\S]*?)<\/span>/g);
// PC version 共通
extract_work_data(work_data, html.between(
// <div class="cy_zhangjie">...<div class="cy_zhangjie_top">
'<div class="cy_zhangjie_top">',
// <div class="cy_plist" id="play_0">
' class="cy_plist"'), /<p>([^<>]+)([\s\S]*?)<\/p>/g);
// PC version, mobile version 共通
extract_work_data(work_data, html);
Object.assign(work_data, this.is_mobile ? {
// 必要屬性:須配合網站平台更改。
last_update : html.between('<span class="date">', '</span>'),
// 選擇性屬性:須配合網站平台更改。
// 網頁中列的description比meta中的完整。
description : get_label(html.between(
// 友绘漫画网
// <p class="txtDesc autoHeight">介绍:...</p>
'<p class="txtDesc autoHeight">', '</p>'))
} : {
// 避免覆寫
qTid : work_data.id,
// 必要屬性:須配合網站平台更改。
title : work_data.title
// nokiacn.js, iqg365.js, 733dm.js
|| get_label(html.between('<h1>', '</h1>')),
author : work_data.作者,
status : work_data.状态,
last_update : work_data.更新时间,
latest_chapter : work_data.最新话,
latest_chapter_url : html.between('最新话:<a href="', '"'),
// 選擇性屬性:須配合網站平台更改。
评分 : work_data.评分 || get_label(html.between(
// 360taofu.js: <p class="fl">评分:<strong class="ui-text-orange"
// id="comicStarDis">...</p>
' id="comicStarDis">', '</p>')),
// 網頁中列的description比meta中的完整。
description : get_label(html.between(
// nokiacn.js, iqg365.js, 733dm.js
// <p id="comic-description">...</p>
' id="comic-description">', '</')) || get_label(html.between(
// 360taofu.js: <div id="workint" class="work-ov">
' id="workint"', '</div>').between('>'))
});
// console.log(work_data);
return work_data;
},
get_chapter_list : function(work_data, html, get_label) {
html = html.between('<div class="cy_plist', '</div>')
// mobile version: <div id="list">
// <ul class="Drama autoHeight" id="mh-chapter-list-ol-0">
// 88bag.js: <div id="list" >
|| html.between('<div id="list"', '</ul>');
// console.log(html);
/**
* <code>
76.js: <li><a target="_blank" href="/chuanyue/tangyinzaiyijie/351280.html"><p>杀手唐寅</p><i></i></a></li>
</code>
*/
var matched, PATTERN_chapter =
// matched: [ all, url, inner ]
/<li><a [^<>]*?href="([^<>"]+)"[^<>]*>([\s\S]+?)<\/li>/g;
work_data.chapter_list = [];
while (matched = PATTERN_chapter.exec(html)) {
var chapter_data = {
url : matched[1],
title : get_label(matched[2])
};
work_data.chapter_list.push(chapter_data);
}
// PC version, mobile version 共通
work_data.chapter_list.reverse();
// console.log(work_data.chapter_list);
},
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
// modify from mh160.js
// console.log(html);
var chapter_data = html.between('qTcms_S_m_murl_e="', '"');
// console.log(chapter_data);
if (chapter_data) {
// 對於非utf-8編碼之中文不能使用 atob()
// e.g., http://www.aikanmh.cn/xuanhuan/zhutianji/499631.html
chapter_data = base64_decode(chapter_data)
.split("$qingtiandy$");
}
if (!Array.isArray(chapter_data)) {
library_namespace.warn({
// gettext_config:{"id":"unable-to-parse-chapter-data-for-«$1»-§$2"}
T : [ '無法解析《%1》§%2 之章節資料!', work_data.title, chapter_NO ]
});
return;
}
// console.log(chapter_data);
// console.log(JSON.stringify(chapter_data));
// console.log(chapter_data.length);
// library_namespace.set_debug(6);
// e.g., http://m.88bag.net/rexue/zuomeigongyu/36279.html
// @see
// http://m.88bag.net/template/wap1/css/d7s/js/show.20170501.js?20190722091626
if (chapter_data.length === 1
&& /^(--|\+)https?:\/\//.test(chapter_data[0])) {
chapter_data = {
limited : chapter_data[0].startsWith('+') ? '对不起,该章节已经下架!!本站仅提供检索服务,请尊重作品版权'
: '请点击下方链接开始观看本期漫画:' + chapter_data[0].slice(2)
};
return chapter_data;
}
// 設定必要的屬性。
chapter_data = {
image_list : chapter_data.map(function(url) {
// 2019/10/20: 採用 base64_decode() 取代 atob() 後,
// aikanmh 不可再 encodeURI()。
// url = encodeURI(url);
// 获取当前图片 function f_qTcms_Pic_curUrl_realpic(v)
// http://www.xatxwh.com/template/skin1/css/d7s/js/show.20170501.js?20190117082944
// f_qTcms_Pic_curUrl() → f_qTcms_Pic_curUrl_realpic(v) @
// http://www.nokiacn.net/template/skin2/css/d7s/js/show.20170501.js?20180805095630
if (this.for_each_image) {
// 733dm.js
// for_each_image:function(url,parameters,base64_encode){return(url);}
url = this.for_each_image(url, {
qTcms_S_m_id : html
.between('qTcms_Pic_m_if="', '"'),
qTcms_S_p_id : html.between('qTcms_S_p_id="', '"')
}, base64_encode);
} else if (url.startsWith('/')) {
// e.g., nokiacn.js
var image_base_url = this.image_base_url;
if (!image_base_url && image_base_url !== '') {
// default: url = qTcms_m_weburl + url;
image_base_url = html.between('qTcms_m_weburl="',
'"');
}
url = image_base_url + url;
} else if (html.between('qTcms_Pic_m_if="', '"') !== "2") {
// e.g.,
// http://www.nokiacn.net/lianai/caozuo100/134257.html
url = url.replace(/\?/gi, "a1a1")
.replace(/&/gi, "b1b1").replace(/%/gi, "c1c1");
url = (this.qTcms_m_indexurl
// this.qTcms_m_indexurl: e.g., 517.js
|| html.between('qTcms_m_indexurl="', '"') || '/')
+ "statics/pic/?p="
+ escape(url)
+ "&picid="
+ html.between('qTcms_S_m_id="', '"')
+ "&m_httpurl="
+ escape(base64_decode(html.between(
'qTcms_S_m_mhttpurl="', '"')));
// Should get Status Code: 302 Found
}
return {
url : url
};
}, this)
};
// console.log(JSON.stringify(chapter_data));
return chapter_data;
}
};
// --------------------------------------------------------------------------------------------
// http://www.aikanmh.cn/template/skin1/css/d7s/js/show.20170501.js?20191014154954
function utf8_decode(str_data) {
var tmp_arr = [], i = 0, ac = 0, c1 = 0, c2 = 0, c3 = 0;
str_data += '';
while (i < str_data.length) {
c1 = str_data.charCodeAt(i);
if (c1 < 128) {
tmp_arr[ac++] = String.fromCharCode(c1);
i++;
} else if ((c1 > 191) && (c1 < 224)) {
c2 = str_data.charCodeAt(i + 1);
tmp_arr[ac++] = String.fromCharCode(((c1 & 31) << 6)
| (c2 & 63));
i += 2;
} else {
c2 = str_data.charCodeAt(i + 1);
c3 = str_data.charCodeAt(i + 2);
tmp_arr[ac++] = String.fromCharCode(((c1 & 15) << 12)
| ((c2 & 63) << 6) | (c3 & 63));
i += 3;
}
}
return tmp_arr.join('');
}
// 對於非utf-8編碼之中文不能使用 atob()
function base64_decode(data) {
var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, dec = "", tmp_arr = [];
if (!data) {
return data;
}
data += '';
do {
h1 = b64.indexOf(data.charAt(i++));
h2 = b64.indexOf(data.charAt(i++));
h3 = b64.indexOf(data.charAt(i++));
h4 = b64.indexOf(data.charAt(i++));
bits = h1 << 18 | h2 << 12 | h3 << 6 | h4;
o1 = bits >> 16 & 0xff;
o2 = bits >> 8 & 0xff;
o3 = bits & 0xff;
if (h3 == 64) {
tmp_arr[ac++] = String.fromCharCode(o1);
} else if (h4 == 64) {
tmp_arr[ac++] = String.fromCharCode(o1, o2);
} else {
tmp_arr[ac++] = String.fromCharCode(o1, o2, o3);
}
} while (i < data.length);
dec = tmp_arr.join('');
dec = utf8_decode(dec);
return dec;
}
// ------------------------------------------
function utf8_encode(argString) {
var string = (argString + '');
var utftext = "";
var start, end;
var stringl = 0;
start = end = 0;
stringl = string.length;
for (var n = 0; n < stringl; n++) {
var c1 = string.charCodeAt(n);
var enc = null;
if (c1 < 128) {
end++;
} else if (c1 > 127 && c1 < 2048) {
enc = String.fromCharCode((c1 >> 6) | 192)
+ String.fromCharCode((c1 & 63) | 128);
} else {
enc = String.fromCharCode((c1 >> 12) | 224)
+ String.fromCharCode(((c1 >> 6) & 63) | 128)
+ String.fromCharCode((c1 & 63) | 128);
}
if (enc !== null) {
if (end > start) {
utftext += string.substring(start, end);
}
utftext += enc;
start = end = n + 1;
}
}
if (end > start) {
utftext += string.substring(start, string.length);
}
return utftext;
}
// btoa()
function base64_encode(data) {
var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, enc = "", tmp_arr = [];
if (!data) {
return data;
}
data = utf8_encode(data + '');
do {
o1 = data.charCodeAt(i++);
o2 = data.charCodeAt(i++);
o3 = data.charCodeAt(i++);
bits = o1 << 16 | o2 << 8 | o3;
h1 = bits >> 18 & 0x3f;
h2 = bits >> 12 & 0x3f;
h3 = bits >> 6 & 0x3f;
h4 = bits & 0x3f;
tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3)
+ b64.charAt(h4);
} while (i < data.length);
enc = tmp_arr.join('');
switch (data.length % 3) {
case 1:
enc = enc.slice(0, -2) + '==';
break;
case 2:
enc = enc.slice(0, -1) + '=';
break;
}
return enc;
}
// --------------------------------------------------------------------------------------------
function new_qTcms2017_comics_crawler(configuration) {
var using_configuration = Object.clone(default_configuration);
if (configuration.using_web_search) {
Object.assign(using_configuration, {
search_URL : using_configuration.search_URL_web,
parse_search_result :
//
using_configuration.parse_search_result_web,
id_of_search_result : null,
title_of_search_result : null
});
} else if (configuration.is_mobile === undefined) {
using_configuration.is_mobile = configuration.base_URL
.includes('://m.');
if (using_configuration.is_mobile) {
Object.assign(using_configuration, {
search_URL : using_configuration.search_URL_mobile,
parse_search_result :
//
using_configuration.parse_search_result_mobile,
title_of_search_result :
//
using_configuration.title_of_search_result_mobile
});
}
}
// 每次呼叫皆創建一個新的實體。
return new library_namespace.work_crawler(Object.assign(
using_configuration, configuration));
}
return new_qTcms2017_comics_crawler;
}

View File

@@ -0,0 +1,160 @@
/**
* @name CeL module for downloading sequential comics.
*
* @fileoverview 本檔案包含了處理、批量下載 可預測圖片網址序列的漫畫 的工具。
*
* <code>
CeL.work_crawler.sequential(configuration).start(work_id);
</code>
*
* 本檔案為僅僅利用可預測的圖片網址序列去下載漫畫作品,不 fetch 作品與章節頁面的範例。
*
* @since 2019/6/17 21:5:52 模組化。
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/comic.en-US/mrblue.js
// https://github.com/kanasimi/work_crawler/blob/master/comic.en-US/bookcube.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.sequential',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
var default_configuration = {
one_by_one : true,
// 這類型網站必須靠偵測到錯誤時,轉到下一個章節來運作;因此當圖片下載錯誤時不能直接中斷跳出。
skip_error : true,
// 但是不保留損壞的檔案。
preserve_bad_image : false,
MAX_ERROR_RETRY : 2,
// base_URL : '',
// 規範 work id 的正規模式提取出引數中的作品id 以回傳。
extract_work_id : function(work_information) {
// e.g., "wt_HQ0005"
if (/^[a-z_\-\d]+$/i.test(work_information))
return work_information;
},
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
// 必須是圖片網址的起始部分。
return '' + work_id + '/';
},
skip_get_work_page : true,
// 解析出作品資料/作品詳情。
parse_work_data : function(html, get_label) {
// 先給一個空的初始化作品資料以便後續作業。
return Object.create(null);
},
// 解析出章節列表。
get_chapter_list : function(work_data, html, get_label) {
if (!Object.hasOwn(this, 'start_chapter_NO')
&& work_data.last_download.chapter > this.start_chapter_NO) {
// 未設定 .start_chapter_NO 且之前下載過,則接續上一次的下載。
this.start_chapter_NO = work_data.last_download.chapter;
}
if (!Array.isArray(work_data.chapter_list)) {
// 先給一個空的章節列表以便後續作業。
work_data.chapter_list = [];
}
// reuse work_data.chapter_list
while (work_data.chapter_list.length < this.start_chapter_NO) {
// 隨便墊入作品資料網址 給本次下載開始下載章節前所有未設定的章節資料,
// 這樣才能準確從 .start_chapter_NO 開始下載。後續章節網址會動態增加。
work_data.chapter_list.push(this.work_URL(work_data.id));
}
// console.log(work_data);
},
// 依照給定序列取得圖片網址。
get_image_url : function(work_data, chapter_NO, image_index) {
return this.work_URL(work_data.id) + chapter_NO + '/'
+ (image_index + 1) + '.jpg';
},
skip_get_chapter_page : true,
// 解析出章節資料。
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
// 設定必要的屬性。
var chapter_data = {
// 先給好本章節第一張圖片的網址。後續圖片網址會動態增加。
image_list : [ this.get_image_url(work_data, chapter_NO, 0) ]
};
// console.log(chapter_data);
return chapter_data;
},
// 設定動態改變章節中的圖片數量。
dynamical_count_images : true,
// 每個圖片下載結束都會執行一次。
after_get_image : function(image_list, work_data, chapter_NO) {
// console.log(image_list);
var latest_image_data = image_list[image_list.index];
// console.log(latest_image_data);
if (!latest_image_data.has_error) {
library_namespace.debug([ work_data.id + ': ', {
// gettext_config:{"id":"the-previous-image-in-this-chapter-was-successfully-downloaded.-download-the-next-image-in-this-chapter"}
T : '本章節上一張圖片下載成功。下載本章節下一幅圖片。'
} ], 3);
image_list.push(this.get_image_url(work_data, chapter_NO,
image_list.length));
return;
}
if (image_list.length === 1) {
library_namespace.debug([ work_data.id + ': ', {
// gettext_config:{"id":"the-first-image-failed-to-download.-ending-download-for-this-work"}
T : '第一張圖就下載失敗了。結束下載本作品。'
} ], 3);
return;
}
// CeL.debug(work_data.id + ': 本章節上一張圖片下載失敗。下載下一個章節的圖片。');
work_data.chapter_list.push(this.work_URL(work_data.id));
// 動態增加章節,必須手動增加章節數量。
work_data.chapter_count++;
}
};
// --------------------------------------------------------------------------------------------
function new_sequential_comics_crawler(configuration, callback, initializer) {
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
// 每次呼叫皆創建一個新的實體。
return new library_namespace.work_crawler(configuration);
}
return new_sequential_comics_crawler;
}

View File

@@ -0,0 +1,242 @@
/**
* @name CeL module for downloading Toomics comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載 Toomics 韓國漫畫 之 **非韓語版**(外語版本) 的工具。
*
* <code>
CeL.toomics(configuration).start(work_id);
</code>
*
* @since 2019/7/12 20:21:25 模組化。
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/toomics_sc.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.toomics',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
var default_configuration = {
// 所有的子檔案要修訂註解說明時應該都要順便更改在CeL.application.net.comic中Comic_site.prototype內的母comments並以其為主體。
// {Natural}MIN_LENGTH:最小容許圖案檔案大小 (bytes)。
// 對於有些圖片只有一條細橫桿的情況。
MIN_LENGTH : 200,
// 圖像檔案下載失敗處理方式:忽略/跳過圖像錯誤。當404圖像不存在、檔案過小或是被偵測出非圖像(如不具有EOI)時依舊強制儲存檔案。default:false
// skip_error : true,
// one_by_one : true,
base_URL : 'https://toomics.com/',
// LANG_PREFIX : '',
// 解析 作品名稱 → 作品id get_work()
search_URL : function(work_title) {
return [ this.LANG_PREFIX + '/webtoon/ajax_search', {
toonData : work_title,
offset : 0,
limit : 20
} ];
},
parse_search_result : function(html, get_label) {
html = html.between('<ul>', '</ul>');
// console.log(html);
function parser(token) {
// console.log(token);
return [ +token.match(/toon=(\d+)&/)[1],
/**
* <code>
<a href="/sc/webtoon/search/?toon=4866&return=%2Fsc%2Fwebtoon%2Fepisode%2Ftoon%2F4866">
<div class="search_box">
<p class="img"><img src="https://thumb-g.toomics.com/upload/thumbnail/20180629102024/2019_03_15_15526329389053.jpg" alt="郑主任为何这样"></p>
</code>
*/
token.between('alt="', '"') ];
}
return library_namespace.work_crawler
.extract_work_id_from_search_result_link(
/<li(?:[^<>]*)>([\s\S]+?)<\/li>/g, html, parser);
},
// 取得作品的章節資料。 get_work_data()
work_URL : '/webtoon/episode/toon/',
parse_work_data : function(html, get_label, extract_work_data) {
// console.log(html);
var text = html.between('<header class="ep-cover_ch"', '</header>');
var work_data = {
// 必要屬性:須配合網站平台更改。
title : get_label(text.between('<h1>', '</h1>')),
author : get_label(text.between(
/**
* <code>
<span class="writer">李玄敏 <span class="text-muted">|</span> Miyune</span>
</p>
</code>
*/
'<span class="writer">', '</p>')).replace(/ +\|/g, ','),
// 選擇性屬性:須配合網站平台更改。
description : get_label(text.between('<h2>', '</h2>')),
genre : get_label(
text.between('<span class="type">', '</span>'))
.replace(/\s{2,}/g, ' '),
// e.g., Updated every Friday
update_weekday : text.between('<span class="date">', '</span>')
};
extract_work_data(work_data, html);
// console.log(work_data);
return work_data;
},
get_chapter_list : function(work_data, html, get_label) {
html = html.between('<ol class="list-ep">', '</ol>');
var matched, PATTERN_chapter =
//
/<li><a href="([^<>"]+)"[^<>]*>([\s\S]+?)<\/li>/g;
work_data.chapter_list = [];
/**
* <code>
<a href="javascript:;" onclick="Webtoon.chkec(this);location.href='/en/webtoon/detail/code/97236/ep/0/toon/4630'" onkeypress="this.onclick"
data-e="NDYzMA==" data-c="OTcyMzY="
data-v="">
</code>
*/
html.each_between('<li', '</li>', function(token) {
var url = token.between("location.href='", "'");
if (!url) {
// limited
return;
}
work_data.chapter_list.push({
title : get_label(token.between('<div class="cell-num">',
'</div>')),
date : token.between('datetime="', '"'),
type : token
.between('<span class="coin-type1">', '</span>'),
thumb : token.between('data-original="', '"'),
rating : token.between('<span class="star-stat">',
'</span>'),
url : url
});
});
// console.log(work_data.chapter_list);
},
pre_parse_chapter_data
// 執行在解析章節資料 process_chapter_data() 之前的作業 (async)。
// 必須自行保證執行 callback(),不丟出異常、中斷。
: function(XMLHttp, work_data, callback, chapter_NO) {
if (this.verificated) {
callback()
return;
}
this.get_URL(this.LANG_PREFIX
// https://toomics.com/tc/age_verification
// 年齡認證 如需關閉安全模式請確認您已年滿18歲。
+ '/age_verification', function(XMLHttp) {
// console.log(XMLHttp);
this.get_URL(this.LANG_PREFIX
// https://toomics.com/sc/index/set_display/?display=A&return=/sc/webtoon/detail/code/55556/ep/1/toon/2509
+ '/index/set_display/?display=A&return=/' + this.LANG_PREFIX
+ '/help/notice_list', function(XMLHttp) {
// console.log(XMLHttp);
// console.log(this.get_URL_options);
this.verificated = true;
callback();
});
});
},
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
var chapter_data = work_data.chapter_list[chapter_NO - 1];
// console.log(chapter_data);
// console.log(html);
Object.assign(chapter_data, {
title : html.between('<div class="viewer-title">', '</div>')
/**
* <code>
<div class="viewer-title">
<a href="/en/webtoon/episode/toon/4630" title="List">Too Pretty<em>Episode 1</em></a>
</div>
</code>
*/
.between('<em>', '</em>'),
image_list : html
// 2019: '<main class="viewer-body">'
// 2020/8: '<main class="viewer-body viewer-body-scroll">'
.between('<main class="viewer-body', '</main>')
// 2019: .all_between('data-original="', '"')
// 2020/8: .all_between('data-src="', '"')
.all_between('data-src="', '"')
});
if (chapter_data.image_list.length === 0) {
// 改版?
// console.trace(html);
}
// console.log(chapter_data);
return chapter_data;
}
};
// --------------------------------------------------------------------------------------------
function new_toomics_comics_crawler(configuration) {
configuration = Object.assign(Object.create(null),
//
default_configuration, {
work_URL : configuration.LANG_PREFIX
+ default_configuration.work_URL
}, configuration);
// 每次呼叫皆創建一個新的實體。
var crawler = new library_namespace.work_crawler(configuration);
return crawler;
}
return new_toomics_comics_crawler;
}

View File

@@ -0,0 +1,217 @@
/**
* @name CeL module for downloading webtoon comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載 WEBTOON 韓國漫畫 的工具。
*
* <code>
CeL.webtoon(configuration).start(work_id);
</code>
*
* @see https://www.webtoons.com/ https://github.com/Fastcampus-WPS-9th/Webtoon
*
* @since 2018/7/27 18:16:19 模組化。
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/dongman.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.webtoon',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
// 2021/12/4 Error: unable to verify the first certificate
//
// https://stackoverflow.com/questions/20082893/unable-to-verify-leaf-signature
// for Error: unable to verify the first certificate
// code: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE'
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
var default_configuration = {
// one_by_one : true,
base_URL : 'https://www.webtoons.com/',
// 最小容許圖案檔案大小 (bytes)。
// 對於有些圖片只有一條細橫桿的情況。
// e.g., webtoon\167 吸血鬼也沒關係\0003 [第2話] 健忘症\167-3-036.png
MIN_LENGTH : 130,
// 解析 作品名稱 → 作品id get_work()
search_URL : function(work_title) {
// 預設方法(callback var API)
return 'https://ac.webtoons.com/ac?q='
+ (this.language_code ? this.language_code + '%11' : '')
+ encodeURIComponent(work_title)
+ '&q_enc=UTF-8&st=1&r_lt=0&r_format=json&r_enc=UTF-8&_callback=jQuery'
+ String(Math.floor(Math.random() * 1e10))
+ String(Math.floor(Math.random() * 1e10)) + '_'
+ Date.now() + '&_=' + Date.now();
},
parse_search_result : function(html) {
// console.log(html);
if (html.startsWith('{')) {
html = JSON.parse(html);
} else {
// for callback
html = eval('(' + html.between('(', {
tail : ')'
}) + ')');
}
var id_list = [], id_data = [];
html = html.items[0];
if (html) {
// assert: Array.isArray(html)
html.forEach(function(work_data) {
// console.log(work_data);
if (work_data[1][0] === 'TITLE') {
id_list.push(+work_data[3][0]);
id_data.push(work_data[0][0]);
}
});
}
return [ id_list, id_data ];
},
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
var matched = typeof work_id === 'string'
&& work_id.match(/^([a-z]+)_(\d+)$/);
if (matched) {
// e.g., 投稿新星專區作品
// https://www.webtoons.com/challenge/episodeList?titleNo=
return matched[1] + '/episodeList?titleNo=' + matched[2];
}
return 'episodeList?titleNo=' + work_id;
},
parse_work_data : function(html, get_label, extract_work_data) {
var matched = html
/**
* 咚漫 2018/10/27? 改版 <code>
<a data-buried-obj="1" data-sc-name="PC_detail-page_read-first-btn" href="//www.dongmanmanhua.cn/fantasy/zhexianlu/%E7%AC%AC%E9%9B%B6%E8%AF%9D-1/viewer?title_no=1307&episode_no=1" class="btn_type7 NPI=a:gofirst,g:zh_CN_zh-hans" id="_btnEpisode">阅读第一话<span class="ico_arr21"></span></a>
</code>
*/
.match(/<a [^<>]*?href="([^<>"]+)"[^<>]+id="_btnEpisode">/),
//
text = html.between('<div class="info">', '</div>'),
//
work_data = {
// 必要屬性:須配合網站平台更改。
title : get_label(text.between('<h1 class="subj">', '</h1>')
// https://www.webtoons.com/zh-hant/challenge/%E5%A6%82%E4%BD%A0%E6%89%80%E9%A1%98/list?title_no=166730
|| text.between('<h3 class="subj _challengeTitle">', '<')),
author : get_label(html.between(
// <meta property="com-linewebtoon:webtoon:author"
// content="A / B" />
':webtoon:author" content="', '"')),
// 選擇性屬性:須配合網站平台更改。
// 看第一集, 阅读第一话
chapter_1_url : matched[1],
status : [
get_label(text.between('<h2 class="genre ', '</h2>')
.between('>')),
// 更新頻率 update_frequency
get_label(html.between('<p class="day_info">', '</p>')) ],
description : get_label(html.between(
// ('<p class="summary">', '</p>')
'<meta name="twitter:description" content="', '"')),
last_update : get_label(html.between('<span class="date">',
'</span>'))
};
extract_work_data(work_data, html);
// console.log(work_data);
return work_data;
},
chapter_list_URL : function(work_id, work_data) {
// return url of the first chapter
return work_data.chapter_1_url;
},
get_chapter_list : function(work_data, html) {
// console.log(html);
var data = html.between('<div class="episode_lst">', '</ul>'), matched,
/**
* 咚漫 2018/10/27? 改版 <code>
<a data-sc-event-parameter="{ title_title:'谪仙录',titleNo:'1307',genre:FANTASY,subcategory_"0":DRAMAsubcategory_"1":FANTASY,picAuthor:泼克文化,wriAuthor:泼克文化,update_day:,serial_status:SERIES,reader_gender:男,episode_name:第零话 1,episodeNo:1,change_mode:'',is_read_complete:'',change_episode_direction:''}" data-sc-event-name="TitleReadChangeEpisode" data-buried-obj="1" data-sc-name="PC_read-page_image-episode-btn" href="//www.dongmanmanhua.cn/fantasy/zhexianlu/%E7%AC%AC%E9%9B%B6%E8%AF%9D-1/viewer?title_no=1307&episode_no=1" class="on N=a:vtw.llist,g:zh_CN_zh-hans">
// 穿越時空愚到你 https://www.webtoons.com/zh-hant/drama/2019foolsday/list?title_no=1562
<span class="subj">水下那一分鐘 ft. 奇奇怪怪 <夢境與真實></span>
</code>
*/
PATTERN_chapter = /<li[^<>]*>[\s\S]*?<a [^<>]*?href="([^"<>]+)"[^<>]*>[\s\S]*?<span class="subj">([\s\S]*?)<\/span>[\s\S]*?<\/li>/g;
work_data.chapter_list = [];
while (matched = PATTERN_chapter.exec(data)) {
var chapter_data = {
url : matched[1],
title : matched[2]
};
work_data.chapter_list.push(chapter_data);
}
// console.log(work_data.chapter_list);
},
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
var chapter_data = {
// 設定必要的屬性。
title : get_label(html.between(
'<h1 class="subj_episode" title="', '"')),
image_list : []
}, PATTERN_image = /<img [^<>]+?data-url="([^<>"]+)"/g, matched;
html = html.between('<div class="viewer_lst">',
'<div class="episode_area"');
while (matched = PATTERN_image.exec(html)) {
matched = new library_namespace.URI(matched[1]);
// 去掉?type=q70s的部分 畫質較好 q70是手機版 q90是電腦版
delete matched.search_params.type;
// 去除?x-oss-process=image/quality,q_90 可會有更高的畫質
delete matched.search_params['x-oss-process'];
chapter_data.image_list.push({
url : encodeURI(matched.toString())
});
}
return chapter_data;
}
};
// --------------------------------------------------------------------------------------------
function new_webtoon_comics_crawler(configuration) {
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
// 每次呼叫皆創建一個新的實體。
var crawler = new library_namespace.work_crawler(configuration);
return crawler;
}
return new_webtoon_comics_crawler;
}

View File

@@ -0,0 +1,304 @@
/**
* @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
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 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(work_data, html) {
// TODO: 對於單話,可能無目次。
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
});
}
},
// 取得每一個章節的各個影像內容資料。 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;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff