全自动清华刷课脚本

清华研究生的选课如今(2020年)还没有采用Waiting List机制,这就使得刷课脚本成为可能,然而验证码一直以来是困扰刷课脚本的最大难题。为了解决验证码,我独立提出了一个模型(我觉得肯定被提出过了,只是没有查阅文献),在朋友的帮助下完成了数据集的采集和标注,并达到了90%以上的准确率,实现了全自动清华刷课。项目在这里

本人设计的刷课脚本仅供学习用途,对使用时造成的损失不负任何责任。项目以GNU GPLv3协议发布,这意味着基于此项目的项目必须以同样的协议发布并开源。项目真的很不容易,写了1k多行的代码,还标注了4000个样本,这占用了我一定的精力。希望大家能用star的方式支持我的项目。

# 1 如何使用

首先你需要安装Python3。然后依次执行下面的命令,准备仓库。项目没有在Windows上测试过,但应该可以运行,欢迎PR来补充Windows的文档,Windows用户请使用第二条pip install指令。

#### Clone Repo
git clone https://github.com/sunziping2016/THUCourseSpider.git
# git clone git@github.com:sunziping2016/THUCourseSpider.git
cd THUCourseSpider

#### Create venv (Optional)
# python -m venv venv
# source venv/bin/activate

#### Install dependecies
pip install -r requirements.txt
# pip install -r requirements-win.txt

接下来的步骤可以分为3步:

  1. 搜集并标注数据(可使用repo里已标注好的captcha.tgz);
  2. 训练模型(可使用我训练好的模型,已经对外开放),在我的RTX 2070 Super上需要约20分钟;
  3. 运行刷课脚本。

对于想使用训练好的模型用户,可以从看1.2.11.3。如果需要自己训练模型,可以看1.1.11.1.51.21.3。对于实在是想知道标数据是什么样的体验的用户,通读整个第1章即可。

本项目的程序大多有命令行参数可供调整,加上--help参数即可查看帮助。

# 1.1 搜集并标注数据

原始数据集存放的目录是captcha。验证码被下下来并确认标记答案正确后,会存放到captcha/<md5hash>.<code>.jpeg。其中<md5hash>是图片的md5散列值,<code>是正确答案。

我们需要对一部分原始图片标记位置,以便更好地对分类网络进行预训练。没有预训练整个网络基本无法工作,所以标记位置是很重要的。位置信息会被存储在segmentation.txt。其内容大致如下:

000a5b757fe41963b3a3d503d3de4640.W2FP.jpeg:70,101,121,142
0019fa074e8d50b0727a570407c9f411.4QFT9.jpeg:57,77,101,123,148
00253474dbe8159a9a05720654d4a6e3.XWF8.jpeg:67,92,119,139
00362bf4a054de99b4f4db7393ea33c1.GXXP8.jpeg:55,76,100,126,150
006b4ce5e73d0a9f506002d71a3a9d71.6EHG.jpeg:66,89,110,138
00912fe666cd2b31102aebd3f8f08041.MT2V9.jpeg:53,77,103,121,151
00bebc089ae21814743963b8d0ec3422.Q9X4J.jpeg:56,80,104,130,150
01231260ea359c43877f654e8ab0e955.KH8P6.jpeg:58,80,104,126,145
01349ce3182ab1d608d4776ed4b79ac1.WQGBM.jpeg:50,76,110,132,152
013bb3f01ad530494837b6bf1b8af957.YK6H.jpeg:66,92,115,137

每一行是<filename>:<pos1>,<pos2>,...,<posN>,其中<filename>是文件名,<pos1><posN>是字符中心位置的x坐标值。一般而言,文件的行是按照文件名排序,x坐标值也是排好序的。

# 1.1.1 使用已标注好数据

解压captcha.tgz.tar.gz格式)到当前目录就可以使用先前标注好的数据。

tar xzvf captcha.tgz

目前包含4000张已经标记了答案的验证码,其中2004张还标记了位置。

# 1.1.2 搜集并标记验证码答案

首先,标记验证码需要登录选课系统,所以你可以将config.example.json拷贝为config.json。然后编辑config.json,修改其中的用户名和密码。

而后运行:

./label-captcha.py

就可以开始标记验证码,它会提示你需要输入验证码。验证码会被下载到captcha.jpeg文件中。输入的时候是不区分大小写的。

合并两个人标记的验证码是很简单的。只需要把captcha/*.jpeg文件拷贝一下就行。注意位置信息,即segmentation.txt不能这样合并,如果需要合并的包含位置信息,见下一节。

# 1.1.3 标记验证码位置

运行:

./segment-captcha.py

会弹出一个窗口,如下:

Segment Tool UI

鼠标点击图中可以标记一个位置,可以点住移动,再松开。单击已有的线会取消,拖拽已有的线会将它置于新的位置。<<表示寻找上一个没有完成标注的,>>同理,它们会循环,对应的快捷键是ctrl+方向键。<是上一张图,>是下一张图。所有的更改只有按了Ok(或者回车)才会保存,然后它会切到下一张图(相当于按了>>)。Prog表示现在共4000张图,2004张已标记,Idx是第几张图,Code是答案。正常的流程是打开软件,按>>切到第一未标记的张图,鼠标点击中心,然后回车,切到下张图,循环往复。

# 1.1.4 深度学习辅助的标记验证码答案

当训练好神经网络之后,标图就更加方便了。运行:

./label-captcha-gui.py
# ./label-captcha-gui.py --generator_save_path ./save/generator-full

如果你使用的是训练好的模型,请执行第2行命令。它会弹出一个窗口,如下:

Label Tool UI

默认模式下会载入神经网络,然后只需要在输入框中修改错误的验证码即可。点击Ok(或者回车)切换下一张验证码。

# 1.1.5 生成用于训练数据集

当数据集准备好了。即有大量的数据被标注好答案,并且大部分的数据都被标注好位置后,就可以生成用于训练数据集。它会切割图片并且切分数据集,运行:

./prepare-dataset.py

生成的用于训练的数据集位于dataset,大致内容如下:

dataset
├── segmented     将字符切割出来的数据集,用于训练分类器,需要被标注位置信息
│   ├── all       既包含训练又包含测试的数据集,用于训练最终给用户的模型
│   │   ├── +     包含空白或者两个字符之间的图片
│   │   │   ├── 000a5b757fe41963b3a3d503d3de4640.W2FP.158.jpeg  文件名.答案.中心位置.jpeg
│   │   │   ...
│   │   ├── 2     包含字符2的图片,下同
│   │   ├── 3
│   │   ...
│   ├── test      测试集
│   │   ├── +
│   │   ...
│   └── train     训练集
│   │   ├── +
│   │   ...
└── whole          完整图片的数据集,只切去了预设定的图片留白边框,用于训练生成器
    ├── all
    │   ├── 000a5b757fe41963b3a3d503d3de4640.W2FP.jpeg  文件名.答案.jpeg
    │   ...
    ├── test
    └── train

# 1.2 训练模型

模型的训练分为两步:

  1. 训练分类器:对单个字符的图片进行分类,用的是CNN;
  2. 训练生成器:讲分类器从原图中滑过提取出的特征,输入到RNN decoder中,生成答案。

实验表明,省去步骤一,直接用步骤二是不太能训练出好的模型的,我个人觉得那需要很强大的算力。实验也表明,提高分类器的准确度能比改进生成器提升得更快。此外,实验还表明,步骤二中似乎不把梯度回传给分类器特征提取部分,效果会更好,但差别不显著。

# 1.2.1 使用训练好的模型

预训练好的模型存放在这里,文件大小为752.3MB。下载后建议放置于save/generator-full/epoch.0050.pth这里,路径如果没有的话请手动创建。而后运行刷课脚本的时候,需要加入额外的参数--generator_save_path save/generator-full

# 1.2.2 训练分类器

./classifier.py
# ./classifier.py --gpu 0,1

运行上面的命令即可。注意如果你有GPU,请务必加上--gpu <id1>,<id2>,...,以加速训练。运行完毕后,save/classifier目录下会多出epoch.<epoch>.pth文件,那是保存的模型,还会有running-log.json,里面是训练时长、参数之类的信息。

此外训练的数据,如loss、accuracy都保存进了TensorBoard。运行下面的命令即可查看:

tensorboard --logdir runs

# 1.2.3 训练生成器

./generator.py
# ./classifier.py --gpu 0,1

运行上面的命令即可。需要注意的点同上。只是保存的目录默认是save/generator,此外目录里还会有predicates.<epoch>.txt文件,是模型在测试集上预测出来的验证码值,按文件名排序。

# 1.3 运行刷课脚本

首先,请确认你准备好了config.json文件,它包含了你的用户名和密码。你可以将config.example.json拷贝为config.json。然后编辑config.json,修改其中的用户名和密码。

然后,请你准备好courses.csv,它包含了你想要抢课的信息。你可以将courses.example.csv拷贝为courses.csv。然后编辑courses.csv。这里示例里给出的是2020年秋季学期学位课的两门英语课。一般情况下,你需要修改学年学期Semester、课程号Course Number和课序号Course ID

./graduate-crawler.py
# ./graduate-crawler.py --generator_save_path ./save/generator-full

如果你使用的是预训练好的模型,请使用第2行。你也可以加入--verbose查看额外的信息,默认情况只有尝试申请课程的时候才会打印输出。

# 2 设计细节

# 2.1 神经网络结构

首先,我利用切割出来的图片训练一个分类器,这里切割出来的图片是从左到右滑动窗口取出来的。如果滑动窗口的中心距离某个字符位置中心很近,这个图片的标签就是这个字符;否则这个图片会被标记为不包含字符,我们用+这个标签表示。可以注意到,并不是所有的数字和大写英文字母都会出现在验证码中,最终分类器是25分类。

分类器由两部分组成:

  1. 由卷积组成的特征提取部分;
  2. 由全连层组成的分类部分。

对于我们而言,最重要的是特征提取部分。

接着我们构建一个生成器,生成器类似于一个Seq2Seq模型,只是差别在于它的Encoder不是一个处理变长序列的RNN(GRU),而是处理定长输入的全连层。全连层将一个分类器特征提取出的所有特征连接在一起,映射到RNN的初始隐藏状态。RNN的输入是上一次产生的Token,输入的第一个Token是一个特殊的SOS(Start-Of-Sentence)字符。最后训练生成器的时候,我不训练分类器特征提取部分,这样似乎更好。

2016-2020 Ziping Sun
京ICP备 17062397号