// ==UserScript== // @name 南理工教务处课程采集助手 V2 // @version 2.1 // @description 从南理工教务处(基于湖南强智科技开发,其他学校或许也可使用)的课程总库里爬取课程信息。支持自动翻页,导出 CSV,带暂停/停止,展示格式化数据表格,拼接完整大纲 URL // @author Light + ChatGPT // @match http://202.119.81.112:8080//Logon.do* // @license MIT // @supportURL https://github.com/NJUST-OpenLib/NJUST-JWC-Enhance // ==/UserScript== (function() { 'use strict'; Element.prototype.removeNode = function(deep) { if (deep && this.parentNode) { this.parentNode.removeChild(this); } else if (this.parentNode) { this.parentNode.removeChild(this); } return this; }; let allCourses = []; let isAutoRunning = false; let isPaused = false; let currentPage = 1; let isSortDesc = false; const panel = document.createElement('div'); panel.style.cssText = ` position: fixed; top: 60px; right: 20px; width: 360px; max-height: 600px; overflow-y: auto; background: rgba(255,255,255,0.97); border: 1px solid #aaa; padding: 12px; font-size: 14px; box-shadow: 0 0 12px rgba(0,0,0,0.2); border-radius: 8px; z-index:99999; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; `; panel.innerHTML = `

课程采集助手V2

URL: ${window.location.href} 触发了捕获规则

确保当前浏览器地址栏URL与其一致,如不一致,可能是iframe嵌入导致,请前往该页面进行处理!

请通过点击页面左上角齿轮图片(看不见请刷新),将“已显示字段”按次序设置为:
课程名称、学分、是否在用、课程编号、教学大纲是否录入、开课单位
设置完成后滚动小齿轮的窗口到底部,点击确定。
顺序必须一致!!
采集完成后,点击 导出CSV 以导出格式化数据,点击”打开转换页面“以转换为json。
注意:刷新页面会丢失已采集的数据

已采集课程数量:0
序号 课程名称 学分 是否在用 课程编号 教学大纲录入 开课单位 操作(课程大纲查看)
`; document.body.appendChild(panel); document.getElementById('btnOpenExternal').onclick = () => { window.open('https://enhance.njust.wiki/tools/csv2json.html', '_blank'); }; function parsePageCourses() { const rows = document.querySelectorAll('tbody tr.smartTr'); const courses = []; rows.forEach(tr => { const tds = tr.querySelectorAll('td'); if (tds.length < 9) return; const indexText = tds[1].innerText.trim(); let syllabusLink = ''; const ondblclick = tr.getAttribute('ondblclick') || ''; const match = ondblclick.match(/JsModck\('([^']+)'\)/); if(match){ let partialUrl = match[1]; if(partialUrl.startsWith('/')){ syllabusLink = location.origin + partialUrl; } else { syllabusLink = location.origin + '/' + partialUrl; } } courses.push({ index: indexText, courseName: tds[2].title || tds[2].innerText.trim(), credit: tds[3].title || tds[3].innerText.trim(), inUse: tds[4].title || tds[4].innerText.trim(), courseCode: tds[5].title || tds[5].innerText.trim(), syllabusEntered: tds[6].title || tds[6].innerText.trim(), department: tds[7].title || tds[7].innerText.trim(), syllabusText: tds[8].innerText.trim(), syllabusLink, }); }); return courses; } function addLog(message) { const logBox = document.getElementById('logBox'); const logEntry = document.createElement('div'); logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; logBox.appendChild(logEntry); logBox.scrollTop = logBox.scrollHeight; } function refreshTable(){ const tbody = document.getElementById('resultTbody'); tbody.innerHTML = ''; const coursesToDisplay = [...allCourses]; if (isSortDesc) { coursesToDisplay.reverse(); } coursesToDisplay.forEach(c => { const tr = document.createElement('tr'); const tdIndex = document.createElement('td'); tdIndex.textContent = c.index; const tdName = document.createElement('td'); tdName.textContent = c.courseName; const tdCredit = document.createElement('td'); tdCredit.textContent = c.credit; const tdInUse = document.createElement('td'); tdInUse.textContent = c.inUse; const tdCode = document.createElement('td'); tdCode.textContent = c.courseCode; const tdSyllabusEntered = document.createElement('td'); tdSyllabusEntered.textContent = c.syllabusEntered; const tdDept = document.createElement('td'); tdDept.textContent = c.department; const tdSyllabusOp = document.createElement('td'); if(c.syllabusLink){ const a = document.createElement('a'); a.href = c.syllabusLink; a.target = '_blank'; a.textContent = c.syllabusText || '查看'; tdSyllabusOp.appendChild(a); } else { tdSyllabusOp.textContent = c.syllabusText || '无'; } tr.appendChild(tdIndex); tr.appendChild(tdName); tr.appendChild(tdCredit); tr.appendChild(tdInUse); tr.appendChild(tdCode); tr.appendChild(tdSyllabusEntered); tr.appendChild(tdDept); tr.appendChild(tdSyllabusOp); tbody.appendChild(tr); }); document.getElementById('resultCount').textContent = `已采集课程数量:${allCourses.length} (第${currentPage}页)`; } function toCSV(arr){ const header = ['序号','课程名称','学分','是否在用','课程编号','教学大纲录入','开课单位','操作(课程大纲查看链接)']; const lines = arr.map(c => [ `"${c.index}"`, `"${c.courseName}"`, `"${c.credit}"`, `"${c.inUse}"`, `"${c.courseCode}"`, `"${c.syllabusEntered}"`, `"${c.department}"`, `"${c.syllabusLink}"` ].join(',')); return header.join(',') + '\n' + lines.join('\n'); } function exportCSV() { if (!Array.isArray(allCourses) || allCourses.length === 0) { alert('无课程数据可导出!'); return; } const csv = toCSV(allCourses); const blob = new Blob([csv], {type: 'text/csv;charset=utf-8'}); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `courses_${Date.now()}.csv`; document.body.appendChild(link); link.click(); document.body.removeChild(link); } function getNextPageButton(){ const btnImg = Array.from(document.querySelectorAll('img')).find(img=>/next/i.test(img.src)); if(btnImg && btnImg.parentElement && btnImg.parentElement.tagName.toLowerCase()==='a'){ return btnImg.parentElement; } return null; } async function autoCollect(){ isAutoRunning = true; isPaused = false; currentPage = 1; addLog(`开始采集第${currentPage}页...`); updateButtons(); while(isAutoRunning){ if(isPaused){ await new Promise(resolve => setTimeout(resolve, 300)); continue; } const pageCourses = parsePageCourses(); let minIndex = ""; let maxIndex = ""; if(pageCourses.length > 0) { minIndex = pageCourses[0].index; maxIndex = pageCourses[pageCourses.length-1].index; } let newCount = 0; pageCourses.forEach(c=>{ if(!allCourses.find(item=>item.index === c.index)){ allCourses.push(c); newCount++; } }); if(pageCourses.length > 0) { addLog(`第${currentPage}页采集完成,编号${minIndex}~${maxIndex},新增${newCount}条课程`); } else { addLog(`第${currentPage}页未找到课程数据`); } refreshTable(); const nextBtn = getNextPageButton(); if(!nextBtn){ addLog(`未找到下一页按钮,采集结束`); break; } if(nextBtn.classList.contains('disabled') || nextBtn.style.pointerEvents==='none' || /no\.gif/i.test(nextBtn.querySelector('img')?.src)){ addLog(`下一页按钮不可用,采集结束`); break; } addLog(`正在翻到第${currentPage+1}页...`); nextBtn.click(); currentPage++; await new Promise(resolve => setTimeout(resolve, 2200)); addLog(`开始采集第${currentPage}页...`); } isAutoRunning = false; updateButtons(); } function updateButtons(){ document.getElementById('btnStart').disabled = isAutoRunning && !isPaused; document.getElementById('btnPause').disabled = !isAutoRunning; document.getElementById('btnPause').textContent = isPaused ? '继续采集' : '暂停采集'; } document.getElementById('btnExtractPage').onclick = () => { const pageCourses = parsePageCourses(); let minIndex = ""; let maxIndex = ""; if(pageCourses.length > 0) { minIndex = pageCourses[0].index; maxIndex = pageCourses[pageCourses.length-1].index; } let newCount = 0; pageCourses.forEach(c=>{ if(!allCourses.find(item=>item.index === c.index)){ allCourses.push(c); newCount++; } }); if(pageCourses.length > 0) { addLog(`手动提取:编号${minIndex}~${maxIndex},新增${newCount}条课程`); } else { addLog(`当前页未找到课程数据`); } refreshTable(); }; document.getElementById('btnExportCSV').onclick = () => { exportCSV(); }; document.getElementById('btnStart').onclick = () => { if(isAutoRunning && isPaused){ isPaused = false; updateButtons(); return; } if(isAutoRunning) return; autoCollect(); }; document.getElementById('btnPause').onclick = () => { if(!isAutoRunning) return; isPaused = !isPaused; updateButtons(); }; document.getElementById('btnSortAsc').onclick = () => { isSortDesc = false; refreshTable(); }; document.getElementById('btnSortDesc').onclick = () => { isSortDesc = true; refreshTable(); }; document.getElementById('btnClose').onclick = () => { if(isAutoRunning && confirm('采集正在进行中,确定要关闭面板吗?')){ document.body.removeChild(panel); } else if(!isAutoRunning) { document.body.removeChild(panel); } }; })();