电子科大成都学院自动化成绩查询

1364 字
7 分钟
电子科大成都学院自动化成绩查询

背景简介#

以前写过的一个针对电子科技大学成都学院教务系统的自动查分脚本。 它能够自动登录教务系统、调用打码平台解决图形验证码、抓取成绩列表,并与本地缓存的成绩进行比对。一旦发现有新的成绩更新,就会通过 IYUU 接口自动发送微信通知。

技术栈说明#

该脚本整合了多个强大的 Node.js 库与第三方服务:

  • Playwright: 负责驱动无头浏览器(Chromium)进行模拟登录、节点交互及屏幕截图。
  • 2Captcha: 第三方打码平台,用于识别教务系统登录页的图形验证码。
  • Axios & Cheerio: 登录成功获取到 Cookie 后,使用 Axios 高效请求成绩页面,并用 Cheerio (类似于 jQuery) 快速解析 HTML 结构提取成绩表格。
  • Redis (ioredis): 用于持久化保存当前有效的 Cookie,以及上一次抓取到的成绩数据。
  • IYUU: 微信消息推送服务,用于将成绩变化通知实时推送到手机。

完整代码实现#

import axios from 'axios'
import Redis from 'ioredis'
import qs from 'qs'
import fs from 'fs'
import path from 'path';
import { chromium } from 'playwright'
import * as cheerio from 'cheerio';
const redis = new Redis({
host: '127.0.0.1',
port: 6379,
password: '1234567',
db: 0
});
redis.on('connect', () => {
});
async function runBrowser() {
const browser = await chromium.launch({
headless: true,
executablePath: '/usr/bin/chromium',
args: [
'--no-sandbox',
'--disable-gpu',
]
});
const page = await browser.newPage();
try {
await page.goto('http://www.cduestc.cn/eams/loginExt.action');
await page.fill('#username', process.env.kc_user);
await page.fill('#password', process.env.ke_password);
const captchaElement = await page.locator('img[src="/eams/captcha/image.action"]');
const captchaImage = await captchaElement.screenshot();
const captchaImageBase64 = captchaImage.toString('base64');
// 请求 2captcha 平台进行打码
const uploadResponse = await axios.post('https://2captcha.com/in.php', {
key: process.env.captcha_key,
method: 'base64',
body: captchaImageBase64,
json: 1,
type: 'ImageToTextTask',
});
if (uploadResponse.data.status !== 1) {
throw new Error(`验证码上传失败: ${uploadResponse.data.request}`);
}
const captchaID = uploadResponse.data.request;
let result = '';
// 轮询获取识别结果
while (true) {
const getResultResponse = await axios.get(`https://2captcha.com/res.php?key=${process.env.captcha_key}&action=get&id=${captchaID}&json=1`);
if (getResultResponse.data.status === 1) {
result = getResultResponse.data.request;
break;
} else if (getResultResponse.data.request === 'ERROR_CAPTCHA_UNSOLVABLE') {
throw new Error('验证码无法识别');
}
await new Promise(resolve => setTimeout(resolve, 2000));
}
console.log('识别验证码', result);
await page.type('#captcha_response', result);
await page.keyboard.press('Enter');
await page.waitForTimeout(2000);
// 处理登录错误情况
const pageSource = await page.content();
const errorMessageRegex = /<span>(.*?)<\/span>/;
const match = pageSource.match(errorMessageRegex);
if (match && match[1]) {
const errorText = match[1];
if (errorText == '验证码不正确') {
console.log(`错误消息: ${errorText}`);
await browser.close();
return runBrowser();
} else if (errorText == '账户不存在' || errorText == '密码错误') {
console.log(`错误消息: ${errorText}`);
await browser.close();
process.exit();
}
}
// 登录成功,保存 Cookie
const cookies = await page.context().cookies();
const cookieW = cookies.map(cookie => `${cookie.name}=${cookie.value}`).join('; ');
await redis.set('kc_cookie', cookieW);
console.log(`Cookie: ${cookieW}`);
await browser.close();
return cjcx();
} catch (error) {
console.error('登录时发生错误:', error);
} finally {
await browser.close();
}
}
async function cjcx() {
try {
const cookie = await redis.get('kc_cookie');
const cjcx = axios.create({
headers: {
'Cookie': cookie,
},
});
// 请求成绩页面
const response = await cjcx.get('http://www.cduestc.cn/eams/teach/grade/course/person!search.action?semesterId=138&projectType=&_=' + (new Date().getTime()));
const pageContent = response.data;
// 如果 Cookie 失效,重新执行登录流程
if (pageContent.includes('<title>上海树维信息科技有限公司教学管理系统</title>')) {
console.log('cookie失效')
return runBrowser();
}
const $ = cheerio.load(response.data);
const extractedData: any[] = [];
$('tbody tr').each((index, element) => {
const row = $(element);
const 学年学期 = row.find('td:nth-child(1)').text().trim();
const 课程代码 = row.find('td:nth-child(2)').text().trim();
const 课程类别 = row.find('td:nth-child(5)').text().trim();
const 学分 = row.find('td:nth-child(6)').text().trim();
const 课程名称 = row.find('td:nth-child(4)').text().trim();
const 总评成绩 = row.find('td:nth-child(8)').text().trim();
const 最终成绩 = row.find('td:nth-child(9)').text().trim();
const courseData = { 学年学期, 课程代码, 课程类别, 学分, 课程名称, 总评成绩, 最终成绩 };
extractedData.push(courseData);
});
// 对比本地 Redis 成绩缓存
const storedData = await redis.get('kc_cj');
let storedCourses = storedData ? JSON.parse(storedData) : [];
let dataChanged = false;
const newStoredCourses = [];
let changeMessage = '';
for (let i = 0; i < extractedData.length; i++) {
const course = extractedData[i];
const storedCourse = storedCourses.find((storedCourse: any) => storedCourse.课程代码 === course.课程代码);
if (!storedCourse || storedCourse.总评成绩 !== course.总评成绩) {
console.log(`课程 ${course.课程名称} 的成绩发生变化`);
dataChanged = true;
changeMessage += `课程名称: ${course.课程名称}\n成绩: ${course.总评成绩}\n\n`;
const index = storedCourses.findIndex((storedCourse: any) => storedCourse.课程代码 === course.课程代码);
if (index !== -1) {
storedCourses[index] = course;
} else {
storedCourses.push(course);
}
}
newStoredCourses.push(course.课程代码);
}
for (let i = 0; i < storedCourses.length; i++) {
const storedCourse = storedCourses[i];
if (!newStoredCourses.includes(storedCourse.课程代码)) {
dataChanged = true;
storedCourses.splice(i, 1);
i--;
}
}
if (dataChanged) {
const currentData = JSON.stringify(storedCourses);
await redis.set('kc_cj', currentData);
// IYUU 微信推送 API
await axios.post('https://iyuu.cn/xxxxxxx.send', null, {
params: {
text: '成绩更新通知',
desp: changeMessage
}
}).then(response => {
console.log('推送成功:', response.data);
}).catch(error => {
console.error('推送失败:', error);
});
} else {
console.log('成绩没有变化');
}
} catch (error) {
console.error('发生错误:', error);
}
}
async function main() {
await cjcx();
process.exit();
}
main();

核心逻辑解析#

  1. 自动登录与验证码处理 (runBrowser)

    • 使用 Playwright 启动无头浏览器并导航至登录页。
    • 填入账密后,精确定位验证码图片,调用 .screenshot() 将其截取并转换为 Base64。
    • 向 2Captcha 平台提交图片并持续轮询等待识别结果。
    • 拿到验证码后自动填入并回车,处理可能出现的登录异常(验证码错误进行递归重试,密码错误直接终止程序)。
    • 成功进入教务系统后获取当前 Context 下所有的 Cookie 保存进 Redis,随后调用查分函数。
  2. 成绩抓取与变动检测 (cjcx)

    • 用 Axios 并携带储存在 Redis 的 Cookie 访问成绩查询页面,避免每次查询都走沉重的无头浏览器渲染。
    • 校验返回内容是否包含登录页特有标签(用于判断 Cookie 是否过期)。如果已过期,重新拉起 runBrowser
    • 解析页面表格 HTML,将新抓取的成绩与 Redis 里的 kc_cj 旧成绩一一对比,生成变动日志。
    • 如果数据存在变动,更新 Redis 里的成绩缓存,同时触发 IYUU 接口把最新成绩推送至微信。
分类
标签
站点统计
文章
14
分类
4
标签
24
总字数
23,486
运行时长
0
最后活动
0 天前

文章目录