├── .gitignore
├── README.md
├── config.py
├── enroll.py
└── mailer.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 | .pytest_cache/
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 | local_settings.py
57 | db.sqlite3
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # Environments
85 | .env
86 | .venv
87 | env/
88 | venv/
89 | ENV/
90 | env.bak/
91 | venv.bak/
92 |
93 | # Spyder project settings
94 | .spyderproject
95 | .spyproject
96 |
97 | # Rope project settings
98 | .ropeproject
99 |
100 | # mkdocs documentation
101 | /site
102 |
103 | # mypy
104 | .mypy_cache/
105 |
106 | auth
107 | courseid
108 | cookie.pkl
109 | captcha.jpg
110 | mailconfig
111 | .vscode/launch.json
112 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # UCAS
2 |
3 | ucas 选课脚本,支持轮询选课,当有人退课时自动选课。
4 |
5 | > 注:每年网站代码可能会变动,因作者无选课权限,且SEP不提供预选测试,故脚本存在失效可能,请注意风险。推荐在选课前查看本仓库代码是否有更新,并通过登录等功能对代码进行测试。如果在选课过程中发现课程的CollegeCode或选课网站的API有更新,欢迎发起PR或issue,非常感谢。
6 |
7 | ## 安装
8 |
9 | ### 环境依赖
10 |
11 | Python 3.x
12 |
13 | ### Mac
14 |
15 | ```bash
16 | brew install python3
17 | sudo easy_install pip
18 | sudo pip install requests
19 | ```
20 |
21 | ### Linux
22 |
23 | ```bash
24 | sudo apt install python3-pip
25 | sudo pip install requests
26 | ```
27 |
28 | ### Windows
29 |
30 | [官网](https://www.python.org/downloads/)中安装Python后安装requests
31 |
32 | ```bash
33 | python -m pip install requests
34 | ```
35 |
36 | ## 初始化
37 |
38 | 在当前目录下创建 `auth` 文件并填入登录信息,格式如下:
39 |
40 | ```
41 | i@iie.ac.cn
42 | inputpassword
43 | ```
44 |
45 | 第一行为用户名,第二行为密码
46 |
47 | 在当前目录下创建 `courseid` 文件并填入课程,格式如下:
48 |
49 | ```
50 | 091M5023H,xx学院
51 | 091M4002H
52 | ```
53 |
54 | 其中`xx学院`可选(建议填写),课程号与学院名称间用`,`分隔(注意中英文符号),学院名必须与选课系统上的名称部分匹配。
55 | 例如英语课是"外语系"开课,那么可以填"外语系"或"外语",但不能填"外国语"
56 |
57 | 截止至2021年01月18日,选课系统上有效的学院名称:
58 | - 数学学院
59 | - 物理学院
60 | - 天文学院
61 | - 化学学院
62 | - 材料学院
63 | - 生命学院
64 | - 地球学院
65 | - 资环学院
66 | - 计算机学院
67 | - 电子学院
68 | - 工程学院
69 | - 经管学院
70 | - 公管学院
71 | - 人文学院
72 | - 马克思主义学院
73 | - 外语系
74 | - 中丹学院
75 | - 国际学院
76 | - 存济医学院
77 | - 体育教研室
78 | - 微电子学院
79 | - 未来技术学院
80 | - 网络空间安全学院
81 | - 心理学系
82 | - 人工智能学院
83 | - 纳米科学与技术学院
84 | - 艺术中心
85 | - 光电学院
86 | - 创新创业学院
87 | - 核学院
88 | - 现代农业科学学院
89 | - 化学工程学院
90 | - 海洋学院
91 | - 航空宇航学院
92 | - 杭州高等研究院
93 |
94 | config文件中共有三个配置,单次请求等待时间,轮询最短时间和轮询最长时间,可根据需求修改
95 |
96 | ## 持久运行
97 |
98 | 非校园网环境登录需要验证码,须长期轮询是否有人退课时,可使用 ``python enroll.py -c`` 命令运行, 此时会在目录下生成captcha.jpg文件,根据该图片的内容输入验证码即可登录。
99 |
100 | ## 邮件提醒
101 |
102 | 需要邮件提醒时,在当前目录下创建 `mailconfig` 文件并填入登录信息,格式如下:
103 |
104 | ```
105 | i@iie.ac.cn
106 | inputpassword
107 | mail.cstnet.cn
108 | i@iie.ac.cn
109 | ```
110 |
111 | 第一行为邮箱用户名,第二行为邮箱密码,第三行为SMTP服务器地址,第四行为接收通知邮件的邮箱。
112 |
113 | 创建完成后,带 `-m` 参数运行即可在选课结束后发信通知。
114 |
115 | 注:
116 |
117 | 1. 网易系邮箱第三方不能使用密码登录,需单独设置授权码。
118 | 2. 学校邮箱服务器为 `mail.cstnet.cn`
119 |
120 | ## 更新概要
121 |
122 | - `2021-09-03` 选课系统加入了CSRF Token与Referer验证,[Yangjiaxi](https://github.com/Yangjiaxi) 更新了对应的逻辑
123 |
124 | - `2021-09-01` [KLOSYX](https://github.com/KLOSYX) 发现SEP系统更新了验证码地址,提交[issue](https://github.com/LyleMi/ucas/pull/10)进行了维护
125 |
126 | - `2021-01-18` [Cirn09](https://github.com/LyleMi/ucas/pull/6) 增加了Python3支持,增加了自动重新登录等特性,添加了新的courseId格式
127 |
128 | - `2019-08-29` [pzhxbz](https://github.com/LyleMi/ucas/pull/3) 更新了CollegeCode
129 |
130 | - `2018-09-06` 参考前人的代码完成选课功能,增加重试等特性
131 |
--------------------------------------------------------------------------------
/config.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 |
5 | class Config:
6 |
7 | timeout = 5
8 | minIdle = 10
9 | maxIdle = 20
10 | waitForUser = 60
11 |
--------------------------------------------------------------------------------
/enroll.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | import os
5 | import re
6 | import sys
7 | import time
8 | import random
9 | import pickle
10 | import logging
11 | import requests
12 |
13 | from config import Config
14 | from mailer import sendemail
15 |
16 | CollegeCode = {
17 | '0701': '数学',
18 | '0702': '物理',
19 | '0704': '天文',
20 | '0703': '化学',
21 | '0802': '材料',
22 | '0710': '生命',
23 | '0706': '地球',
24 | '0705': '资环',
25 | '0812': '计算',
26 | '0808': '电子',
27 | '0801': '工程',
28 | '1256': '工程',
29 | '1205': '经管',
30 | '0201': '经管',
31 | '0202': '公管',
32 | '0301': '公管',
33 | '1204': '公管',
34 | '0503': '人文',
35 | '0502': '外语',
36 | '16': '中丹',
37 | '17': '国际',
38 | '1001': '存济',
39 | '0854': '微电',
40 | '0839': '网络',
41 | '2001': '未来',
42 | '22': '',
43 | '0101': '马克',
44 | '0305': '马克',
45 | '0402': '心理',
46 | '0811': '人工',
47 | '0702': '纳米',
48 | '1302': '艺术',
49 | '0452': '体育',
50 | }
51 |
52 |
53 | class Login:
54 | page = 'http://sep.ucas.ac.cn'
55 | url = page + '/slogin'
56 | system = page + '/portal/site/226/821'
57 | pic = page + '/randomcode.jpg'
58 |
59 |
60 | class Course:
61 | base = 'http://jwxk.ucas.ac.cn'
62 | identify = base + '/login?Identity='
63 | selected = base + '/courseManage/selectedCourse'
64 | selection = base + '/courseManage/main'
65 | category = base + '/courseManage/selectCourse?s='
66 | save = base + '/courseManage/saveCourse?s='
67 |
68 |
69 | class NetworkSucks(Exception):
70 | pass
71 |
72 |
73 | class AuthInvalid(Exception):
74 | pass
75 |
76 |
77 | class Cli(object):
78 |
79 | headers = {
80 | 'Connection': 'keep-alive',
81 | 'Pragma': 'no-cache',
82 | 'Cache-Control': 'no-cache',
83 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
84 | 'Upgrade-Insecure-Requests': '1',
85 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:61.0) Gecko/20100101 Firefox/61.0',
86 | 'Accept-Encoding': 'gzip, deflate, sdch',
87 | 'Accept-Language': 'zh-CN,zh;q=0.8,en;q=0.6',
88 | }
89 |
90 | def __init__(self, user, password, captcha=False):
91 | super(Cli, self).__init__()
92 | self.logger = logging.getLogger('logger')
93 | self.s = requests.Session()
94 | self.s.headers = self.headers
95 | self.s.timeout = Config.timeout
96 | self.login(user, password, captcha)
97 | self.initCourse()
98 |
99 | def get(self, url, *args, **kwargs):
100 | r = self.s.get(url, *args, **kwargs)
101 | if r.status_code != requests.codes.ok:
102 | raise NetworkSucks
103 | return r
104 |
105 | def post(self, url, *args, **kwargs):
106 | r = self.s.post(url, *args, **kwargs)
107 | if r.status_code != requests.codes.ok:
108 | raise NetworkSucks
109 | return r
110 |
111 | def initCourse(self):
112 | self.courseid = []
113 | with open('courseid', 'r', encoding='utf8') as fh:
114 | for c in fh:
115 | tmp = c.replace(' ', '').strip()
116 | if len(tmp):
117 | self.courseid.append(tmp.split(','))
118 |
119 | def login(self, user, password, captcha):
120 | if os.path.exists('cookie.pkl'):
121 | self.load()
122 | if self.auth():
123 | return
124 | else:
125 | self.logger.debug('cookie expired...')
126 | os.unlink('cookie.pkl')
127 | self.get(Login.page)
128 | data = {
129 | 'userName': user,
130 | 'pwd': password,
131 | 'sb': 'sb'
132 | }
133 | if captcha:
134 | with open('captcha.jpg', 'wb') as fh:
135 | fh.write(self.get(Login.pic).content)
136 | data['certCode'] = input('input captcha >>> ')
137 | self.post(Login.url, data=data)
138 | if 'sepuser' not in self.s.cookies.get_dict():
139 | self.logger.error('login fail...')
140 | sys.exit()
141 | self.save()
142 | self.auth()
143 |
144 | def auth(self):
145 | r = self.get(Login.system)
146 | identity = r.text.split('
170 | raise AuthInvalid
171 | courseid = []
172 | self.logger.debug(self.courseid)
173 | for info in self.courseid:
174 | cid = info[0]
175 | college = info[1] if len(info) > 1 else None
176 | if cid in r.text:
177 | self.logger.info('course %s already selected' % cid)
178 | continue
179 | error = self.enrollCourse(cid, college)
180 | if error:
181 | self.logger.debug(
182 | 'try enroll course %s fail: %s' % (cid, error))
183 | courseid.append(info)
184 | else:
185 | self.logger.debug("enroll course %s success" % cid)
186 | return courseid
187 |
188 | def enrollCourse(self, cid, college):
189 | r = self.get(Course.selection)
190 | depRe = re.compile(r'' in r.text:
224 | return None
225 | else:
226 | return "full"
227 | else:
228 | return "not found"
229 |
230 |
231 | def initLogger():
232 | formatStr = '[%(asctime)s] [%(levelname)s] %(message)s'
233 | logger = logging.getLogger('logger')
234 | logger.setLevel(logging.DEBUG)
235 | ch = logging.StreamHandler()
236 | chformatter = logging.Formatter(formatStr)
237 | ch.setLevel(logging.DEBUG)
238 | ch.setFormatter(chformatter)
239 | logger.addHandler(ch)
240 |
241 |
242 | def main():
243 | initLogger()
244 | with open('auth', 'r', encoding='utf8') as fh:
245 | user = fh.readline().strip()
246 | password = fh.readline().strip()
247 | if '-c' in sys.argv or 'captcha' in sys.argv:
248 | captcha = True
249 | else:
250 | captcha = False
251 | c = Cli(user, password, captcha)
252 | reauth = False
253 | while True:
254 | try:
255 | if reauth:
256 | c.auth()
257 | reauth = False
258 |
259 | courseid = c.enroll()
260 | if not courseid:
261 | break
262 | c.courseid = courseid
263 | time.sleep(random.randint(Config.minIdle, Config.maxIdle))
264 | except IndexError as e:
265 | c.logger.info("Course not found, maybe not start yet")
266 | time.sleep(random.randint(Config.minIdle, Config.maxIdle))
267 | except KeyboardInterrupt as e:
268 | c.logger.info('user abored')
269 | break
270 | except (
271 | NetworkSucks,
272 | requests.exceptions.ConnectionError,
273 | requests.exceptions.ConnectTimeout
274 | ) as e:
275 | c.logger.debug('network error')
276 | except AuthInvalid as e:
277 | c.logger.error('wait for user operating')
278 | reauth = True
279 | time.sleep(Config.waitForUser)
280 | # reauth next loop
281 | except Exception as e:
282 | c.logger.error(repr(e))
283 | if ('-m' in sys.argv or 'mail' in sys.argv) and os.path.exists('mailconfig'):
284 | with open('mailconfig', 'rb') as fh:
285 | user = fh.readline().strip()
286 | pwd = fh.readline().strip()
287 | smtpServer = fh.readline().strip()
288 | receiver = fh.readline().strip()
289 | sendemail(user, pwd, smtpServer, receiver)
290 |
291 |
292 | if __name__ == '__main__':
293 | main()
294 |
--------------------------------------------------------------------------------
/mailer.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 |
5 | import smtplib
6 | from email.mime.multipart import MIMEMultipart
7 | from email.mime.text import MIMEText
8 | from email.mime.application import MIMEApplication
9 | from email.header import Header
10 |
11 |
12 | def sendemail(user, pwd, smtpServer, receiver):
13 | username = "同学"
14 | title = "选课成功通知"
15 | content = "选课已完成,请登录课程网站确认"
16 | receivers = [receiver]
17 | server = smtplib.SMTP_SSL(smtpServer, port=465)
18 | message = MIMEMultipart()
19 | message['From'] = Header('UCAS 选课插件', 'utf-8')
20 | message['To'] = Header(username, 'utf-8')
21 | subject = title
22 | message['Subject'] = Header(subject, 'utf-8')
23 |
24 | text = MIMEText(content, 'html', 'utf-8')
25 | message.attach(text)
26 |
27 | try:
28 | server.login(user, pwd)
29 | server.sendmail(user, receivers, message.as_string())
30 | server.close()
31 | return True
32 | except smtplib.SMTPRecipientsRefused as e:
33 | print('Recipient refused')
34 | except smtplib.SMTPAuthenticationError as e:
35 | print('Auth error')
36 | except smtplib.SMTPSenderRefused as e:
37 | print('Sender refused')
38 | except smtplib.SMTPException as e:
39 | print(repr(e))
40 | return False
41 |
--------------------------------------------------------------------------------