学习通刷课:由本地自动化到云端部署

6266 字
16 分钟

学习通刷课:由本地自动化到云端部署

发布于

缘起:刷课需求

刷课界面
刷课界面

刚步入大一就要面对大学复杂的课程结构。但我永远忘不了某位PKUer所说,最顶尖的同学用自动化完成平常作业,那边是因为他们有更重要的目标、任务和使命。

本次项目需求为学习通刷课。学习通平台分为诸多任务点,要求每个任务点的视频逐次播放,且在第一次播放时不能快进或倍速。

本篇文章侧重于寻找一个解决方案(solution),亦即技术的组合。宽广的思路在vibe-coding下与程设同等重要。

脚本寻找

经过搜索笔者选用chaoxing仓库。README文件片段如下:

  1. clone 项目至本地
git clone --depth=1 https://github.com/Samueli924/chaoxing 
cd chaoxing
  1. 安装依赖
pip install -r requirements.txt

或使用 pip install .(通过 pyproject.toml 安装依赖)
3. (可选直接运行)

python main.py

由于个人习惯,在安装依赖之前通常要激活虚拟环境,避免繁多依赖冲突。但时过境迁virtualenv不复存在,而是运行:python3 -m venv venv,最后运行代码。

运行
运行

之后按照提示操作即可开始刷课。该脚本刷课最大的特点是可以实现并行的效果,即大约四个视频课可以同时开始在命令行中模拟播放,然而仍然不能实现快进或直接完成任务点。当然,由于笔者撰文时刷课已经结束,运行成功截图不再展示。

进阶问题的提出

  • 其一,对于大学生常用的笔记本电脑而言,因为刷课时间较长设备不能一直处于登录状态;当屏幕自动锁定时,mac的网络连接会断开,导致刷课终端;
  • 其二,刷课脚本配置本身较为繁琐,对操作水平要求高,所以我们需求寻找多开刷课的方法。

升级:服务器租赁与配置

对于高耗时低能耗、又需求多任务多开的个人业务来说,笔者注意到服务器是不二之选。当今的云电脑产品(如阿里云无影)也可以提供GUI云电脑,然而对于命令行程序而言,一台具备SSH(或其他远程终端)的服务器足够满足需求。

服务器配置
服务器配置

因为程序实在是太过轻量,我在此直接选择费用倒数第二便宜的订阅计划(34¥每月);对于操作系统并无太多需求,可安装阿里的Linux或Ubuntu Linux等熟悉的OS。

经过后续troubleshooting笔者经验是务必在控制台设置密码,并且以后均用root进行登录,以便在shell基础安装命令中规避报错。

这里还需要特别注意一个隐藏问题:阿里控制台的属性是弹出式窗口,尽管它是以一个普通的标签页显示的。而macOS会自动拦截弹出式窗口,需要在搜索框右侧点击一个小按钮打开被拦截的标签页,

配置程序

初始状态 macOS和Ubuntu在我印象中都是自带git的,然而Alibaba Cloud Linux却并非如此,而且我个人并不熟悉dnf包管理器。好在我们在shell中竟然可以输入自然语言,一开始也会遇到诸多权限和依赖的问题,但是AI会帮我们解决掉所有!(图略)

之后按照在本地配置的方法配置最终我们得到:

[root@iZt4ngyhe0s6h6pf5131w8Z ~]# ls chaoxing
chaoxing.2026-09-25_11-36-58_746733.log  pyproject.toml
chaoxing.2026-09-25_11-53-41_802150.log  README.md
chaoxing.2026-09-25_12-11-16_891039.log  repeat_run.sh
chaoxing.log                             requirements.txt
config_template.ini                      resource
cookies.txt                              run.sh
Dockerfile                               tests
LICENSE                                  api
main.py                                  app.py
[root@iZt4ngyhe0s6h6pf5131w8Z ~]# which git
/usr/bin/git

新问题的发现:网页版命令行的脆弱性

在Workbench命令行中运行python脚本后,我发现刷课基本成功,但当按下Command+W时,Safari浏览器弹窗提示在该网页有尚未保留的更改。这通常表明如果离开网页,会导致文件没保存,在今天的例子中,就是终端进程终止。

乍看之下,关掉网页命令行导致python程序中止与服务器持续运行十分矛盾。但经过更进一步的思考我们会发现终端会话其实不能与服务器的开关机混为一谈。关掉网页有类于在计算机中关掉命令行窗口,必然会导致该会话中进程停止。

此时GPT可以出场了(为了演示简便,换用了doubao):


✅ 网页版终端关掉,服务器本身还在正常跑,不会关机!

简单区分两个东西:

  1. 阿里云ECS服务器实例:这台云主机本身,只要你不手动停止实例、不欠费,它就一直运行,网站、后台程序都继续跑。
  2. 网页版终端(阿里云Workbench):它只是远程连接工具,就像Xshell、Putty。
    • 关闭网页标签 / 关掉Workbench终端窗口:只是断开你这一次的远程连接,服务器不会停。
    • 但是!你在终端里直接跑的前台程序会被终止(重点)

关键坑点(很容易踩) 如果你在网页终端里直接敲命令跑程序(比如python app.py、node、java),不加后台运行参数:

  • 只要关闭网页终端,连接断开 → 会话结束 → 这个前台进程直接被杀掉,程序停止。

想要断开终端后程序继续跑,任选一种方案

  1. nohup(最简单)
nohup python app.py &

输出日志会写到 nohup.out,关闭终端程序继续运行。

  1. screen / tmux(推荐,方便后续再进去看输出)
# screen示例
screen -S myrun
# 在里面启动你的程序,然后按 Ctrl+A 再按 D 后台脱离
# 下次再连回来:screen -r myrun
  1. 做成系统服务(systemd),最稳定,开机自动启动。

快速总结

  • 关网页终端 ≠ 关机器 ✔ 服务器实例继续在线
  • 关网页终端 = 断开ssh会话,前台运行的程序会被杀
  • 想要程序后台常驻:nohup / screen / tmux / systemd

对于强交互性的CLI,必定使用screen/tmux,笔者选用后者。阿里Linux本身没有安装tmux,因此先使用自然语言安装。得到:

[root@iZt4ngyhe0s6h6pf5131w8Z ~]# which tmux
/usr/bin/tmux

之后我们可以使用如下命令首次创建会话:

tmux new -s mtl

回车后会进入另一个终端,如果需要脱离,按下Ctrl+B再单独按下D即可。如果要再次进入该会话:

tmux attach -t mtl`

如果我们进入会话后运行python脚本,在运行过程中脱离会话,程序仍然在服务器中运行,但此时如果关掉网页,我们将不会关掉正在工作的shell,自然不会中止进程。

按照这种思路,我给四五位同学分别创建了属于他们的tmux标签页,分别运行该脚本,即可实现多开。

demo
demo

更大问题:脚本提前终止与解决

笔者发现脚本会意外终止,如果遇到下面几类问题:

  1. 触发网站的安全验证,且脚本的自动验证程序未能通过;
  2. 因为脚本识别错误,误认为完成所有任务,意外提前中止;
  3. 网络问题和其他未知问题导致json报错。

除此之外,笔者发现python程序会因为未知问题卡住。

考虑时间精力等诸多成本,我们如何最简单地解决?当然是重新运行,然而手动运行无疑背离了自动化初衷。

我们考虑如下的shell程序:该程序接受两个参数,手机号和密码(给定courseID),将其传入python脚本。如果程序停止运行或较长时间没有新的输出,则进入循环重新运行。

通过vibe-coding得到以下脚本:

#!/bin/bash
set +o histexpand

# 默认参数
USERNAME=""
PASSWORD=""
LIST_ID="266377701"
# 无输出判定超时:单位秒,默认20s日志不增长就杀进程
NO_OUTPUT_TIMEOUT=20

# 解析shell参数
while getopts "u:p:l:w:" opt; do
  case $opt in
    u) USERNAME="$OPTARG" ;;
    p) PASSWORD="$OPTARG" ;;
    l) LIST_ID="$OPTARG" ;;
    w) NO_OUTPUT_TIMEOUT="$OPTARG" ;;
    \?) echo "无效参数: -$OPTARG" >&2; exit 1 ;;
    :) echo "选项 -$OPTARG 需要参数." >&2; exit 1 ;;
  esac
done

# 校验必填参数
if [ -z "$USERNAME" ] || [ -z "$PASSWORD" ]; then
  echo "用法: $0 -u 账号 -p 密码 [-l 课程id] [-w 无输出超时秒数]"
  echo "示例:$0 -u 13999999776 -p 'lzh13999999776!!' -l 266377701 -w 300"
  echo "说明:-w 连续多久无新输出则判定卡死,默认300秒(5分钟)"
  exit 1
fi

echo "==== 配置信息 ===="
echo "账号: $USERNAME"
echo "课程ID: $LIST_ID"
echo "无输出超时阈值: ${NO_OUTPUT_TIMEOUT}s"
echo "=================="

# 无限循环
while true
do
    echo -e "\n========== 启动本轮 python 程序 =========="
    TMP_LOG=$(mktemp /tmp/py_run.XXXXXX.log)
    echo "临时日志文件: $TMP_LOG"

    # 2>&1 捕获stderr,stdout+stderr全部交给tee,屏幕打印+写入日志
    python3 main.py -u "$USERNAME" -p "$PASSWORD" -l "$LIST_ID" 2>&1 | tee "$TMP_LOG" &
    PY_PID=$!
    echo "Python进程PID: $PY_PID"

    # 后台监控:检测日志文件大小是否不再变化
    (
        prev_size=$(stat -c %s "$TMP_LOG")
        start_no_output=$(date +%s)
        while true; do
            sleep 10
            current_size=$(stat -c %s "$TMP_LOG")
            now=$(date +%s)
            # 文件变大,重置计时器
            if [ "$current_size" -gt "$prev_size" ]; then
                prev_size=$current_size
                start_no_output=$now
                continue
            fi
            # 文件大小不变,计算无输出时长
            no_output_duration=$(( now - start_no_output ))
            if [ $no_output_duration -ge $NO_OUTPUT_TIMEOUT ]; then
                echo "⚠️ 检测到连续 ${NO_OUTPUT_TIMEOUT}s 无任何输出,判定卡死,杀死进程 $PY_PID"
                kill "$PY_PID" 2>/dev/null
                exit 0
            fi
            # python进程已经结束,监控退出
            if ! ps -p "$PY_PID" > /dev/null; then
                exit 0
            fi
        done
    ) &
    MONITOR_PID=$!

    # 等待python进程结束(不管是正常退出,还是被监控kill掉,都会到这一行)
    wait $PY_PID
    PY_RET=$?
    # 关闭后台监控进程
    kill $MONITOR_PID 2>/dev/null

    echo "========== 本轮结束,python返回码: $PY_RET =========="
    rm -f "$TMP_LOG"
    echo "等待3秒后重新运行..."
    sleep 3
done

注:shell中python脚本调用时,是可以直接传参数的,不用如一开始那样先运行再分别输入手机号、密码。

番外收获:shell语法的盲区

我在iPad阿里云应用程序上通过SSH登录服务器,发现当我尝试为一位同学输入用户名和密码时,命令行的输入却越来越长。

异常
异常

为什么别的同学没问题,单单这位同学有问题?我的解决尝试包括用电脑登录、删除会话重开。最终,感叹号勾起了我的好奇,于是我从GPT得到如下答案:


核心报错:unrecognized arguments: main.py

意思:程序收到了多余参数 main.py,说明你的命令被重复拼接了,和 ! 确实有关系。

  1. 根本原因:bash 的 ! 历史扩展(history expansion) 在 bash 默认设置下,! 是历史命令引用符号,不是普通字符。 你这条命令:
python3 main.py -u 13999999776 -p lzh13999999776!! -l 266377701

里面的 !!,bash 会自动替换成上一条完整命令。 于是最终传给 python 的参数就变成:

-p lzh13999999776python3 main.py -u 13999999776 -p lzh13999999776!! -l 266377701

等于把整条旧命令塞到 -p 的密码位置,额外多出 main.py 这个参数 → python 识别到无法识别的参数 main.py,直接报这个错。

  1. 两种解决办法 方案A:给密码加单引号(推荐) 用单引号包裹带感叹号的字符串,bash 不会解析里面的特殊符号:
python3 main.py -u 13999999776 -p 'lzh13999999776!!' -l 266377701

⚠️ 不能用双引号 " ",双引号里面 !! 依然会触发历史扩展!

方案B:临时关闭bash历史扩展 执行一次这个命令关闭 ! 功能:

set +o histexpand

之后再运行你的python命令,! 就只是普通字符。 想要恢复默认:set -o histexpand

  1. 补充小提示
  • 从截图看,每次敲完带 !! 的命令,回显直接多出一长串 python3 main.py ...,就是 !! 被自动替换的证据。
  • 返回码 2 在 argparse 里就是命令行参数解析失败,正好对应这个问题。

你直接复制方案A那条命令试一下,就能正常启动。


结语

这个方法仍然不是百分百可靠的。然而我终究帮助包括我在内的四名同学在短时间内刷完了水课。 许多平淡的叙事背后,都是无数debug和troubleshooting换来的, nResFE 但是我无论如何也愿意尝试一下。 XLpzkZ