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