🏠 Home 

Greasy Fork is available in English.

NGA帖子导出EPUB

下载NGA帖子所有页面并导出为EPUB文件,每个帖子按顺序命名,并生成目录嵌入到EPUB中。

// ==UserScript==
// @name         NGA帖子导出EPUB
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  下载NGA帖子所有页面并导出为EPUB文件,每个帖子按顺序命名,并生成目录嵌入到EPUB中。
// @author       none
// @match        *://nga.178.com/read.php?tid=*
// @grant        none
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// ==/UserScript==
(function () {
'use strict';
let allPosts = []; // 保存所有抓取的帖子
let threadTitle = 'NGA帖子'; // 默认文件名
let currentPage = 0; // 当前抓取页数
let totalPages = 0; // 总页数
let progressElement = null; // 显示进度的元素
function getPostData(document) {
const posts = [];
const postElements = document.querySelectorAll('.postbox');
postElements.forEach((post, index) => {
const contentElement = post.querySelector('.postcontent');
const content = contentElement ? convertSpecialTags(contentElement.innerHTML) : '';
posts.push({ title: `Post ${allPosts.length + index + 1}`, content });
});
return posts;
}
function getThreadTitle(document) {
const titleElement = document.querySelector('#currentTopicName');
return titleElement ? titleElement.textContent.trim() : 'NGA帖子';
}
function convertSpecialTags(content) {
return content
.replace(/\[b\](.*?)\[\/b\]/gi, '<strong>$1</strong>') // 加粗
.replace(/\[color=(.*?)\](.*?)\[\/color\]/gi, '<span style="color:$1;">$2</span>') // 颜色
.replace(/\[size=(\d+%?)\](.*?)\[\/size\]/gi, '<span style="font-size:$1;">$2</span>') // 字号
.replace(/\[url=(.*?)\](.*?)\[\/url\]/gi, '<a href="$1">$2</a>') // 链接
.replace(/\[img\](.*?)\[\/img\]/gi, '') // 忽略图片
.replace(/<img[^>]+>/gi, '') // 移除 HTML 图片标签
.replace(/\[.*?\]/g, ''); // 移除其他未知标记
}
function updateProgress() {
if (progressElement) {
progressElement.textContent = `抓取进度: ${currentPage}/${totalPages || '?'}`;
}
}
async function fetchWithGBK(url) {
const response = await fetch(url);
const buffer = await response.arrayBuffer();
const decoder = new TextDecoder('gbk');
return decoder.decode(buffer);
}
async function fetchNextPage(url, resolve) {
try {
const html = await fetchWithGBK(url);
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
if (!threadTitle) {
threadTitle = getThreadTitle(doc);
}
if (totalPages === 0) {
const lastPageLink = doc.querySelector('#pagebtop a[title="最后页"]');
totalPages = lastPageLink ? parseInt(lastPageLink.textContent, 10) : 1;
}
allPosts = allPosts.concat(getPostData(doc));
currentPage++;
updateProgress();
const nextPageLink = doc.querySelector('#pagebtop a[title="下一页"]');
if (nextPageLink) {
const nextPageUrl = new URL(nextPageLink.href, window.location.origin).href;
fetchNextPage(nextPageUrl, resolve);
} else {
resolve();
}
} catch (err) {
console.error('抓取页面失败:', err);
resolve();
}
}
async function createEPUB(posts) {
const zip = new JSZip();
const tocItems = posts.map((post, index) => `
<navPoint id="post${index + 1}" playOrder="${index + 1}">
<navLabel>
<text>${post.title}</text>
</navLabel>
<content src="content.xhtml#post${index + 1}"/>
</navPoint>`).join('');
const tocNCX = `<?xml version="1.0" encoding="UTF-8"?>
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">
<head>
<meta name="dtb:uid" content="urn:uuid:nga-thread" />
<meta name="dtb:depth" content="1" />
<meta name="dtb:totalPageCount" content="0" />
<meta name="dtb:maxPageNumber" content="0" />
</head>
<docTitle>
<text>${threadTitle}</text>
</docTitle>
<navMap>
${tocItems}
</navMap>
</ncx>`;
const meta = `<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="book-id">
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
<dc:title>${threadTitle}</dc:title>
<dc:creator>Tampermonkey脚本</dc:creator>
<dc:language>zh</dc:language>
</metadata>
<manifest>
<item id="toc" href="toc.ncx" media-type="application/x-dtbncx+xml"/>
<item id="content" href="content.xhtml" media-type="application/xhtml+xml"/>
</manifest>
<spine toc="toc">
<itemref idref="content"/>
</spine>
</package>`;
const contentXHTML = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>${threadTitle}</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.5; }
.post { margin-bottom: 20px; }
</style>
</head>
<body>
${posts.map((post, index) => `
<div id="post${index + 1}" class="post">
<h2>${post.title}</h2>
<div>${post.content}</div>
</div>`).join('')}
</body>
</html>`;
zip.file('mimetype', 'application/epub+zip');
const metaFolder = zip.folder('META-INF');
metaFolder.file('container.xml', `<?xml version="1.0" encoding="UTF-8"?>
<container xmlns="urn:oasis:names:tc:opendocument:xmlns:container" version="1.0">
<rootfiles>
<rootfile full-path="content.opf" media-type="application/oebps-package+xml"/>
</rootfiles>
</container>`);
zip.file('toc.ncx', tocNCX);
zip.file('content.opf', meta);
zip.file('content.xhtml', contentXHTML);
zip.generateAsync({ type: 'blob' }).then(content => {
saveAs(content, `${threadTitle}.epub`);
});
}
function init() {
const button = document.createElement('button');
button.textContent = '抓取所有页面并导出为EPUB';
button.style.position = 'fixed';
button.style.bottom = '10px';
button.style.right = '10px';
button.style.zIndex = '9999';
progressElement = document.createElement('span');
progressElement.style.marginLeft = '10px';
progressElement.style.fontSize = '16px';
progressElement.style.color = '#333';
button.onclick = () => {
allPosts = [];
threadTitle = '';
currentPage = 0;
totalPages = 0;
updateProgress();
const currentUrl = window.location.href;
fetchNextPage(currentUrl, () => createEPUB(allPosts));
};
document.body.appendChild(button);
button.insertAdjacentElement('afterend', progressElement);
}
init();
})();