├── .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 | --------------------------------------------------------------------------------