NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Qidian Chapter Downloader
// @name:zh-CN 起点章节下载器
// @namespace http://tampermonkey.net/
// @version 0.2
// @description Download chapter content from qidian
// @description:zh-CN 从起点下载章节文本
// @author oovz
// @match https://www.qidian.com/chapter/*
// @grant none
// @source https://gist.github.com/oovz/3257e1acd16ef2fa2913b430d95dc283
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Configure your XPath here
const TITLE_XPATH = '//div[contains(@class, "chapter-wrapper")]//div[contains(@class, "print")]//h1'; // Fill this with your XPath
const CONTENT_XPATH = '//div[contains(@class, "chapter-wrapper")]//div[contains(@class, "print")]//main/p'; // Base path to p elements
const CONTENT_SPAN_XPATH = '//div[contains(@class, "chapter-wrapper")]//div[contains(@class, "print")]//main/p/span'; // For p with span structure
// Create GUI elements
const gui = document.createElement('div');
gui.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
background: white;
padding: 15px;
border: 1px solid #ccc;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
z-index: 9999;
`;
const output = document.createElement('textarea');
output.style.width = '300px';
output.style.height = '150px';
output.style.marginBottom = '10px';
output.readOnly = true;
const copyButton = document.createElement('button');
copyButton.textContent = 'Copy Text';
copyButton.style.display = 'block';
// Add elements to GUI
gui.appendChild(output);
gui.appendChild(copyButton);
document.body.appendChild(gui);
// Extract text function
function getElementsByXpath(xpath) {
const results = [];
const query = document.evaluate(
xpath,
document,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null
);
for (let i = 0; i < query.snapshotLength; i++) {
const node = query.snapshotItem(i);
if (node) {
// Get only direct text content and exclude child elements
let directTextContent = '';
for (let j = 0; j < node.childNodes.length; j++) {
const childNode = node.childNodes[j];
if (childNode.nodeType === Node.TEXT_NODE) {
directTextContent += childNode.textContent;
}
}
// Only trim if it's the title (preserve indentation for content)
if (xpath === TITLE_XPATH) {
directTextContent = directTextContent.trim();
if (directTextContent) {
results.push(directTextContent);
}
} else {
// For content, preserve indentation
if (directTextContent) {
results.push(directTextContent);
}
}
}
}
return results;
}
// Initial extraction
function updateTitleOutput() {
const elements = getElementsByXpath(TITLE_XPATH);
return elements.join('\n');
}
function updateContentOutput() {
// Try to get content from spans first
let elements = getElementsByXpath(CONTENT_SPAN_XPATH);
// If no spans found, try direct p tags but filter out those with spans to avoid duplications
if (elements.length === 0) {
// First, get all p elements
const pElements = document.evaluate(
CONTENT_XPATH,
document,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null
);
for (let i = 0; i < pElements.snapshotLength; i++) {
const pNode = pElements.snapshotItem(i);
// Check if this p element has span children
const hasSpans = pNode.querySelectorAll('span').length > 0;
if (!hasSpans) {
// Only get text from p elements that don't have spans
elements.push(pNode.textContent.trim());
}
}
}
return elements.join('\n');
}
function updateOutput() {
const title = updateTitleOutput();
const content = updateContentOutput();
output.value = title ? title + '\n\n' + content : content;
}
// Run initial extraction
updateOutput();
// Add event listener for copy button
copyButton.addEventListener('click', () => {
output.select();
document.execCommand('copy');
copyButton.textContent = 'Copied!';
setTimeout(() => {
copyButton.textContent = 'Copy Text';
}, 1000);
});
// Update when DOM changes
const observer = new MutationObserver(updateOutput);
observer.observe(document.body, {
childList: true,
subtree: true
});
})();