爱丽丝书屋
https://xn--vcsx64d.alicesw2.buzz
helloworld246 (13644) 05/17 21:56 下载:5276
小说 小说 爱丽丝书屋
爱丽丝书屋小说书源,支持搜索、详情、目录、正文、发现;正文严格删除所有空格。
// @uuid 019e15a3-c945-7819-8602-d6dc04136666
// @name 爱丽丝书屋
// @version 1.0.6
// @author hello world
// @url https://xn--vcsx64d.alicesw2.buzz
// @type novel
// @enabled true
// @tags 小说,爱丽丝书屋
// @description 爱丽丝书屋小说书源,支持搜索、详情、目录、正文、发现;正文严格删除所有空格。
var BASE = 'https://xn--vcsx64d.alicesw2.buzz';
var HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Referer': BASE + '/'
};
var CATS = [
['最新','/all/order/update_time+desc.html'],
['原创','/original.html'],
['科幻','/lists/71.html'],
['校园','/lists/61.html'],
['玄幻','/lists/62.html'],
['乡村','/lists/63.html'],
['都市','/lists/64.html'],
['历史','/lists/67.html'],
['武侠','/lists/68.html'],
['系统','/lists/69.html'],
['明星','/lists/72.html'],
['同人','/lists/73.html'],
['奇幻','/lists/75.html'],
['经典','/lists/79.html'],
['穿越','/lists/70.html'],
['纯爱','/lists/19.html'],
['百合','/lists/47.html'],
['其他','/lists/57.html']
];
function toAbs(url, baseUrl) {
if (url === null || url === undefined) return '';
url = String(url).replace(/&/g, '&').trim();
if (!url) return '';
if (url.indexOf('http://') === 0 || url.indexOf('https://') === 0) return url;
if (url.indexOf('//') === 0) return 'https:' + url;
var base = baseUrl || BASE;
if (url.charAt(0) === '/') {
var m = base.match(/^(https?:\/\/[^\/]+)/);
return (m ? m[1] : BASE) + url;
}
if (base.indexOf('?') >= 0) base = base.substring(0, base.indexOf('?'));
if (base.charAt(base.length - 1) !== '/') {
var p = base.lastIndexOf('/');
if (p > 8) base = base.substring(0, p + 1);
else base = base + '/';
}
return base + url;
}
function decodeHtml(s) {
if (!s) return '';
return String(s).replace(/ /g,' ').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,"'").replace(/'/g,"'").replace(/“/g,'“').replace(/”/g,'”').replace(/—/g,'—');
}
function cleanText(s) {
if (s === null || s === undefined) return '';
s = String(s).replace(/<script[\s\S]*?<\/script>/gi,'').replace(/<style[\s\S]*?<\/style>/gi,'').replace(/<[^>]+>/g,'');
s = decodeHtml(s).replace(/\u00a0/g,' ').replace(/[ \t\r\n]+/g,' ');
return s.trim();
}
function selectText(root, sel) {
try {
var el = legado.dom.select(root, sel);
if (!el) return '';
return cleanText(legado.dom.text(el));
} catch(e) { return ''; }
}
function selectAttr(root, sel, attr, baseUrl) {
try {
var el = legado.dom.select(root, sel);
if (!el) return '';
var v = legado.dom.attr(el, attr) || '';
if (attr === 'href' || attr === 'src' || attr === 'data-src') return toAbs(v, baseUrl || BASE);
return v;
} catch(e) { return ''; }
}
function firstMatch(s, reg) {
var m = String(s || '').match(reg);
return m ? cleanText(m[1]) : '';
}
function bookIdFromUrl(url) {
var m = String(url || '').match(/\/novel\/(\d+)\.html/);
if (m) return m[1];
m = String(url || '').match(/\/other\/chapters\/id\/(\d+)\.html/);
if (m) return m[1];
return '';
}
function pushBook(arr, seen, b) {
if (!b || !b.name || !b.bookUrl) return;
if (seen[b.bookUrl]) return;
seen[b.bookUrl] = 1;
arr.push(b);
}
function getPageUrl(path, page) {
page = page || 1;
var url = path;
if (page > 1) {
if (url.indexOf('?') >= 0) url = url + '&page=' + page;
else url = url.replace(/\.html$/, '') + '.html?page=' + page;
}
return toAbs(url, BASE);
}
function htmlToContent(block) {
if (!block) return '';
block = String(block);
block = block.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '');
block = block.replace(/<\s*br\s*\/?>/gi, '\n');
block = block.replace(/<\/(?:p|div|li|h[1-6])\s*>/gi, '\n\n');
block = block.replace(/<(?:p|div|li|h[1-6])\b[^>]*>/gi, '');
block = block.replace(/<[^>]+>/g, '');
block = decodeHtml(block);
return segmentText(block);
}
// ========== 正文处理:严格删除所有空格 ==========
function segmentText(text) {
if (text === null || text === undefined) return '';
text = String(text).replace(/\r/g, '\n');
// 彻底删除所有空格类字符(半角空格、全角空格、 、制表符、零宽空格等)
text = text
.replace(/\u00A0/g, '') //
.replace(/\u3000/g, '') // 全角空格
.replace(/\u200B/g, '') // 零宽空格(可选)
.replace(/[ \t\f\v]+/g, ''); // 删除半角空格和制表符
// 处理换行:将连续的换行符压缩为两个换行(段落分隔)
text = text.replace(/[ \t]*\n[ \t]*/g, '\n');
text = text.replace(/\n{3,}/g, '\n\n');
var raw = text.split(/\n+/);
var out = [];
for (var i = 0; i < raw.length; i++) {
var p = raw[i].replace(/^[ \s]+|[ \s]+$/g, '');
if (!p) continue;
addSegmentedParagraph(out, p);
}
return out.join('\n\n').replace(/\n{3,}/g, '\n\n').trim();
}
function addSegmentedParagraph(out, p) {
// 段落内部再次严格删除所有空格(保险)
p = p.replace(/\u00A0/g, '').replace(/\u3000/g, '').replace(/[ \t]+/g, '');
var max = 220;
while (p.length > max) {
var cut = -1;
var start = max - 80;
if (start < 40) start = 40;
var end = max + 60;
if (end > p.length) end = p.length;
for (var i = end; i >= start; i--) {
var ch = p.charAt(i);
if (ch === '。' || ch === '!' || ch === '?' || ch === ';' || ch === '…') {
cut = i + 1;
break;
}
}
if (cut < 0) {
for (var j = end; j >= start; j--) {
var ch2 = p.charAt(j);
if (ch2 === '”' || ch2 === '」' || ch2 === '』') {
cut = j + 1;
break;
}
}
}
if (cut < 0) cut = max;
var s = p.substring(0, cut).replace(/^[ \s]+|[ \s]+$/g, '');
if (s) out.push(s);
p = p.substring(cut).replace(/^[ \s]+|[ \s]+$/g, '');
}
if (p) out.push(p);
}
function regexChapterContent(html) {
var m = String(html || '').match(/<div[^>]+class=["'][^"']*(?:read-content|j_readContent)[^"']*["'][^>]*>([\s\S]*?)<\/div>/i);
if (m) return htmlToContent(m[1]);
m = String(html || '').match(/<div[^>]+id=["']?j_chapterBox["']?[^>]*>([\s\S]*?)<div[^>]+class=["'](?:admire-wrap|chapter-control|right-bar-list)/i);
if (m) return htmlToContent(m[1]);
return '';
}
function parseSearchBooks(html, pageUrl) {
var doc = legado.dom.parse(html);
var books = [];
var seen = {};
var items = legado.dom.selectAll(doc, '.list-group .list-group-item');
for (var i = 0; i < items.length; i++) {
var it = items[i];
var meta = selectText(it, 'p.mb-1');
var kind = selectText(it, 'p.text-muted').replace(/标签[::]?\s*/, '');
pushBook(books, seen, {
name: selectText(it, 'h5 a').replace(/^\d+\.\s*/, ''),
author: selectText(it, 'p.mb-1 a'),
bookUrl: selectAttr(it, 'h5 a', 'href', pageUrl),
intro: selectText(it, '.content-txt'),
kind: kind,
status: selectText(it, 'small'),
wordCount: firstMatch(meta, /字数[::]\s*([^\s ]+)/)
});
}
legado.dom.free(doc);
return books;
}
function parseListBooks(html, pageUrl) {
var doc = legado.dom.parse(html);
var books = [];
var seen = {};
var items;
var i;
items = legado.dom.selectAll(doc, 'ul.item-index li');
for (i = 0; i < items.length; i++) {
var it1 = items[i];
pushBook(books, seen, {
name: selectText(it1, 'a.ititle'),
author: selectText(it1, '.author'),
bookUrl: selectAttr(it1, 'a.ititle', 'href', pageUrl),
kind: selectText(it1, 'a.ctitle'),
latestChapter: selectText(it1, 'a.zjie'),
latestChapterUrl: selectAttr(it1, 'a.zjie', 'href', pageUrl),
updateTime: selectText(it1, '.time')
});
}
items = legado.dom.selectAll(doc, 'ul');
for (i = 0; i < items.length; i++) {
var it2 = items[i];
var name2 = selectText(it2, 'li.two a');
var url2 = selectAttr(it2, 'li.two a', 'href', pageUrl);
if (!url2 || url2.indexOf('/novel/') < 0) continue;
pushBook(books, seen, {
name: name2,
author: selectText(it2, 'li.four'),
bookUrl: url2,
kind: selectText(it2, 'li.sev'),
latestChapter: selectText(it2, 'li.three a'),
latestChapterUrl: selectAttr(it2, 'li.three a', 'href', pageUrl),
wordCount: selectText(it2, 'li.five'),
updateTime: selectText(it2, 'li.six')
});
}
items = legado.dom.selectAll(doc, '.class-img, .hot-img, .itemr');
for (i = 0; i < items.length; i++) {
var it3 = items[i];
var name3 = selectText(it3, 'h5 a');
if (!name3) name3 = selectText(it3, 'a.hottitle');
var url3 = selectAttr(it3, 'h5 a', 'href', pageUrl);
if (!url3) url3 = selectAttr(it3, 'a.hottitle', 'href', pageUrl);
if (!url3 || url3.indexOf('/novel/') < 0) continue;
var author3 = selectText(it3, '.authors');
if (!author3) author3 = firstMatch(legado.dom.text(it3), /作者[::]\s*([^\s]+)/);
author3 = author3.replace(/^作者[::]\s*/, '');
pushBook(books, seen, {
name: name3,
author: author3,
bookUrl: url3,
coverUrl: selectAttr(it3, 'img', 'data-src', pageUrl) || selectAttr(it3, 'img', 'src', pageUrl),
intro: selectText(it3, 'p')
});
}
legado.dom.free(doc);
return books;
}
async function search(keyword, page) {
legado.log('[爱丽丝书屋][search] keyword=' + keyword + ' page=' + page);
page = page || 1;
var url = BASE + '/search.html?q=' + encodeURIComponent(keyword) + '&f=_all';
if (page > 1) url = url + '&page=' + page;
var html = await legado.http.get(url, HEADERS);
var books = parseSearchBooks(html, url);
legado.log('[爱丽丝书屋][search] found=' + books.length);
return books;
}
async function bookInfo(bookUrl) {
legado.log('[爱丽丝书屋][bookInfo] url=' + bookUrl);
var html = await legado.http.get(bookUrl, HEADERS);
var doc = legado.dom.parse(html);
var id = bookIdFromUrl(bookUrl);
var name = selectText(doc, '.novel_title');
if (!name) name = firstMatch(html, /<title>([\s\S]*?)(?:-|_)/);
var infoText = selectText(doc, '.novel_info');
var author = selectText(doc, '.novel_info p a[href*="f=author"]');
if (!author) author = firstMatch(infoText, /作\s*者[::]\s*([^\s]+)/);
var kind = selectText(doc, '.bread-crumbs li:nth-child(2) a') || firstMatch(infoText, /分\s*类[::]\s*([^\s]+)/);
var wordCount = firstMatch(infoText, /字\s*数[::]\s*([^·\s]+)/);
var chapterCount = firstMatch(infoText, /章\s*节[::]\s*(\d+)/);
var status = firstMatch(infoText, /状\s*态[::]\s*([^\s]+)/);
var lastChapter = selectText(doc, '.novel_info p a[href*="/book/"]');
var lastChapterUrl = selectAttr(doc, '.novel_info p a[href*="/book/"]', 'href', bookUrl);
var tags = selectText(doc, '.tags_list').replace(/标签[::]?\s*/, '').replace(/注意[\s\S]*$/, '').trim();
if (tags) kind = kind ? kind + ' ' + tags : tags;
var intro = selectText(doc, '.jianjie p') || firstMatch(html, /<meta property="og:description" content="([^"]*)"/i);
var coverUrl = selectAttr(doc, '.pic img', 'data-src', bookUrl) || selectAttr(doc, '.pic img', 'src', bookUrl) || firstMatch(html, /<meta property="og:image" content="([^"]*)"/i);
legado.dom.free(doc);
return {
name: name,
author: author,
bookUrl: bookUrl,
coverUrl: coverUrl,
intro: intro,
kind: kind,
wordCount: wordCount,
chapterCount: chapterCount,
status: status,
lastChapter: lastChapter,
latestChapter: lastChapter,
latestChapterUrl: lastChapterUrl,
tocUrl: id ? BASE + '/other/chapters/id/' + id + '.html' : bookUrl
};
}
async function chapterList(tocUrl) {
legado.log('[爱丽丝书屋][chapterList] url=' + tocUrl);
var id = bookIdFromUrl(tocUrl);
if (id && tocUrl.indexOf('/other/chapters/') < 0) tocUrl = BASE + '/other/chapters/id/' + id + '.html';
var html = await legado.http.get(tocUrl, HEADERS);
var doc = legado.dom.parse(html);
var chapters = [];
var items = legado.dom.selectAll(doc, '.mulu_list li a');
if (!items || items.length === 0) items = legado.dom.selectAll(doc, '.book_newchap .con li a');
for (var i = 0; i < items.length; i++) {
var name = cleanText(legado.dom.text(items[i]));
var url = toAbs(legado.dom.attr(items[i], 'href') || '', tocUrl);
if (name && url) chapters.push({ name: name, url: url });
}
legado.dom.free(doc);
legado.log('[爱丽丝书屋][chapterList] count=' + chapters.length);
return chapters;
}
async function chapterContent(chapterUrl) {
legado.log('[爱丽丝书屋][chapterContent] url=' + chapterUrl);
var html = await legado.http.get(chapterUrl, HEADERS);
var content = regexChapterContent(html);
if (!content || content.length < 20) {
var doc = legado.dom.parse(html);
var box = legado.dom.select(doc, '.read-content.j_readContent');
if (!box) box = legado.dom.select(doc, '.read-content');
if (!box) box = legado.dom.select(doc, '.j_readContent');
if (box) {
var ps = legado.dom.selectAll(box, 'p');
var arr = [];
for (var i = 0; i < ps.length; i++) {
var t = cleanText(legado.dom.text(ps[i]));
if (t) addSegmentedParagraph(arr, t);
}
if (arr.length > 0) content = arr.join('\n\n');
else content = segmentText(legado.dom.text(box));
}
legado.dom.free(doc);
}
content = segmentText(content.replace(/爱丽丝书屋所有小说中出现的人物均为18岁以上的成人/g, ''));
legado.log('[爱丽丝书屋][chapterContent] len=' + content.length);
return content;
}
async function explore(page, category) {
legado.log('[爱丽丝书屋][explore] category=' + category + ' page=' + page);
page = page || 1;
if (!category || category === 'GETALL') {
var names = [];
for (var i = 0; i < CATS.length; i++) names.push(CATS[i][0]);
return names;
}
var path = '';
for (var j = 0; j < CATS.length; j++) {
if (CATS[j][0] === category) {
path = CATS[j][1];
break;
}
}
if (!path) return [];
var url = getPageUrl(path, page);
var html = await legado.http.get(url, HEADERS);
var books = parseListBooks(html, url);
legado.log('[爱丽丝书屋][explore] found=' + books.length);
return books;
}
async function TEST(type) {
if (type === '__list__') return ['explore','bookInfo','chapterList','chapterContent','search'];
if (type === 'explore') {
var e = await explore(1, '玄幻');
return { passed: e && e.length > 0, message: '发现返回 ' + (e ? e.length : 0) + ' 条' };
}
if (type === 'bookInfo') {
var b = await bookInfo(BASE + '/novel/50761.html');
return { passed: !!(b && b.name && b.tocUrl), message: '书名=' + (b ? b.name : '') };
}
if (type === 'chapterList') {
var c = await chapterList(BASE + '/other/chapters/id/50761.html');
return { passed: c && c.length > 0, message: '目录 ' + (c ? c.length : 0) + ' 章' };
}
if (type === 'chapterContent') {
var txt = await chapterContent(BASE + '/book/51995/3d547b821dac7.html');
return { passed: txt && txt.length > 100, message: '正文长度=' + (txt ? txt.length : 0) };
}
if (type === 'search') {
var s = await search('斗罗', 1);
return { passed: s && s.length > 0, message: '搜索返回 ' + (s ? s.length : 0) + ' 条' };
}
return { passed: false, message: '未知测试类型' };
}