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 }); })();