// ==UserScript== // @name 学习通题目 万能提取器 // @namespace http://tampermonkey.net/ // @version 3.0.2 // @description UI全面优化,保留所有核心功能:新增导出docx功能。 // @author kkkxfr // @match *://*.chaoxing.com/exam-ans/exam/test/reVersionPaperMarkContentNew* // @match *://*.chaoxing.com/exam-ans/exam/test/look* // @match *://*.chaoxing.com/mooc-ans/work/selectWorkQuestionYiPiYue* // @match *://*.mooc1.chaoxing.com/mooc-ans/mooc2/work/view?* // @match *://*.chaoxing.com/work/doHomeWorkNew* // @match *://*.mooc-ans/work/doHomeWorkNew* // @match *://*.mooc1-api.chaoxing.com/mooc-ans/mooc2/work* // @match *://*.chaoxing.com/api/selectWorkQuestionYiPiYue* // @grant GM_setClipboard // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // ==/UserScript== (function(){ 'use strict'; const AUTO_EXTRACT_KEY = 'chaoxing_auto_extract_enabled'; /* ---------- 核心逻辑 Helpers (保持不变) ---------- */ function normalizeText(s){ if(!s) return ''; return String(s).replace(/\u00A0/g,' ').replace(/\r?\n/g,' ').replace(/\s+/g,' ').trim(); } function getCleanText(el){ if(!el) return ''; const c=el.cloneNode(true); c.querySelectorAll('script,style').forEach(x=>x.remove()); let html=c.innerHTML||''; html=html.replace(/
]*>/gi,'\n').replace(/
]*>/gi,'\n'); const tmp=document.createElement('div'); tmp.innerHTML=html; return normalizeText(tmp.textContent||tmp.innerText||''); }
function xmlEscape(s){ if(!s) return ''; return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); }
function stripLeadingLabels(s){
if(!s) return '';
let t = normalizeText(s);
while(/^(?:答案解析|正确答案|参考答案|答案)[::\s]*/i.test(t)){
t = t.replace(/^(?:答案解析|正确答案|参考答案|答案)[::\s]*/i, '');
}
return t.trim();
}
function pullLeadingScore(stem){
if(!stem) return {score:'', rest: ''};
const s = stem.trim();
const m = s.match(/^[,,]?\s*([0-9]+(?:\.[0-9]+)?)\s*分[\)\)]?\s*(.*)$/);
if(m){
return { score: m[1] + ' 分', rest: (m[2]||'').trim() };
}
return { score: '', rest: s };
}
/* ---------- 页面标题提取 ---------- */
function getPageTitle(){
const selectors=['#workTitle','.work_title','.courseName','.course-title','.mark_title','.title','h1','h2','.fanyaMarking_left.whiteBg','.detailsHead'];
for(const sel of selectors){
try{ const el=document.querySelector(sel); if(el){ const t=normalizeText(el.innerText||el.textContent||''); if(t && t.length>2 && !/error|404|登录|未找到/i.test(t)) return t.split(/\r?\n/)[0]; } }catch(e){}
}
const t=normalizeText(document.title||''); if(t && t.length>2) return t; return '试卷';
}
/* ---------- 题目解析逻辑 ---------- */
function findOptionContainers(qNode){
const sels = ['.mark_letter','.Cy_ulTop','.Zy_ulTop','.options','.option-list','.optionsList'];
const found = [];
for(const s of sels){
const el = qNode.querySelector(s);
if(el) found.push(el);
}
const uls = Array.from(qNode.querySelectorAll('ul,ol'));
uls.forEach(u=>{
if((u.className && /ul|options|Cy_ulTop|Zy_ulTop|mark_letter/.test(u.className)) || u.querySelector('li')) {
if(!found.includes(u)) found.push(u);
}
});
return found;
}
function extractCorrectAnswerFromNode(qNode){
if(!qNode) return null;
const selectors=['.rightAnswerContent','.right_answer','.answerRight','.Py_answer','.answer-content','.answer'];
const collected = [];
selectors.forEach(s=>{
qNode.querySelectorAll(s).forEach(el=>{
const t=getCleanText(el);
if(t && !/未提取|未作答|暂无/i.test(t)) collected.push(t.replace(/\s*[\r\n]+\s*/g,' '));
});
});
if(collected.length>0) return collected.join(' ');
const inline = Array.from(qNode.querySelectorAll('p,div,span,li,td'));
for(const el of inline){
const t=(el.textContent||'').trim();
if(!t) continue;
if(/(?:正确答案|参考答案|答案)[::]/.test(t) && !/我的答案/i.test(t)){
const m=t.match(/(?:正确答案|参考答案|答案)[::]?\s*([\s\S]+)/);
if(m && m[1]) return m[1].replace(/\s*[\r\n]+\s*/g,' ').trim();
}
}
const optContainers = findOptionContainers(qNode);
for(const cont of optContainers){
const liList = Array.from(cont.querySelectorAll('li'));
for(const li of liList){
const inner = li.innerText || '';
if(/√|正确|selected|checked|icon-ok|dui|check_answer_right/.test(inner)){
const label = (li.querySelector('.mark_letter_span')?.innerText || li.querySelector('i.fl')?.innerText || '').replace(/[^A-Za-z0-9\u4e00-\u9fa5]/g,'').trim();
if(label) return label;
return normalizeText(li.textContent || li.innerText || '');
}
}
}
const hidden = qNode.querySelector('.element-invisible-hidden, .Py_answer span.element-invisible-hidden');
if(hidden){
const t = normalizeText(hidden.innerText||hidden.textContent||'');
if(t) return t;
}
return null;
}
function normalizeAnswerString(s, qType){
if(!s) return '';
s = normalizeText(s);
const blanks = s.match(/(?:\(\s*\d+\s*\)|(\s*\d+\s*))[\s\S]*?(?=(?:\(\s*\d+\s*\)|(\s*\d+\s*)|$))/g);
if(blanks && blanks.length > 1) return blanks.map(b => normalizeText(b)).join(' ');
if(qType === '判断题'){
if(/^(对|正确|true)$/i.test(s)) return '对';
if(/^(错|错误|false)$/i.test(s)) return '错';
if(/对|正确|true/i.test(s)) return '对';
if(/错|错误|false/i.test(s)) return '错';
return s;
}
if(qType === '多选题'){
const letters = s.match(/[A-D]/gi);
if(letters){
return Array.from(new Set(letters.map(x=>x.toUpperCase()))).sort().join('');
}
return s;
}
let m = s.match(/\b([A-D])\b/i);
if(m && m[1]) return m[1].toUpperCase();
const patterns = [
/^\s*([A-Da-d])[\.\)\、\s]/,
/[\s\(\[]([A-Da-d])[\.\)\、\s]/,
/正确答案[::]?\s*([A-Da-d])/i,
/答案[::]?\s*([A-Da-d])/i
];
for(const p of patterns){
m = s.match(p);
if(m && m[1]) return m[1].toUpperCase();
}
const letters = s.match(/[A-D]/gi);
if(letters && letters.length>0){
return letters[0].toUpperCase();
}
return s;
}
function parseQuestionNode(node){
try{
const titleEl = node.querySelector('.mark_name, .Cy_TItle, .Qtitle, .question-title, .marking-title') || node;
let stemRaw = getCleanText(titleEl) || getCleanText(node);
let stem = normalizeText(stemRaw).replace(/^\d+[.\、]\s*/,'').trim();
stem = stem.replace(/^[\(\(]?(?:单选题|多选题|判断题|填空题)[\)\)]?\s*/,'');
stem = stem.replace(/[\s]*[\(\(](?:单选题|多选题|判断题|填空题)[\)\)][\s]*$/,'');
const wholeTxt = (node.innerText||node.textContent||'').toLowerCase();
let type = '单选题';
if(/多选|multiple|multi/i.test(wholeTxt)) type = '多选题';
else if(/判断|是非|对错|true|false/i.test(wholeTxt)) type = '判断题';
else if(/填空|空格|空/i.test(wholeTxt)) type = '填空题';
else {
const dt = node.getAttribute && (node.getAttribute('data-type') || node.getAttribute('qtype') || '');
if(dt && /multi|checkbox|多选/i.test(dt)) type = '多选题';
}
const options = [];
const containers = findOptionContainers(node);
if(containers.length > 0){
let chosen = containers.find(c => c.querySelector && c.querySelector('li'));
if(!chosen) chosen = containers[0];
if(chosen){
const lis = Array.from(chosen.querySelectorAll('li'));
if(lis.length === 0){
const children = Array.from(chosen.children).filter(ch => normalizeText(ch.innerText||'').length>0);
children.forEach((ch, idx) => {
let label = (ch.querySelector('.mark_letter_span')?.innerText || ch.querySelector('i.fl')?.innerText || '').replace(/[^A-Za-z0-9]/g,'').trim();
let clone = ch.cloneNode(true); if(clone.querySelector('.mark_letter_span')) clone.querySelector('.mark_letter_span').remove(); if(clone.querySelector('i.fl')) clone.querySelector('i.fl').remove();
let text = normalizeText(clone.textContent || clone.innerText || '');
const m = text.match(/^[\(\[]?\s*([A-Da-d])[\.\、\)\]\s]+/);
if(!label && m){ label = m[1].toUpperCase(); text = text.replace(/^[\(\[]?\s*([A-Da-d])[\.\、\)\]\s]+/,'').trim(); }
if(!label) label = String.fromCharCode(65 + (idx % 26));
options.push({ key: label.toUpperCase(), text: text.replace(/^[A-D][\.\、\s]*/i,'').trim() });
});
} else {
lis.forEach((li, idx)=>{
let label = (li.querySelector('.mark_letter_span')?.innerText || li.querySelector('i.fl')?.innerText || '').replace(/[^A-Za-z0-9]/g,'').trim();
let clone = li.cloneNode(true); if(clone.querySelector('.mark_letter_span')) clone.querySelector('.mark_letter_span').remove(); if(clone.querySelector('i.fl')) clone.querySelector('i.fl').remove();
let text = normalizeText(clone.textContent || clone.innerText || '');
const m = text.match(/^[\(\[]?\s*([A-Da-d])[\.\、\)\]\s]+/);
if(!label && m){ label = m[1].toUpperCase(); text = text.replace(/^[\(\[]?\s*([A-Da-d])[\.\、\)\]\s]+/,'').trim(); }
if(!label) label = String.fromCharCode(65 + (idx % 26));
options.push({ key: label.toUpperCase(), text: text.replace(/^[A-D][\.\、\s]*/i,'').trim() });
});
}
}
} else {
const immediateLis = Array.from(node.querySelectorAll(':scope > ul > li, :scope > ol > li'));
immediateLis.forEach((li, idx)=>{
let label = (li.querySelector('.mark_letter_span')?.innerText || li.querySelector('i.fl')?.innerText || '').replace(/[^A-Za-z0-9]/g,'').trim();
let clone = li.cloneNode(true); if(clone.querySelector('.mark_letter_span')) clone.querySelector('.mark_letter_span').remove(); if(clone.querySelector('i.fl')) clone.querySelector('i.fl').remove();
let text = normalizeText(clone.textContent || clone.innerText || '');
const m = text.match(/^[\(\[]?\s*([A-Da-d])[\.\、\)\]\s]+/);
if(!label && m){ label = m[1].toUpperCase(); text = text.replace(/^[\(\[]?\s*([A-Da-d])[\.\、\)\]\s]+/,'').trim(); }
if(!label) label = String.fromCharCode(65 + (idx % 26));
options.push({ key: label.toUpperCase(), text: text.replace(/^[A-D][\.\、\s]*/i,'').trim() });
});
}
let answerRaw = extractCorrectAnswerFromNode(node) || '';
answerRaw = stripLeadingLabels(answerRaw);
let answer = normalizeAnswerString(answerRaw || '', type);
const analysisEl = node.querySelector('.analysisDiv, .analysis, .py_analyse, .Py_addpy .pingyu, .explain, .analysisTxt');
let analysis = analysisEl ? getCleanText(analysisEl) : '';
analysis = stripLeadingLabels(analysis);
const sectionTitle = findPrevSectionTitle(node) || '';
return { type, stem, options, answer, analysis, sectionTitle };
} catch(e){
console.error('parseQuestionNode error', e);
return null;
}
}
function findPrevSectionTitle(node){
const headerSels=['.type_tit','.Cy_TItle1','h2','.markTitle','.typeTitle','.mark_section','.section-title'];
let cur=node;
for(let i=0;i<15;i++){
if(!cur) break;
let ps=cur.previousElementSibling;
for(let k=0;k<30 && ps;k++){
for(const sel of headerSels) {
if(ps.matches && ps.matches(sel)) {
const t = normalizeText(ps.innerText||ps.textContent||'');
if(t && !/^\d+[.、]/.test(t) && !/(\d+.*分)/.test(t) && !/\(\d+.*分\)/.test(t)) {
return t;
}
}
}
ps=ps.previousElementSibling;
}
cur=cur.parentElement;
}
const headers = Array.from(document.querySelectorAll('h1,h2,h3,.type_tit,.Cy_TItle1,.markTitle'));
let candidate = null;
for(const h of headers){
if(h.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING) break;
const t = normalizeText(h.innerText||h.textContent||'');
if(t && !/^\d+[.、]/.test(t)) candidate = t;
}
if(candidate) return candidate;
return '';
}
function buildStructuredPaperFromDOM(){
const root = document.querySelector('.fanyaMarking') || document.querySelector('.mark_table') || document.body;
const selectors = ['.questionLi','.TiMu','.question-item','.mark_question','.exam-question','.paper-question','.Ques','.questionBox'];
const nodeSet = new Set();
selectors.forEach(sel=>{
document.querySelectorAll(sel).forEach(n=>{
if(n && root.contains(n)) nodeSet.add(n);
});
});
if(nodeSet.size === 0){
const candidates = Array.from(root.querySelectorAll('*'));
candidates.forEach(n=>{
try{
if(!(n instanceof HTMLElement)) return;
const textLen = (n.innerText||n.textContent||'').trim().length;
const hasStem = !!n.querySelector('.mark_name, .Cy_TItle, .question-title, .Qtitle');
const hasOptions = !!n.querySelector('.mark_letter, .Cy_ulTop, .Zy_ulTop, .options, ul, ol');
if(hasStem || (textLen>12 && hasOptions)) nodeSet.add(n);
}catch(e){}
});
}
const arr = Array.from(nodeSet);
arr.sort((a,b)=> {
if(a===b) return 0;
const pos = a.compareDocumentPosition(b);
if(pos & Node.DOCUMENT_POSITION_PRECEDING) return 1;
if(pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
return 0;
});
const parsedQuestions = [];
const seenFp = new Set();
for(const node of arr){
const q = parseQuestionNode(node);
if(!q) continue;
const fp = (q.stem+'||'+ q.options.map(o=>o.text).slice(0,8).join('||')).slice(0,2000);
if(seenFp.has(fp)) continue;
seenFp.add(fp);
parsedQuestions.push(q);
}
const sectionsMap = new Map();
for(const q of parsedQuestions){
const key = q.sectionTitle && q.sectionTitle.trim() ? q.sectionTitle.trim() : '未分组';
if(!sectionsMap.has(key)) sectionsMap.set(key, []);
sectionsMap.get(key).push(q);
}
const pageTitle = getPageTitle();
if(sectionsMap.size === 1 && sectionsMap.has('未分组')){
const allQs = parsedQuestions.slice();
sectionsMap.clear();
sectionsMap.set(pageTitle, allQs);
} else {
if(sectionsMap.size > 1){
const headersTop = Array.from(document.querySelectorAll('h1,h2,.detailsHead,.fanyaMarking_left')).filter(h => {
try { return h.getBoundingClientRect && h.getBoundingClientRect().top < 240; } catch(e) { return false; }
});
if(headersTop.length === 1){
const merged = [];
for(const arrQ of sectionsMap.values()) merged.push(...arrQ);
sectionsMap.clear();
sectionsMap.set(normalizeText(headersTop[0].innerText||headersTop[0].textContent||pageTitle), merged);
}
}
}
const sections = [];
for(const [title, qlist] of sectionsMap.entries()){
let orderIndex = Infinity;
const headers = Array.from(document.querySelectorAll('h1,h2,h3,.type_tit,.Cy_TItle1,.markTitle'));
headers.forEach((h, idx) => { if(normalizeText(h.innerText||h.textContent||'') === normalizeText(title) && orderIndex===Infinity) orderIndex = idx; });
sections.push({ title, qlist, orderIndex });
}
sections.sort((a,b) => {
if(a.orderIndex === Infinity && b.orderIndex===Infinity) return 0;
if(a.orderIndex === Infinity) return 1;
if(b.orderIndex === Infinity) return -1;
return a.orderIndex - b.orderIndex;
});
let counter = 1;
const paper = { title: pageTitle, sections: [] };
for(const s of sections){
const sec = { title: s.title, questions: [] };
for(const q of s.qlist){
q.no = counter++;
sec.questions.push(q);
}
paper.sections.push(sec);
}
if(paper.sections.length === 0){
const fallbackNodes = Array.from(document.querySelectorAll('.TiMu,.questionLi'));
if(fallbackNodes.length>0){
const sec = { title: pageTitle || '题目', questions: [] };
for(const node of fallbackNodes){
const q = parseQuestionNode(node);
if(q){ q.no = counter++; sec.questions.push(q); }
}
if(sec.questions.length) paper.sections.push(sec);
}
}
return paper;
}
/* ---------- Word 生成 (无需变动逻辑) ---------- */
function buildContentTypes(){ return '