// ==UserScript== // @name CNKI 论文下载(参考文献智能提取 + 摘要格式化保存) // @namespace https://www.0x99.top // @version 0.9.5 // @description 在CNKI论文页面顶部中央添加三个按钮:保存正文、保存参考文献、保存摘要(作者.标题,来源,日期 + 摘要内容) // @author zmrbak // @match https://kns.cnki.net/reader/xml* // @grant none // ==/UserScript== (function() { 'use strict'; // ---------- 定义 XPath ---------- const CONTENT_XPATH = '/html/body/div[3]/div/div[2]/div[2]/div/div/div[3]/div'; const REF_XPATH = '/html/body/div[3]/div/div[2]/div[3]/section/div[2]/div[1]/ul'; const TITLE_XPATH = '/html/body/div[3]/div/div[2]/div[2]/div/div/div[3]/div/div[2]/div[1]/div[1]/h1'; const ABSTRACT_CONTAINER_XPATH = '/html/body/div[3]/div/div[2]/div[2]/div/div/div[3]/div/div[2]/div[1]'; const META_DIV_XPATH = '/html/body/div[3]/div/section/div/div[1]/div'; // 元数据区域(来源、日期) // 获取来源与日期字符串(格式:来源名称, 年份(期号)) function getSourceAndDateStr() { try { const metaDiv = document.evaluate(META_DIV_XPATH, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; if (!metaDiv) return ''; // 提取来源类型(如“期刊”、“博士论文”) let sourceType = ''; const typeSpan = metaDiv.querySelector('span'); if (typeSpan) { const match = typeSpan.innerText.match(/[((](.+?)[))]/); if (match) sourceType = match[1]; } // 提取来源名称(期刊名或学校名) let sourceName = ''; const nameLink = metaDiv.querySelector('a.xmlhref'); if (nameLink) { sourceName = nameLink.innerText.trim(); } else { // 如果没有链接(例如学位论文),尝试从 div 文本中提取学校名称(常见格式:在来源类型后的文本) const divText = metaDiv.innerText; const h1Title = metaDiv.querySelector('h1.xmltitle'); if (h1Title) { let rest = divText.replace(h1Title.innerText, ''); if (sourceType) { const regex = new RegExp(`[((]${sourceType}[))]\\s*([^\\d]+)`); const match = rest.match(regex); if (match && match[1]) sourceName = match[1].trim(); } } } // 提取年份和期号 const emList = metaDiv.querySelectorAll('em.xmlem'); let year = '', issue = ''; if (emList.length > 0) year = emList[0].innerText.trim(); if (emList.length > 1) issue = emList[1].innerText.trim(); // 构建最终来源+日期字符串 let result = ''; if (sourceName) { result = sourceName; } else if (sourceType) { result = sourceType; // 降级使用来源类型(如“博士论文”) } else { return ''; // 无可用信息 } if (year) { result += `,${year}`; if (issue) result += issue; // issue 已含括号,如“(05)” } return result; } catch (e) { console.warn('提取来源/日期失败:', e); return ''; } } // 解析单条参考文献 function parseReferenceItem(raw) { let text = raw.replace(/^\[\d+\]\s*/, ''); text = text.replace(/\s*\([A-Za-z\s,.;:()]+\)\s*$/, ''); const regex = /^(.+?)\.\s+(.+?)\[([A-Z]+)\]([^0-9]*)(\d{4})/; const match = text.match(regex); if (match) { let author = match[1].trim(); let title = match[2].trim(); let sourceType = match[3]; let sourceDetail = match[4].trim(); let year = match[5]; const sourcePart = sourceDetail ? `${sourceDetail}` : ''; return `${author}. ${title}. [${sourceType}]. ${sourcePart} ${year}.`.replace(/\s+/g, ' ').trim(); } return text.trim(); } function getReferenceText() { const result = document.evaluate(REF_XPATH, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); const ul = result.singleNodeValue; if (!ul) return ''; const liList = ul.querySelectorAll('li'); if (!liList.length) return ''; const refs = []; for (const li of liList) { const raw = li.innerText || li.textContent || ''; if (raw.trim()) refs.push(parseReferenceItem(raw)); } return refs.join('\n'); } function getTextFromXPath(xpath) { const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); const node = result.singleNodeValue; return node ? (node.innerText || node.textContent || '').trim() : ''; } // 获取格式化后的摘要(作者.标题 + 来源/日期 + 摘要单行) function getFormattedAbstract() { const container = document.evaluate(ABSTRACT_CONTAINER_XPATH, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; if (!container) return ''; // 1. 提取标题 let title = ''; const titleElem = container.querySelector('h1.Chapter'); if (titleElem) title = titleElem.innerText.trim(); if (!title) title = getTextFromXPath(TITLE_XPATH); if (!title) title = '未知标题'; // 2. 提取作者 let authors = []; const authorUl = container.querySelector('ul.Chapter-people'); if (authorUl) { const items = authorUl.querySelectorAll('li'); for (let li of items) { let name = ''; const link = li.querySelector('a'); if (link) { name = link.innerText.trim(); } else { name = li.innerText.trim(); } if (name && !name.includes('大学') && !name.includes('学院') && !name.includes('研究中心')) { authors.push(name); } } } if (authors.length === 0) { const authorElems = document.querySelectorAll('.author, .authors, span.author, a.author'); for (let elem of authorElems) { let name = (elem.innerText || elem.textContent).trim(); if (name && !name.includes('大学') && !name.includes('学院')) authors.push(name); } } authors = [...new Set(authors)]; let authorStr = authors.length ? authors.join(',') : '未知作者'; // 3. 提取来源与日期(新增) const sourceDateStr = getSourceAndDateStr(); // 4. 提取摘要文本 let abstractText = ''; const paras = container.querySelectorAll('.para'); let abstractPara = null; for (let para of paras) { const text = para.innerText || para.textContent || ''; if (text.includes('摘要:')) { abstractPara = para; break; } } if (abstractPara) { let fullText = abstractPara.innerText.trim(); fullText = fullText.replace(/^摘要[::]\s*/, ''); abstractText = fullText.replace(/\s+/g, ' ').trim(); } if (!abstractText) { const abstractElem = document.querySelector('.abstract, .abstract-text, [class*="abstract"]'); if (abstractElem) abstractText = abstractElem.innerText.trim(); } if (!abstractText) abstractText = '无摘要内容'; // 格式化输出 let firstLine = `${authorStr}. ${title}`; if (sourceDateStr) { firstLine += `,${sourceDateStr}`; } const secondLine = `摘要: ${abstractText}`; return `${firstLine}\n${secondLine}`; } function getBaseTitle() { let title = getTextFromXPath(TITLE_XPATH); if (!title) { const now = new Date(); title = `${now.getFullYear()}-${now.getMonth()+1}-${now.getDate()}_${now.getHours()}-${now.getMinutes()}-${now.getSeconds()}`; } title = title.replace(/[\\/:*?"<>|]/g, ''); if (title.length > 100) title = title.slice(0, 100); return title; } function saveAsTextFile(content, filename) { if (!content) { alert('未找到目标文本,可能页面结构已变化。'); return false; } const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); return true; } function showNotice(message, isError = false) { const notice = document.createElement('div'); notice.textContent = message; notice.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background: ${isError ? '#f44336' : '#4caf50'}; color: white; padding: 6px 12px; border-radius: 4px; font-size: 12px; z-index: 10001; opacity: 0.9; max-width: 350px; word-break: break-all; `; document.body.appendChild(notice); setTimeout(() => notice.remove(), 3000); } function createStyledButton(text, onClick) { const btn = document.createElement('button'); btn.textContent = text; btn.style.cssText = ` padding: 6px 16px; font-size: 14px; font-weight: bold; color: #fff; background-color: #2c7da0; border: none; border-radius: 30px; cursor: pointer; transition: background-color 0.2s, transform 0.1s; box-shadow: 0 1px 3px rgba(0,0,0,0.2); white-space: nowrap; font-family: inherit; `; btn.onmouseover = () => btn.style.backgroundColor = '#1f5e7a'; btn.onmouseout = () => btn.style.backgroundColor = '#2c7da0'; btn.addEventListener('click', onClick); return btn; } function addButtons() { const oldContainer = document.getElementById('cnki-buttons-container'); if (oldContainer) oldContainer.remove(); ['cnki-save-content-btn', 'cnki-save-ref-btn', 'cnki-save-abstract-btn'].forEach(id => { const oldBtn = document.getElementById(id); if (oldBtn) oldBtn.remove(); }); const container = document.createElement('div'); container.id = 'cnki-buttons-container'; container.style.cssText = ` position: fixed; top: 12px; left: 50%; transform: translateX(-50%); display: flex; gap: 12px; z-index: 10000; background: transparent; padding: 4px 0; box-shadow: none; `; const contentBtn = createStyledButton('📄 保存正文', () => { const content = getTextFromXPath(CONTENT_XPATH); const filename = `${getBaseTitle()}.txt`; const success = saveAsTextFile(content, filename); showNotice(success ? `✅ 正文已保存: ${filename}` : '⚠️ 未找到正文内容'); }); contentBtn.id = 'cnki-save-content-btn'; const refBtn = createStyledButton('📚 保存参考文献', () => { const refText = getReferenceText(); const filename = `${getBaseTitle()}_参考文献.txt`; const success = saveAsTextFile(refText, filename); showNotice(success ? `✅ 参考文献已保存: ${filename}` : '⚠️ 未找到参考文献'); }); refBtn.id = 'cnki-save-ref-btn'; const abstractBtn = createStyledButton('📝 保存摘要', () => { const abstractText = getFormattedAbstract(); const filename = `${getBaseTitle()}_摘要.txt`; const success = saveAsTextFile(abstractText, filename); showNotice(success ? `✅ 摘要已保存: ${filename}` : '⚠️ 未找到摘要内容'); }); abstractBtn.id = 'cnki-save-abstract-btn'; container.appendChild(contentBtn); container.appendChild(refBtn); container.appendChild(abstractBtn); document.body.appendChild(container); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', addButtons); } else { addButtons(); } })();