三五中文网
http://www.xkushu.org
ethereal-essence (13554) 16小时前 下载:321
小说
三五中文网 (xkushu.org) 小说书源,支持搜索/分类/详情/章节目录/正文;兼容 async 宿主 API(Legado Tauri 书源规范)
// @uuid 019e1211-35ec-7c83-b819-d6fc68076f4d
// @name 三五中文网
// @version 1.2.0
// @author Ethereal
// @url http://www.xkushu.org
// @logo http://www.xkushu.org/favicon.ico
// @type novel
// @enabled true
// @description 三五中文网 (xkushu.org) 小说书源,支持搜索/分类/详情/章节目录/正文;兼容 async 宿主 API(Legado Tauri 书源规范)
var BASE = 'http://www.xkushu.org';
/**
* Tauri 端多为同步返回字符串;鸿蒙等宿主可能返回 Promise,或对异常输入返回非字符串。
* 统一在此收敛为 string,避免后续 .replace / .split 报错。
*/
async function htmlDecodeSafe(str) {
if (str == null || str === '') {
return '';
}
var s = String(str);
if (!legado.htmlDecode) {
return s;
}
var r = legado.htmlDecode(s);
if (r != null && typeof r.then === 'function') {
r = await r;
}
return String(r == null ? '' : r);
}
var CATEGORIES = [
'玄幻奇幻',
'修真武侠',
'都市言情',
'历史军事',
'同人名著',
'游戏竞技',
'科幻灵异',
'耽美动漫',
];
/**
* 补全为绝对地址;二参 base 为书籍目录页 URL(以 / 结尾)时,用于相对章节如 1.html
* 不依赖全局 URL 构造器(部分 Legado/鸿蒙 运行时不提供 URL)
*/
function absUrl(href, base) {
if (!href) {
return '';
}
if (href.indexOf('http://') === 0 || href.indexOf('https://') === 0) {
return href;
}
if (href.indexOf('//') === 0) {
if (base && base.indexOf('https:') === 0) {
return 'https:' + href;
}
return 'http:' + href;
}
if (!base) {
if (href.charAt(0) === '/') {
return BASE + href;
}
return BASE + '/' + href.replace(/^\//, '');
}
if (base.indexOf('http://') !== 0 && base.indexOf('https://') !== 0) {
base = absUrl(base, null);
}
if (href.charAt(0) === '/') {
var hostM = base.match(/^(https?:\/\/[^/]+)/i);
if (hostM) {
return hostM[1] + href;
}
return BASE + href;
}
if (base.charAt(base.length - 1) !== '/') {
base = base + '/';
}
return base + href;
}
function normalizeIndexUrl(url) {
if (!url) {
return url;
}
var u = String(url).trim();
if (u.indexOf('http://') !== 0 && u.indexOf('https://') !== 0) {
u = absUrl(u);
}
if (u.charAt(u.length - 1) !== '/') {
u = u + '/';
}
return u;
}
function pickMeta(html, property) {
var r = new RegExp('property="' + property + '"\\s+content="([^"]*)"', 'i');
var m = html.match(r);
return m ? m[1] : '';
}
function pickTitle(html) {
var m = html.match(/<title>([^<]+)<\/title>/i);
return m ? m[1] : '';
}
function pickMetaName(html, name) {
var r = new RegExp('name="' + name + '"\\s+content="([^"]*)"', 'i');
var m = html.match(r);
return m ? m[1] : '';
}
/**
* /zwwinfo/{分卷}/{id}.htm 通常不带 og:novel:*,但正文表格与「内容简介」段仍有完整字段;
* 解析后可避免再 GET 体积很大的目录页 /35zwhtml/.../(详情页提速关键)。
*/
function parseZwwinfoDetail(html) {
var out = { name: '', author: '', kind: '', intro: '' };
if (!html) {
return out;
}
var h1m = html.match(/<a[^>]*href="[^"]*35zwhtml\/[^"]*"[^>]*>\s*<h1>([^<]+)<\/h1>\s*<\/a>/i);
if (!h1m) {
h1m = html.match(/<h1>([^<]+)<\/h1>/i);
}
if (h1m) {
out.name = h1m[1].replace(/^\s+|\s+$/g, '');
}
var km = html.match(/类(?: |\s)+别[::]\s*([^<]+)<\/td>/i);
if (km) {
out.kind = km[1].replace(/ /g, ' ').replace(/^\s+|\s+$/g, '');
}
var am = html.match(/作(?: |\s)+者[::][^<]*<a[^>]+>([^<]+)<\/a>/i);
if (!am) {
am = html.match(/作者[::][^<]*<a[^>]+>([^<]+)<\/a>/i);
}
if (am) {
out.author = am[1].replace(/^\s+|\s+$/g, '');
}
var im = html.match(/内容简介:<\/span>([\s\S]*?)<span\s+class="hottext">/i);
if (im) {
var raw = im[1];
raw = raw.replace(/<br\s*\/?>/gi, '\n');
raw = raw.replace(/<[^>]+>/g, '');
raw = raw.replace(/ /gi, ' ');
out.intro = raw.replace(/^\s+|\s+$/g, '');
}
return out;
}
/**
* 简介文本归一化:处理字面 \n/\r\n 并压缩空白行
*/
async function normalizeIntro(text) {
if (!text) {
return '';
}
var t = await htmlDecodeSafe(String(text));
t = t.replace(/\\r\\n/g, '\n');
t = t.replace(/\\n/g, '\n');
t = t.replace(/\r\n/g, '\n');
t = t.replace(/\r/g, '\n');
t = t.replace(/[ \t]+\n/g, '\n');
t = t.replace(/\n{3,}/g, '\n\n');
return t.replace(/^\s+|\s+$/g, '');
}
/** 由 /35zwhtml/分卷/书id/ 推断封面,与书站 og:image 规则一致 */
function coverUrlFrom35Path(frag) {
if (!frag) {
return '';
}
var bid = String(frag).match(/\/35zwhtml\/(\d+)\/(\d+)\//);
if (!bid) {
return '';
}
return absUrl(
'/35zwimage/' + bid[1] + '/' + bid[2] + '/' + bid[2] + 's.jpg',
);
}
/** 从书库/搜索结果的 grid 表格解析书籍列表 */
function parseGridBookRows(html) {
var re = new RegExp(
'<td class="odd"><a href="(/35zwhtml/\\d+/\\d+/)">([^<]+)</a></td>\\s*' +
'<td class="even">[\\s\\S]*?</td>\\s*' +
'<td class="odd">([^<]+)</td>',
'gi',
);
var list = [];
var m;
re.lastIndex = 0;
while (true) {
m = re.exec(html);
if (!m) {
break;
}
var path = m[1];
var bookUrl = absUrl(path);
var cov = coverUrlFrom35Path(path);
list.push({
name: m[2],
author: m[3].replace(/^\s+|\s+$/g, ''),
bookUrl: bookUrl,
coverUrl: cov,
cover: cov,
});
}
return list;
}
/**
* 搜索(POST 到 /modules/article/search.php)
* @param keyword
* @param page 页码,从 1 开始(站点若无分页则始终只有一页)
*/
async function search(keyword, page) {
legado.log('[三五中文网] search keyword=' + String(keyword) + ', page=' + String(page));
var p = page;
if (!p || p < 1) {
p = 1;
}
var body =
'searchtype=articlename&searchkey=' + encodeURIComponent(keyword) + '&page=' + encodeURIComponent(String(p));
var html = await legado.http.post(
BASE + '/modules/article/search.php',
body,
{ 'Content-Type': 'application/x-www-form-urlencoded' },
);
return parseGridBookRows(html);
}
/**
* 分类/发现 /zwwsort{1-8}/0/{page}.htm
* category === 'GETALL' 时须返回 string[] 分类名(与 Legado Tauri 发现页协议一致,见官方文档 discover)
* @param page 页码,从 1 开始
* @param category 分类名、数字 1-8,或 'GETALL'(仅取分类列表,不发请求)
*/
var SORT_ID_MAP = {
玄幻奇幻: 1,
修真武侠: 2,
都市言情: 3,
历史军事: 4,
同人名著: 5,
游戏竞技: 6,
科幻灵异: 7,
耽美动漫: 8,
};
async function explore(page, category) {
legado.log('[三五中文网] explore page=' + String(page) + ', category=' + String(category));
if (category === 'GETALL') {
return CATEGORIES.slice();
}
var pg = page;
if (!pg || pg < 1) {
pg = 1;
}
var sortId = 1;
if (category !== null && category !== undefined && String(category) !== '') {
var c = String(category);
if (SORT_ID_MAP[c] !== undefined) {
sortId = SORT_ID_MAP[c];
} else {
var n = parseInt(c, 10);
if (!isNaN(n) && n >= 1 && n <= 8) {
sortId = n;
} else {
return CATEGORIES.slice();
}
}
}
var url = BASE + '/zwwsort' + String(sortId) + '/0/' + String(pg) + '.htm';
var html = await legado.http.get(url);
return parseGridBookRows(html);
}
/**
* 书籍详情
* 为了加速:优先请求体积更小的 /zwwinfo/{cat}/{id}.htm,再回退到目录页 /35zwhtml/.../
* @param bookUrl 书籍页 URL,以 / 结尾
*/
async function bookInfo(bookUrl) {
legado.log('[三五中文网] bookInfo url=' + String(bookUrl));
if (
bookUrl &&
typeof bookUrl === 'object' &&
typeof bookUrl.length === 'number' &&
bookUrl.length > 0 &&
typeof bookUrl !== 'string'
) {
bookUrl = bookUrl[0];
}
var u = normalizeIndexUrl(bookUrl);
var bid = u.match(/\/35zwhtml\/(\d+)\/(\d+)\//);
var infoHtml = '';
if (bid) {
// 详情简介页比目录页小很多,优先用它
var infoUrl = BASE + '/zwwinfo/' + bid[1] + '/' + bid[2] + '.htm';
infoHtml = await legado.http.get(infoUrl);
}
// 优先从 infoHtml 取;不足再回退到 tocHtml
var name = '';
var author = '';
var kind = '';
var intro = '';
var cover = '';
if (infoHtml) {
var zw = parseZwwinfoDetail(infoHtml);
name = pickMeta(infoHtml, 'og:novel:book_name');
if (!name) {
name = zw.name;
}
if (!name) {
var t = pickTitle(infoHtml);
if (t) {
// e.g. 斗破苍穹最新章节-天蚕土豆-三五中文网(优先 h1,避免书名带「最新章节」)
name = t.split('-')[0];
}
}
author = pickMeta(infoHtml, 'og:novel:author');
if (!author) {
author = zw.author;
}
if (!author) {
var am = infoHtml.match(/作者[::][^<]*<a[^>]+>([^<]+)<\/a>/);
if (am) {
author = am[1];
}
}
kind = pickMeta(infoHtml, 'og:novel:category');
if (!kind) {
kind = zw.kind;
}
intro = pickMeta(infoHtml, 'og:description');
if (!intro) {
intro = zw.intro;
}
if (!intro) {
intro = pickMetaName(infoHtml, 'description');
}
intro = await normalizeIntro(intro);
}
// 封面可直接按路径推断(最快),否则再从页面 meta 取
if (bid) {
cover = absUrl('/35zwimage/' + bid[1] + '/' + bid[2] + '/' + bid[2] + 's.jpg');
}
if (!name || !author || !kind || !intro) {
// 回退读取目录页(较大,但字段最全)
var tocHtml = await legado.http.get(u);
if (!name) {
name = pickMeta(tocHtml, 'og:novel:book_name');
if (!name) {
var h1m = tocHtml.match(/<div id="title">\s*<h1>([^<]+)<\/h1>/i);
if (h1m) {
name = h1m[1].replace(/全文阅读$/, '');
} else {
name = pickTitle(tocHtml);
}
}
}
if (!author) {
author = pickMeta(tocHtml, 'og:novel:author');
if (!author) {
var am2 = tocHtml.match(/作者[::][^<]*<a[^>]+>([^<]+)<\/a>/);
if (am2) {
author = am2[1];
}
}
}
if (!kind) {
kind = pickMeta(tocHtml, 'og:novel:category');
}
if (!intro) {
intro = await normalizeIntro(pickMeta(tocHtml, 'og:description'));
}
if (!cover) {
cover = pickMeta(tocHtml, 'og:image');
if (cover) {
cover = absUrl(cover);
}
}
}
return {
name: name,
author: author,
kind: kind,
intro: intro,
bookUrl: u,
tocUrl: u,
coverUrl: cover,
};
}
/**
* 从 <a> 开标签属性里取 href、title(忽略顺序;兼容仅有正文无 title)
*/
function parseChapterAOpen(tagOpen) {
var hrefM = tagOpen.match(/\bhref\s*=\s*"([0-9]+\.html)"/i);
if (!hrefM) {
hrefM = tagOpen.match(/\bhref\s*=\s*'([0-9]+\.html)'/i);
}
if (!hrefM) {
return null;
}
var tM = tagOpen.match(/\btitle\s*=\s*"([^"]*)"/i);
if (!tM) {
tM = tagOpen.match(/\btitle\s*=\s*'([^']*)'/i);
}
return { href: hrefM[1], titleAttr: tM ? tM[1] : '' };
}
/**
* 章节目录(目录为书籍首页 #list dl dd a)
* @param tocUrl 与 bookInfo 的 tocUrl 相同
*/
async function chapterList(tocUrl) {
legado.log('[三五中文网] chapterList tocUrl=' + String(tocUrl));
var u = normalizeIndexUrl(tocUrl);
var html = await legado.http.get(u);
var i0 = html.indexOf('id="list"');
if (i0 < 0) {
return [];
}
var sub = html.substring(i0);
var iEnd = sub.indexOf('class="listend"');
if (iEnd > 0) {
sub = sub.substring(0, iEnd);
}
// 任意属性格式:<a href="1.html" ...> 与 <a title="x" href="1.html" ...> 均支持
var re = /<a(\s+[^>]+)>([^<]*)<\/a>/gi;
var seen = {};
var chapters = [];
var m;
re.lastIndex = 0;
while (true) {
m = re.exec(sub);
if (!m) {
break;
}
var p = parseChapterAOpen(' ' + m[1]);
if (!p) {
continue;
}
var full = absUrl(p.href, u);
if (seen[full]) {
continue;
}
seen[full] = 1;
var inner = m[2] ? m[2].replace(/^\s+|\s+$/g, '') : '';
var title = p.titleAttr ? p.titleAttr : inner;
title = await htmlDecodeSafe(title);
if (!title) {
title = p.href;
}
chapters.push({ name: title, title: title, url: full });
}
return chapters;
}
/**
* 从章节页只取 #content 内小说部分:在「广告 gg_read_content_up 的 </div>」与「#content 结束的 </div>」之间,不含 read_* 脚本
*/
function extractContentBodyHtml(pageHtml) {
var p0 = pageHtml.indexOf('id="content"');
if (p0 < 0) {
return '';
}
p0 = pageHtml.indexOf('>', p0) + 1;
var afterGg = p0;
if (pageHtml.indexOf('gg_read_content_up', p0) >= 0) {
var pGg = pageHtml.indexOf('gg_read_content_up', p0);
var closeGg = pageHtml.indexOf('</div>', pGg);
if (closeGg > 0) {
afterGg = closeGg + 6;
}
}
var close = pageHtml.indexOf('</div>', afterGg);
if (close < 0) {
return '';
}
var block = pageHtml.substring(afterGg, close);
block = block.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
return block;
}
/**
* 简单 HTML 片段转纯文本:br/换行,去标签
*/
async function htmlFragmentToText(fragment) {
if (!fragment) {
return '';
}
var t = fragment;
t = t.replace(/<br\s*\/?>\s*/gi, '\n');
t = t.replace(/<\/p>\s*/gi, '\n');
t = t.replace(/ /gi, ' ');
t = t.replace(/<[^>]+>/g, '');
if (legado.htmlDecode) {
t = await htmlDecodeSafe(t);
} else {
t = t.replace(/</g, '<');
t = t.replace(/>/g, '>');
t = t.replace(/&/g, '&');
t = t.replace(/"/g, '"');
}
t = t.replace(/[ \t]+\n/g, '\n');
t = t.replace(/\n{3,}/g, '\n\n');
return t;
}
/**
* 剔除 read_* 残留与括号包起来的「三五 + 全角/半角域名」水印
*/
function stripSiteWatermarks(t) {
if (!t) {
return '';
}
t = t.replace(/read_[a-z0-9_]*\s*\(\s*\)\s*;?/gi, '');
t = t.replace(/[((][\s\S]*?三五中文网[\s\S]*?[))]/g, '');
t = t.replace(/[wwwwWW]{1,3}[.\\.\s\n]*[33三][55五][zzZ][wwWW][wwWW][.\\.][\s\n]*[ccC][ooOO][mmMM][))\s]*/g, '');
t = t.replace(/www\.35zww\.com/gi, '');
t = t.replace(/35zww[..][comcom]/gi, '');
t = t.replace(/www[..][33][55][zz][ww]{2}/gi, '');
t = t.replace(/三五中文网/g, '');
t = t.replace(/[))]?\s*$/g, '');
t = t.replace(/^\s*[))((]\s*/g, '');
t = t.replace(/\n{3,}/g, '\n\n');
return t;
}
/**
* 章节正文(用 HTML 子串 + 去广告,避免 #content 内 read_* 脚本名进入正文)
* @param chapterUrl
*/
async function chapterContent(chapterUrl) {
legado.log('[三五中文网] chapterContent url=' + String(chapterUrl));
var html = await legado.http.get(chapterUrl);
var body = extractContentBodyHtml(html);
var text;
if (body) {
text = await htmlFragmentToText(body);
} else {
var doc = legado.dom.parse(html);
text = legado.dom.selectText(doc, '#content');
legado.dom.free(doc);
}
if (!text) {
return '';
}
text = stripSiteWatermarks(text);
return text;
}
async function TEST(type) {
if (type === '__list__') {
return ['search', 'explore'];
}
if (type === 'search') {
var r = await search('斗破苍穹', 1);
if (!r || r.length < 1) {
return { passed: false, message: '搜索无结果' };
}
return { passed: true, message: '搜索返回 ' + r.length + ' 条' };
}
if (type === 'explore') {
var b = await explore(1, '玄幻奇幻');
if (!b || b.length < 1) {
return { passed: false, message: '发现页为空' };
}
return { passed: true, message: '发现页 ' + b.length + ' 条' };
}
return { passed: false, message: '未知: ' + String(type) };
}