2023年6月1日

最近在搞爬取深圳技术大学的教务系统,搞定之后做个笔记。

实际上搞定模拟登录之后后面的就很简单了,只剩下爬课表和转json要处理。

前置准备与puppeteer

A high-level API to control headless Chrome over the DevTools Protocol

官方描述上面的英文就是,这里直接说该怎么用。

首先导入需要的npm包,然后准备好账号和密码,以及登录界面的Url。这里深圳技术大学教务系统的html为例。

const puppeteer = require('puppeteer');
const fs = require('fs');

const loginUrl = 'https://auth.sztu.edu.cn/idp/authcenter/ActionAuthChain?entityId=jiaowu';
const targetUrl = 'https://jwxt.sztu.edu.cn/jsxsd/framework/xsMain.htmlx';
const username = 'xxxxxxxxxxx'; 
const password = 'xxxxxxxxxxx';

准备好之后,通过puppeteer包中方法实现模拟浏览器和打开页面。由于像学校教务系统这些经常需要内网,挂着vpn或者外网环境时通常无法访问。所以建议用try-catch接收异常方便后面调试。同时记得自己先尝试访问教务系统的登录界面,看看能不能打开再进行后续测试。

const puppeteer = require('puppeteer');
const fs = require('fs');

const loginUrl = 'https://auth.sztu.edu.cn/idp/authcenter/ActionAuthChain?entityId=jiaowu';
const targetUrl = 'https://jwxt.sztu.edu.cn/jsxsd/framework/xsMain.htmlx';
const username = 'xxxxxxxxxxx'; 
const password = 'xxxxxxxxxxx'; 

async function simulateLogin() {
    try {
        const browser = await puppeteer.launch();
        const page = await browser.newPage();

        await page.goto(loginUrl);

    } catch (error) {
        console.error('登录失败:', error);
    }
}

simulateLogin();

然后想想还需要些啥?模拟登录,说白了还是“登录”。

一般的登录流程:

  • 打开登录界面
  • 在指定的文本框中输入账号和密码
  • 点击登录按钮

更详细的登录流程:

  • 访问登录界面,根据html初始化页面
  • 在指定的文本框中输入账号和密码,输入完成后对当前网页的account和password赋值。
  • 点击登录按钮,触发响应事件,将account和password(password大部分都会加密,具体是DES还是md5需要去网页源码下找相关函数)传到数据库进行校验。

我们现在完成了第一步。接下来就是为当前网页的account和password赋值。

直接右键“检查”输入账号和密码的文本框,以深圳技术大学的教务系统为例

<input type="text" class="inputLogin leoPwd" name="j_username" id="j_username" value="工号" title="202x0020xxxx" onmouseover="this.title=this.value" onblur="inputOnblur(this); verCodeDisplay('/idp', 'j_username', '1', 'yzm1Bar', true)" maxlength="64">

这里“检查”的是账号的输入框,可以发现它的nameh和id为”j_username”。有name或者id就好办了,直接检索当前页面各元素,然后给它赋值即可。密码同理,直接放上代码

const puppeteer = require('puppeteer');
const fs = require('fs');

const loginUrl = 'https://auth.sztu.edu.cn/idp/authcenter/ActionAuthChain?entityId=jiaowu';
const targetUrl = 'https://jwxt.sztu.edu.cn/jsxsd/framework/xsMain.htmlx';
const username = 'xxxxxxxxxxx'; 
const password = 'xxxxxxxxxxx'; 

async function simulateLogin() {
    try {
        const browser = await puppeteer.launch();
        const page = await browser.newPage();

        await page.goto(loginUrl);
 
        await page.type('input[name="j_username"]', username);
        await page.type('input[name="j_password"]', password);

    } catch (error) {
        console.error('登录失败:', error);
    }
}

simulateLogin();

模拟登录

探讨是否需要跳转网页

现在还差最后一步–模拟点击按钮。

但真的需要去“点击”按钮吗?很明显不需要。按钮只是为了触发响应事件做的,说明你并不需要去“点击”按钮,直接触发它的响应事件即可。

但是既然用了puppeteer包,那不如直接模拟点击按钮,明显省事的多。上面只是提供一种可行的思路,因为有些事件不是通过<button>响应的,这种情况的话就得考虑直接触发响应事件了。

当然,对于我学校的教务来说,点击按钮后直接就跳转到了教务界面。然后其他所有的操作都是动态加载的,即全部都在同一个url链接上完成加载。举个例子

  • 教务界面:https://white-night.club
  • 课表界面:https://white-night.club

但是,假设你和我一样在爬自己学校的课表。但是你的课表页面和登录后的界面是分开的。例子如下

  • 教务界面:https://white-night.club
  • 课表界面:https://white-night.club/courses

那么就可以分为两种情况。如果你的情况和我一样,可以写跳转,也可以不写。虽然我个人还是建议写个跳转以防万一。

如果是第二种情况,那我强烈建议写跳转。不然又要写一堆模拟点击按钮的操作。而且有些网页的响应事件不是通过button组件触发的,这种情况下你需要检索整个网页的html去“手动”触发响应事件。很明显超级麻烦。所以不如直接跳转到课表界面。

还是以深圳技术大学的教务为例,代码如下

const puppeteer = require('puppeteer');
const fs = require('fs');

const loginUrl = 'https://auth.sztu.edu.cn/idp/authcenter/ActionAuthChain?entityId=jiaowu';
const targetUrl = 'https://jwxt.sztu.edu.cn/jsxsd/framework/xsMain.htmlx';
const username = 'xxxxxxxxxxx'; 
const password = 'xxxxxxxxxxx'; 

async function simulateLogin() {
    try {
        const browser = await puppeteer.launch();
        const page = await browser.newPage();

        await page.goto(loginUrl);
 
        await page.type('input[name="j_username"]', username);
        await page.type('input[name="j_password"]', password);

        await page.waitForNavigation();

        // 如果想写跳转,保留这一行
        await page.goto(targetUrl);

        // 由于网速不同,建议自行设置等待时间以保证页面完全加载
        await page.waitForTimeout(2000);
    } catch (error) {
        console.error('登录失败:', error);
    }
}

simulateLogin();

接下来就是获取课表的html然后继续些什么字符串分割啊,正则表达式处理啊,划分后保存到json文件里啥的操作了。

写者注

和群友讨论的时候,讨论到我们学校的课表界面有个按钮,触发后就是直接把excel形式的课表保存到本地。用这种方式获取课表是最方便的。如果不需要后续数据处理的话可以试着直接保存为excel。
不过这种方法对于需要后续数据处理的有个问题。保存的路径是浏览器的下载路径,没法自定义。即使能自定义,这意味着我后续操作需要多一步获取文件路径的方法。而自动获取路径那是超级,超级麻烦的一件事。100个用户有99个的路径不一样,有英文有中文,有自定义有默认的。
当然,可以让用户通过filepicker去手动选择文件。但这种操作你仔细想一下就会发现,后续会出现很多修改或者实现起来超级麻烦的地方(比如学校给excel的命名规则改了;用户重复导入的话要保留这次导入的,然后把上一次导入的excel文件删了;如果上次导入的文件用户打开了在后台挂着,导致这个文件变为“只读”,你无法删除它;)。
当然并不是说无法实现,只是不建议这么做。如果我是用户我肯定希望我需要手动操作的地方越少越好。
所以最后还是选择了直接把课表网页的html保存下来,对html作操作的方法。这样保存的html直接在JavaScript程序的根目录下,操作起来还是很方便的。