├── user_agents.txt.gz ├── requirements.txt ├── .gitignore ├── HISTORY.md ├── main.py ├── autoelective ├── __init__.py ├── environ.py ├── rule.py ├── _internal.py ├── utils.py ├── course.py ├── iaaa.py ├── parser.py ├── cli.py ├── const.py ├── monitor.py ├── logger.py ├── client.py ├── exceptions.py ├── config.py ├── hook.py ├── elective.py └── loop.py ├── MIGRATION_GUIDE.md ├── Docker ├── latest.Dockerfile └── monitor.Dockerfile ├── DOCKER.md ├── .github └── ISSUE_TEMPLATE.md ├── LICENSE ├── MIGRATION_GUIDE_origin.md ├── README.md ├── config.sample.ini ├── HISTORY_origin.md └── README_origin.md /user_agents.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylelv2000/PKUAutoElective_updated/HEAD/user_agents.txt.gz -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Werkzeug==0.15.4 2 | lxml==4.3.0 3 | requests==2.22.0 4 | numpy==1.18.2 5 | ddddocr==1.4.2 6 | Flask==1.0.2 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | __pycache__/ 3 | /log 4 | /cache 5 | /config 6 | /bin 7 | /archive 8 | /tmp 9 | config.ini 10 | user_agents.user.txt 11 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | Release History 2 | =============== 3 | 4 | v6.0.0.1 (2021-09-11) 5 | ------------------- 6 | - 更新了对选课网部分 API 的请求方法 (`SupplyCancel.do` -> `SupplyCancel.do?hx=`) 7 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # filename: main.py 4 | # modified: 2019-09-11 5 | 6 | from autoelective.cli import run 7 | 8 | if __name__ == '__main__': 9 | run() 10 | -------------------------------------------------------------------------------- /autoelective/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # filename: __init__.py 4 | # modified: 2019-09-13 5 | 6 | __version__ = "7.0.0" 7 | __date__ = "2022.02.20" 8 | __author__ = "qaz" 9 | -------------------------------------------------------------------------------- /MIGRATION_GUIDE.md: -------------------------------------------------------------------------------- 1 | Migration Guide 2 | ==================== 3 | 4 | v5.0.1 -> 6.0.0.1 5 | ------------------ 6 | - 新的验证码识别模块依赖 opencv,安装命令 `pip3 install opencv-python`; 7 | - 新的验证码识别模块不再依赖于 joblib,并删除了对 simplejson 的可选依赖; 8 | - PyTorch 版本需保证大于等于 1.5.0,否则无法读取 CNN 模型。 9 | -------------------------------------------------------------------------------- /Docker/latest.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:slim 2 | 3 | LABEL maintainer="you.siki@outlook.com" 4 | 5 | RUN pip install --no-cache-dir \ 6 | -i https://pypi.tuna.tsinghua.edu.cn/simple \ 7 | lxml \ 8 | numpy \ 9 | Pillow \ 10 | sklearn \ 11 | requests \ 12 | simplejson 13 | 14 | ADD . /workspace 15 | 16 | VOLUME [ "/config" ] 17 | 18 | WORKDIR /workspace 19 | 20 | CMD [ \ 21 | "python", \ 22 | "main.py", \ 23 | "--config=/config/config.ini", \ 24 | "--course-csv-gbk=/config/course.gbk.csv", \ 25 | "--course-csv-utf8=/config/course.utf-8.csv" ] -------------------------------------------------------------------------------- /autoelective/environ.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # filename: environ.py 4 | # modified: 2020-02-16 5 | 6 | from .utils import Singleton 7 | from collections import defaultdict 8 | import numpy as np 9 | 10 | class Environ(object, metaclass=Singleton): 11 | 12 | def __init__(self): 13 | self.config_ini = None 14 | self.with_monitor = None 15 | self.iaaa_loop = 0 16 | self.elective_loop = 0 17 | self.errors = defaultdict(lambda: 0) 18 | self.iaaa_loop_thread = None 19 | self.elective_loop_thread = None 20 | self.monitor_thread = None 21 | self.goals = [] # [Course] 22 | self.ignored = {} # {Course, reason} 23 | -------------------------------------------------------------------------------- /Docker/monitor.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:slim 2 | 3 | LABEL maintainer="you.siki@outlook.com" 4 | 5 | RUN pip install --no-cache-dir \ 6 | -i https://pypi.tuna.tsinghua.edu.cn/simple \ 7 | lxml \ 8 | numpy \ 9 | Pillow \ 10 | sklearn \ 11 | requests \ 12 | simplejson 13 | 14 | RUN pip install --no-cache-dir \ 15 | -i https://pypi.tuna.tsinghua.edu.cn/simple \ 16 | flask \ 17 | werkzeug 18 | 19 | ADD . /workspace 20 | 21 | VOLUME [ "/config" ] 22 | 23 | WORKDIR /workspace 24 | 25 | CMD [ \ 26 | "python", \ 27 | "main.py", \ 28 | "--with-monitor", \ 29 | "--config=/config/config.ini", \ 30 | "--course-csv-gbk=/config/course.gbk.csv", \ 31 | "--course-csv-utf8=/config/course.utf-8.csv" ] -------------------------------------------------------------------------------- /autoelective/rule.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # filename: rule.py 4 | # modified: 2020-02-20 5 | 6 | class Mutex(object): 7 | 8 | __slots__ = ["_cids",] 9 | 10 | def __init__(self, cids): 11 | self._cids = cids 12 | 13 | @property 14 | def cids(self): 15 | return self._cids 16 | 17 | 18 | class Delay(object): 19 | 20 | __slots__ = ["_cid","_threshold"] 21 | 22 | def __init__(self, cid, threshold): 23 | assert threshold > 0 24 | self._cid = cid 25 | self._threshold = threshold 26 | 27 | @property 28 | def cid(self): 29 | return self._cid 30 | 31 | @property 32 | def threshold(self): 33 | return self._threshold 34 | 35 | -------------------------------------------------------------------------------- /autoelective/_internal.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # filename: _internal.py 4 | # modified: 2019-09-08 5 | 6 | import os 7 | import gzip 8 | 9 | def mkdir(path): 10 | if not os.path.exists(path): 11 | os.mkdir(path) 12 | 13 | def absp(*paths): 14 | return os.path.normpath(os.path.abspath(os.path.join(os.path.dirname(__file__), *paths))) 15 | 16 | def read_list(file, encoding='utf-8-sig', **kwargs): 17 | if file.endswith('.gz'): 18 | fp = gzip.open(file, 'rt', encoding=encoding, **kwargs) 19 | else: 20 | fp = open(file, 'r', encoding=encoding, **kwargs) 21 | try: 22 | return [ line.rstrip('\n') for line in fp if not line.isspace() ] 23 | finally: 24 | fp.close() 25 | -------------------------------------------------------------------------------- /DOCKER.md: -------------------------------------------------------------------------------- 1 | # PKUAutoElective Docker Image 2 | 3 | 依赖于原项目v2.0.2(2019.09.09)版本。 4 | 5 | ## Tags 6 | 7 | 1. latest 8 | 2. monitor 9 | 10 | ## latest 11 | 12 | 包含python3,依赖库,以及项目源代码。 13 | 14 | ### 运行方法 15 | 16 | ``` bash 17 | docker run -d \ 18 | --name=pae \ 19 | -v /path/to/config/folder:/config \ 20 | yousiki/pkuautoelective:latest # 运行工具 21 | docker logs pae # 查看输出 22 | docker stop pae # 停止工具 23 | ``` 24 | 25 | ## monitor 26 | 27 | 额外包含Monitor运行依赖的库。 28 | 29 | ### 运行方法 30 | 31 | `config.ini`中的`host`值建议设为`0.0.0.0` 32 | 33 | ``` bash 34 | docker run -d \ 35 | --name=pae \ 36 | -p 7074:7074 \ 37 | -v /path/to/config/folder:/config \ 38 | yousiki/pkuautoelective:latest # 运行工具 39 | docker logs pae # 查看输出 40 | docker stop pae # 停止工具 41 | ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Check List 2 | 3 | - [ ] 我已经阅读了 [Readme](https://github.com/zhongxinghong/PKUAutoElective/blob/master/README.md), [Migration Guide](https://github.com/zhongxinghong/PKUAutoElective/blob/master/MIGRATION_GUIDE.md), [Realease History](https://github.com/zhongxinghong/PKUAutoElective/blob/master/HISTORY.md) ,但是并没有找到有用的信息 4 | - [ ] 我已经搜索了已有的 [Issues](https://github.com/zhongxinghong/PKUAutoElective/issues) ,但是没有找到相同的问题 5 | 6 | ### Version / Environment 7 | 8 | System infomation: [ ] \( Windows10 64bit, MacOS 10.13.6, Ubuntu 18.04.3 amd64, ... ) 9 | Python version: [ ] \( run `python3 --version` ) 10 | AutoElective version: [ ] \( run `python3 main.py --version` ) 11 | 12 | ### Config 13 | 14 | 除了学号/密码外的其他配置 15 | 16 | ### Issue Description 17 | 18 | #### What 19 | 20 | 遇到的问题 21 | 22 | #### Console Log 23 | 24 | 必要的终端输出信息 25 | 26 | #### Reproduce 27 | 28 | 如有必要,提供复现的步骤 29 | 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Rabbit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /autoelective/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # filename: utils.py 4 | # modified: 2019-09-09 5 | 6 | import os 7 | import pickle 8 | import gzip 9 | import hashlib 10 | from requests.compat import json 11 | 12 | 13 | def b(s): 14 | if isinstance(s, (str,int,float)): 15 | return str(s).encode("utf-8") 16 | elif isinstance(s, bytes): 17 | return s 18 | else: 19 | raise TypeError("unsupport type %s of %r" % (type(s), s)) 20 | 21 | def u(s): 22 | if isinstance(s, bytes): 23 | return s.decode("utf-8") 24 | elif isinstance(s, (str,int,float)): 25 | return str(s) 26 | else: 27 | raise TypeError("unsupport type %s of %r" % (type(s), s)) 28 | 29 | def xMD5(data): 30 | return hashlib.md5(b(data)).hexdigest() 31 | 32 | def xSHA1(data): 33 | return hashlib.sha1(b(data)).hexdigest() 34 | 35 | def json_load(file, *args, **kwargs): 36 | if not os.path.exists(file): 37 | return None 38 | with open(file, "r", encoding="utf-8-sig") as fp: 39 | try: 40 | return json.load(fp, *args, **kwargs) 41 | except json.JSONDecodeError: 42 | return None 43 | 44 | def json_dump(obj, file, *args, **kwargs): 45 | with open(file, "w", encoding="utf-8") as fp: 46 | json.dump(obj, fp, *args, **kwargs) 47 | 48 | def pickle_gzip_dump(obj, file): 49 | with gzip.open(file, "wb") as fp: 50 | pickle.dump(obj, fp) 51 | 52 | def pickle_gzip_load(file): 53 | with gzip.open(file, "rb") as fp: 54 | return pickle.load(fp) 55 | 56 | 57 | class Singleton(type): 58 | """ 59 | Singleton Metaclass 60 | @link https://github.com/jhao104/proxy_pool/blob/428359c8dada998481f038dbdc8d3923e5850c0e/Util/utilClass.py 61 | """ 62 | _inst = {} 63 | 64 | def __call__(cls, *args, **kwargs): 65 | if cls not in cls._inst: 66 | cls._inst[cls] = super(Singleton, cls).__call__(*args, **kwargs) 67 | return cls._inst[cls] 68 | -------------------------------------------------------------------------------- /autoelective/course.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # filename: course.py 4 | # modified: 2019-09-08 5 | 6 | class Course(object): 7 | 8 | __slots__ = ['_name','_class_no','_school','_status','_href','_ident'] 9 | 10 | def __init__(self, name, class_no, school, status=None, href=None): 11 | self._name = name 12 | self._class_no = int(class_no) # 确保 01 与 1 为同班号,因为表格软件将 01 视为 1 13 | self._school = school 14 | self._status = status # (maxi, used) 限选 / 已选 15 | self._href = href # 选课链接 16 | self._ident = (self._name, self._class_no, self._school) 17 | 18 | @property 19 | def name(self): 20 | return self._name 21 | 22 | @property 23 | def class_no(self): 24 | return self._class_no 25 | 26 | @property 27 | def school(self): 28 | return self._school 29 | 30 | @property 31 | def status(self): 32 | return self._status 33 | 34 | @property 35 | def href(self): 36 | return self._href 37 | 38 | @property 39 | def max_quota(self): 40 | assert self._status is not None 41 | return self._status[0] 42 | 43 | @property 44 | def used_quota(self): 45 | assert self._status is not None 46 | return self._status[1] 47 | 48 | @property 49 | def remaining_quota(self): 50 | assert self._status is not None 51 | maxi, used = self._status 52 | return maxi - used 53 | 54 | def is_available(self): 55 | assert self._status is not None 56 | maxi, used = self._status 57 | return maxi > used 58 | 59 | def to_simplified(self): 60 | return Course(self._name, self._class_no, self._school) 61 | 62 | def __eq__(self, other): 63 | if not isinstance(other, self.__class__): 64 | return False 65 | return self._ident == other._ident 66 | 67 | def __hash__(self): 68 | return hash(self._ident) 69 | 70 | def __repr__(self): 71 | if self._status is not None: 72 | return "%s(%s, %s, %s, %d / %d)" % ( 73 | self.__class__.__name__, 74 | self._name, self._class_no, self._school, *self._status, 75 | ) 76 | else: 77 | return "%s(%s, %s, %s)" % ( 78 | self.__class__.__name__, 79 | self._name, self._class_no, self._school, 80 | ) 81 | -------------------------------------------------------------------------------- /autoelective/iaaa.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # filename: iaaa.py 4 | # modified: 2019-09-10 5 | 6 | from urllib.parse import quote 7 | from .client import BaseClient 8 | from .hook import get_hooks, debug_print_request, check_status_code, check_iaaa_success 9 | from .const import IAAAURL, ElectiveURL 10 | 11 | _hooks_check_status_code = get_hooks( 12 | debug_print_request, 13 | check_status_code, 14 | ) 15 | 16 | _hooks_check_iaaa_success = get_hooks( 17 | debug_print_request, 18 | check_status_code, 19 | check_iaaa_success, 20 | ) 21 | 22 | 23 | class IAAAClient(BaseClient): 24 | 25 | default_headers = { 26 | "Accept": "application/json, text/javascript, */*; q=0.01", 27 | "Accept-Encoding": "gzip, deflate, br", 28 | "Accept-Language": "en-US,en;q=0.9", 29 | "Host": IAAAURL.Host, 30 | "Origin": "https://%s" % IAAAURL.Host, 31 | "Connection": "keep-alive", 32 | } 33 | 34 | def oauth_home(self, **kwargs): 35 | headers = kwargs.pop("headers", {}) 36 | headers["Referer"] = ElectiveURL.HomePage 37 | headers["Upgrade-Insecure-Requests"] = "1" 38 | 39 | r = self._get( 40 | url=IAAAURL.OauthHomePage, 41 | params={ 42 | "appID": "syllabus", 43 | "appName": "学生选课系统", 44 | "redirectUrl": ElectiveURL.SSOLoginRedirect, 45 | }, 46 | headers=headers, 47 | hooks=_hooks_check_status_code, 48 | **kwargs, 49 | ) 50 | return r 51 | 52 | def oauth_login(self, username, password, **kwargs): 53 | headers = kwargs.pop("headers", {}) 54 | headers["Referer"] = "%s?appID=syllabus&appName=%s&redirectUrl=%s" % ( 55 | IAAAURL.OauthHomePage, quote("学生选课系统"), ElectiveURL.SSOLoginRedirect) 56 | headers["X-Requested-With"] = "XMLHttpRequest" 57 | 58 | r = self._post( 59 | url=IAAAURL.OauthLogin, 60 | data={ 61 | "appid": "syllabus", 62 | "userName": username, 63 | "password": password, 64 | "randCode": "", 65 | "smsCode": "", 66 | "otpCode": "", 67 | "redirUrl": ElectiveURL.SSOLoginRedirect, 68 | }, 69 | headers=headers, 70 | hooks=_hooks_check_iaaa_success, 71 | **kwargs, 72 | ) 73 | return r 74 | -------------------------------------------------------------------------------- /autoelective/parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # filename: parser.py 4 | # modified: 2019-09-09 5 | 6 | import re 7 | from lxml import etree 8 | from .course import Course 9 | 10 | _regexBzfxSida = re.compile(r'\?sida=(\S+?)&sttp=(?:bzx|bfx)') 11 | 12 | 13 | def get_tree_from_response(r): 14 | return etree.HTML(r.text) # 不要用 r.content, 否则可能会以 latin-1 编码 15 | 16 | def get_tree(content): 17 | return etree.HTML(content) 18 | 19 | def get_tables(tree): 20 | return tree.xpath('.//table//table[@class="datagrid"]') 21 | 22 | def get_table_header(table): 23 | return table.xpath('.//tr[@class="datagrid-header"]/th/text()') 24 | 25 | def get_table_trs(table): 26 | return table.xpath('.//tr[@class="datagrid-odd" or @class="datagrid-even"]') 27 | 28 | def get_title(tree): 29 | title = tree.find('.//head/title') 30 | if title is None: # 双学位 sso_login 后先到 主修/辅双 选择页,这个页面没有 title 标签 31 | return None 32 | return title.text 33 | 34 | def get_errInfo(tree): 35 | tds = tree.xpath(".//table//table//table//td") 36 | assert len(tds) == 1 37 | td = tds[0] 38 | strong = td.getchildren()[0] 39 | assert strong.tag == 'strong' and strong.text in ('出错提示:', '提示:') 40 | return "".join(td.xpath('./text()')).strip() 41 | 42 | def get_tips(tree): 43 | tips = tree.xpath('.//td[@id="msgTips"]') 44 | if len(tips) == 0: 45 | return None 46 | td = tips[0].xpath('.//table//table//td')[1] 47 | return "".join(td.xpath('.//text()')).strip() 48 | 49 | def get_sida(r): 50 | return _regexBzfxSida.search(r.text).group(1) 51 | 52 | def get_courses(table): 53 | header = get_table_header(table) 54 | trs = get_table_trs(table) 55 | ixs = tuple(map(header.index, ["课程名","班号","开课单位"])) 56 | cs = [] 57 | for tr in trs: 58 | t = tr.xpath('./th | ./td') 59 | name, class_no, school = map(lambda ix: t[ix].xpath('.//text()')[0], ixs) 60 | c = Course(name, class_no, school) 61 | cs.append(c) 62 | return cs 63 | 64 | def get_courses_with_detail(table): 65 | header = get_table_header(table) 66 | trs = get_table_trs(table) 67 | ixs = tuple(map(header.index, ["课程名","班号","开课单位","限数/已选","补选"])) 68 | cs = [] 69 | for tr in trs: 70 | t = tr.xpath('./th | ./td') 71 | name, class_no, school, status, _ = map(lambda ix: t[ix].xpath('.//text()')[0], ixs) 72 | status = tuple(map(int, status.split("/"))) 73 | href = t[ixs[-1]].xpath('./a/@href')[0] 74 | c = Course(name, class_no, school, status, href) 75 | cs.append(c) 76 | return cs 77 | 78 | -------------------------------------------------------------------------------- /autoelective/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # filename: cli.py 4 | # modified: 2020-02-20 5 | 6 | from optparse import OptionParser 7 | from threading import Thread 8 | from multiprocessing import Queue 9 | from . import __version__, __date__ 10 | 11 | 12 | def create_default_parser(): 13 | 14 | parser = OptionParser( 15 | description='PKU Auto-Elective Tool v%s (%s)' % (__version__, __date__), 16 | version=__version__, 17 | ) 18 | 19 | ## custom input files 20 | 21 | parser.add_option( 22 | '-c', 23 | '--config', 24 | dest='config_ini', 25 | metavar="FILE", 26 | help='custom config file encoded with utf8', 27 | ) 28 | 29 | ## boolean (flag) options 30 | 31 | parser.add_option( 32 | '-m', 33 | '--with-monitor', 34 | dest='with_monitor', 35 | action='store_true', 36 | default=False, 37 | help='run the monitor thread simultaneously', 38 | ) 39 | 40 | return parser 41 | 42 | 43 | def setup_default_environ(options, args, environ): 44 | 45 | environ.config_ini = options.config_ini 46 | environ.with_monitor = options.with_monitor 47 | 48 | 49 | def create_default_threads(options, args, environ): 50 | 51 | # import here to ensure the singleton `config` will be init later than parse_args() 52 | from autoelective.loop import run_iaaa_loop, run_elective_loop 53 | from autoelective.monitor import run_monitor 54 | 55 | tList = [] 56 | 57 | t = Thread(target=run_iaaa_loop, name="IAAA") 58 | environ.iaaa_loop_thread = t 59 | tList.append(t) 60 | 61 | t = Thread(target=run_elective_loop, name="Elective") 62 | environ.elective_loop_thread = t 63 | tList.append(t) 64 | 65 | if options.with_monitor: 66 | t = Thread(target=run_monitor, name="Monitor") 67 | environ.monitor_thread = t 68 | tList.append(t) 69 | 70 | return tList 71 | 72 | 73 | def run(): 74 | 75 | from .environ import Environ 76 | 77 | environ = Environ() 78 | 79 | parser = create_default_parser() 80 | options, args = parser.parse_args() 81 | 82 | setup_default_environ(options, args, environ) 83 | 84 | tList = create_default_threads(options, args, environ) 85 | 86 | for t in tList: 87 | t.daemon = True 88 | t.start() 89 | 90 | # 91 | # Don't use join() to block the main thread, or Ctrl + C in Windows can't work. 92 | # 93 | # for t in tList: 94 | # t.join() 95 | # 96 | try: 97 | Queue().get() 98 | except KeyboardInterrupt as e: 99 | pass 100 | -------------------------------------------------------------------------------- /autoelective/const.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # filename: const.py 4 | # modified: 2019-09-11 5 | 6 | import os 7 | from ._internal import mkdir, absp, read_list 8 | 9 | CACHE_DIR = absp("../cache/") 10 | CAPTCHA_CACHE_DIR = absp("../cache/captcha/") 11 | LOG_DIR = absp("../log/") 12 | ERROR_LOG_DIR = absp("../log/error") 13 | REQUEST_LOG_DIR = absp("../log/request/") 14 | WEB_LOG_DIR = absp("../log/web/") 15 | 16 | CNN_MODEL_FILE = absp("../model/cnn.20210311.1.pt") 17 | USER_AGENTS_TXT_GZ = absp("../user_agents.txt.gz") 18 | USER_AGENTS_USER_TXT = absp("../user_agents.user.txt") 19 | DEFAULT_CONFIG_INI = absp("../config.ini") 20 | 21 | mkdir(CACHE_DIR) 22 | mkdir(CAPTCHA_CACHE_DIR) 23 | mkdir(LOG_DIR) 24 | mkdir(ERROR_LOG_DIR) 25 | mkdir(REQUEST_LOG_DIR) 26 | mkdir(WEB_LOG_DIR) 27 | 28 | if os.path.exists(USER_AGENTS_USER_TXT): 29 | USER_AGENT_LIST = read_list(USER_AGENTS_USER_TXT) 30 | else: 31 | USER_AGENT_LIST = read_list(USER_AGENTS_TXT_GZ) 32 | 33 | 34 | class IAAAURL(object): 35 | """ 36 | Host 37 | OauthHomePage 38 | OauthLogin 39 | """ 40 | Host = "iaaa.pku.edu.cn" 41 | OauthHomePage = "https://iaaa.pku.edu.cn/iaaa/oauth.jsp" 42 | OauthLogin = "https://iaaa.pku.edu.cn/iaaa/oauthlogin.do" 43 | 44 | 45 | class ElectiveURL(object): 46 | """ 47 | Host 48 | SSOLoginRedirect 重定向链接 49 | SSOLogin sso登录 50 | Logout 登出 51 | HelpController 选课帮助页 52 | ShowResults 选课结果页 53 | SupplyCancel 补退选页 54 | Supplement 补退选页第一页之后 55 | DrawServlet 获取一张验证码 56 | validate 补退选验证码校验接口 57 | """ 58 | Scheme = "https" 59 | Host = "elective.pku.edu.cn" 60 | HomePage = "https://elective.pku.edu.cn/elective2008/" 61 | SSOLoginRedirect = "http://elective.pku.edu.cn:80/elective2008/ssoLogin.do" 62 | SSOLogin = "https://elective.pku.edu.cn/elective2008/ssoLogin.do" 63 | Logout = "https://elective.pku.edu.cn/elective2008/logout.do" 64 | HelpController = "https://elective.pku.edu.cn/elective2008/edu/pku/stu/elective/controller/help/HelpController.jpf" 65 | ShowResults = "https://elective.pku.edu.cn/elective2008/edu/pku/stu/elective/controller/electiveWork/showResults.do" 66 | SupplyCancel = "https://elective.pku.edu.cn/elective2008/edu/pku/stu/elective/controller/supplement/SupplyCancel.do" 67 | Supplement = "https://elective.pku.edu.cn/elective2008/edu/pku/stu/elective/controller/supplement/supplement.jsp" 68 | DrawServlet = "https://elective.pku.edu.cn/elective2008/DrawServlet" 69 | Validate = "https://elective.pku.edu.cn/elective2008/edu/pku/stu/elective/controller/supplement/validate.do" 70 | -------------------------------------------------------------------------------- /autoelective/monitor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # filename: monitor.py 4 | # modified: 2019-09-11 5 | 6 | import logging 7 | import werkzeug._internal as _werkzeug_internal 8 | from flask import Flask, current_app, jsonify 9 | from flask.logging import default_handler 10 | from .environ import Environ 11 | from .config import AutoElectiveConfig 12 | from .logger import ConsoleLogger 13 | 14 | environ = Environ() 15 | config = AutoElectiveConfig() 16 | cout = ConsoleLogger("monitor") 17 | ferr = ConsoleLogger("monitor.error") 18 | 19 | monitor = Flask(__name__, static_folder=None) # disable static rule 20 | 21 | monitor.config["JSON_AS_ASCII"] = False 22 | monitor.config["JSON_SORT_KEYS"] = False 23 | 24 | _werkzeug_internal._logger = cout # custom _logger for werkzeug 25 | 26 | monitor.logger.removeHandler(default_handler) 27 | for logger in [cout, ferr]: 28 | for handler in logger.handlers: 29 | monitor.logger.addHandler(handler) 30 | 31 | 32 | @monitor.route("/", methods=["GET"]) 33 | @monitor.route("/rules", methods=["GET"]) 34 | @monitor.route("/stat", methods=["GET"], strict_slashes=False) 35 | def _root(): 36 | rules = [] 37 | for r in sorted(current_app.url_map.iter_rules(), key=lambda r: r.rule): 38 | line = "{method} {rule}".format( 39 | method=','.join( m for m in r.methods if m not in ("HEAD","OPTIONS") ), 40 | rule=r.rule 41 | ) 42 | rules.append(line) 43 | return jsonify({ 44 | "rules": rules, 45 | }) 46 | 47 | @monitor.route("/stat/loop", methods=["GET"]) 48 | def _stat_iaaa_loop(): 49 | it = environ.iaaa_loop_thread 50 | et = environ.elective_loop_thread 51 | it_alive = it is not None and it.is_alive() 52 | et_alive = et is not None and et.is_alive() 53 | finished = not it_alive and not et_alive 54 | error_encountered = not finished and ( not it_alive or not et_alive ) 55 | return jsonify({ 56 | "iaaa_loop": environ.iaaa_loop, 57 | "elective_loop": environ.elective_loop, 58 | "iaaa_loop_is_alive": it_alive, 59 | "elective_loop_is_alive": et_alive, 60 | "finished": finished, 61 | "error_encountered": error_encountered, 62 | }) 63 | 64 | @monitor.route("/stat/course", methods=["GET"]) 65 | def _stat_course(): 66 | goals = environ.goals # [course] 67 | ignored = environ.ignored # {course, reason} 68 | return jsonify({ 69 | "goals": [ str(c) for c in goals ], 70 | "current": [ str(c) for c in goals if c not in ignored ], 71 | "ignored": { str(c): r for c, r in ignored.items() }, 72 | }) 73 | 74 | @monitor.route("/stat/error", methods=["GET"]) 75 | def _stat_error(): 76 | return jsonify({ 77 | "errors": environ.errors, 78 | }) 79 | 80 | 81 | def run_monitor(): 82 | monitor.run( 83 | host=config.monitor_host, 84 | port=config.monitor_port, 85 | debug=True, 86 | use_reloader=False, 87 | ) 88 | -------------------------------------------------------------------------------- /autoelective/logger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # filename: logger.py 4 | # modified: 2019-09-09 5 | 6 | import os 7 | import logging 8 | from logging import StreamHandler 9 | from logging.handlers import TimedRotatingFileHandler 10 | from .config import AutoElectiveConfig 11 | from .const import ERROR_LOG_DIR 12 | from ._internal import mkdir 13 | 14 | config = AutoElectiveConfig() 15 | 16 | _USER_ERROR_LOG_DIR = os.path.join(ERROR_LOG_DIR, config.get_user_subpath()) 17 | mkdir(_USER_ERROR_LOG_DIR) 18 | 19 | 20 | class BaseLogger(object): 21 | 22 | default_level = logging.DEBUG 23 | default_format = logging.Formatter("[%(levelname)s] %(name)s, %(asctime)s, %(message)s", "%H:%M:%S") 24 | 25 | def __init__(self, name, level=None, format=None): 26 | if self.__class__ is __class__: 27 | raise NotImplementedError 28 | self._name = name 29 | self._level = level if level is not None else self.__class__.default_level 30 | self._format = format if format is not None else self.__class__.default_format 31 | self._logger = logging.getLogger(self._name) 32 | self._logger.setLevel(self._level) 33 | self._logger.addHandler(self._get_handler()) 34 | 35 | @property 36 | def handlers(self): 37 | return self._logger.handlers 38 | 39 | def _get_handler(self): 40 | raise NotImplementedError 41 | 42 | def log(self, level, msg, *args, **kwargs): 43 | return self._logger.log(level, msg, *args, **kwargs) 44 | 45 | def debug(self, msg, *args, **kwargs): 46 | return self._logger.debug(msg, *args, **kwargs) 47 | 48 | def info(self, msg, *args, **kwargs): 49 | return self._logger.info(msg, *args, **kwargs) 50 | 51 | def warn(self, msg, *args, **kwargs): 52 | return self._logger.warn(msg, *args, **kwargs) 53 | 54 | def warning(self, msg, *args, **kwargs): 55 | return self._logger.warning(msg, *args, **kwargs) 56 | 57 | def error(self, msg, *args, **kwargs): 58 | return self._logger.error(msg, *args, **kwargs) 59 | 60 | def exception(self, msg, *args, **kwargs): 61 | kwargs.setdefault("exc_info", True) 62 | return self._logger.exception(msg, *args, **kwargs) 63 | 64 | def fatal(self, msg, *args, **kwargs): 65 | return self._logger.fatal(msg, *args, **kwargs) 66 | 67 | def critical(self, msg, *args, **kwargs): 68 | return self._logger.critical(msg, *args, **kwargs) 69 | 70 | 71 | class ConsoleLogger(BaseLogger): 72 | """ 控制台日志输出类 """ 73 | 74 | default_level = logging.DEBUG 75 | 76 | def _get_handler(self): 77 | handler = logging.StreamHandler() 78 | handler.setLevel(self._level) 79 | handler.setFormatter(self._format) 80 | return handler 81 | 82 | 83 | class FileLogger(BaseLogger): 84 | """ 文件日志输出类 """ 85 | 86 | default_level = logging.WARNING 87 | 88 | def _get_handler(self): 89 | file = os.path.join(_USER_ERROR_LOG_DIR, "%s.log" % self._name) 90 | handler = TimedRotatingFileHandler(file, when='d', interval=1, encoding="utf-8-sig") 91 | handler.setLevel(self._level) 92 | handler.setFormatter(self._format) 93 | return handler 94 | -------------------------------------------------------------------------------- /autoelective/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # filename: client.py 4 | # modified: 2019-09-09 5 | 6 | from requests.models import Request 7 | from requests.sessions import Session 8 | from requests.cookies import extract_cookies_to_jar 9 | 10 | class BaseClient(object): 11 | 12 | default_headers = {} 13 | default_client_timeout = 10 14 | 15 | def __init__(self, *args, **kwargs): 16 | if self.__class__ is __class__: 17 | raise NotImplementedError 18 | self._timeout = kwargs.get("timeout", self.__class__.default_client_timeout) 19 | self._session = Session() 20 | self._session.headers.update(self.__class__.default_headers) 21 | 22 | @property 23 | def user_agent(self): 24 | return self._session.headers.get('User-Agent') 25 | 26 | def _request(self, method, url, 27 | params=None, data=None, headers=None, cookies=None, files=None, 28 | auth=None, timeout=None, allow_redirects=True, proxies=None, 29 | hooks=None, stream=None, verify=None, cert=None, json=None): 30 | 31 | # Extended from requests/sessions.py for '_client' kwargs 32 | 33 | req = Request( 34 | method=method.upper(), 35 | url=url, 36 | headers=headers, 37 | files=files, 38 | data=data or {}, 39 | json=json, 40 | params=params or {}, 41 | auth=auth, 42 | cookies=cookies, 43 | hooks=hooks, 44 | ) 45 | prep = self._session.prepare_request(req) 46 | prep._client = self # hold the reference to client 47 | 48 | 49 | proxies = proxies or {} 50 | 51 | settings = self._session.merge_environment_settings( 52 | prep.url, proxies, stream, verify, cert 53 | ) 54 | 55 | # Send the request. 56 | send_kwargs = { 57 | 'timeout': timeout or self._timeout, # set default timeout 58 | 'allow_redirects': allow_redirects, 59 | } 60 | send_kwargs.update(settings) 61 | resp = self._session.send(prep, **send_kwargs) 62 | 63 | return resp 64 | 65 | def _get(self, url, params=None, **kwargs): 66 | return self._request('GET', url, params=params, **kwargs) 67 | 68 | def _post(self, url, data=None, json=None, **kwargs): 69 | return self._request('POST', url, data=data, json=json, **kwargs) 70 | 71 | def set_user_agent(self, user_agent): 72 | self._session.headers["User-Agent"] = user_agent 73 | 74 | def persist_cookies(self, r): 75 | """ 76 | From requests/sessions.py, Session.send() 77 | 78 | Session.send() 方法会首先 dispatch_hook 然后再 extract_cookies_to_jar 79 | 80 | 在该项目中,对于返回信息异常的请求,在 hooks 校验时会将错误抛出,send() 之后的处理将不会执行。 81 | 遇到的错误往往是 SystemException / TipsException ,而这些客户端认为是错误的情况, 82 | 对于服务器端来说并不是错误请求,服务器端在该次请求结束后可能会要求 Set-Cookies 83 | 但是由于 send() 在 dispatch_hook 时遇到错误而中止,导致后面的 extract_cookies_to_jar 84 | 未能调用,因此 Cookies 并未更新。下一次再请求服务器的时候,就会遇到会话过期的情况。 85 | 86 | 在这种情况下,需要在捕获错误后手动更新 cookies 以确保能够保持会话 87 | 88 | """ 89 | if r.history: 90 | 91 | # If the hooks create history then we want those cookies too 92 | for resp in r.history: 93 | extract_cookies_to_jar(self._session.cookies, resp.request, resp.raw) 94 | 95 | extract_cookies_to_jar(self._session.cookies, r.request, r.raw) 96 | 97 | def clear_cookies(self): 98 | self._session.cookies.clear() 99 | -------------------------------------------------------------------------------- /MIGRATION_GUIDE_origin.md: -------------------------------------------------------------------------------- 1 | Migration Guide 2 | ==================== 3 | 4 | v5.0.1 -> 6.0.0 5 | ------------------ 6 | - 新的验证码识别模块依赖 opencv,安装命令 `pip3 install opencv-python` 7 | - 新的验证码识别模块不再依赖于 joblib,并删除了对 simplejson 的可选依赖 8 | - PyTorch 版本需保证大于 1.4.x,否则无法读取 CNN 模型 9 | 10 | v3.0.8 -> 5.0.1 11 | ------------------ 12 | - `config.ini` 中添加 `elective_client_max_life` 用于设置 elective 客户端的存活时间,到期后 elective 会话会被主动关闭 13 | - `config.ini` 中添加 `print_mutex_rules` 选项,可以选择是否打印完整的互斥规则列表 14 | 15 | v3.0.6 -> 3.0.8 16 | ------------------ 17 | - 添加了 [Issue #28](https://github.com/zhongxinghong/PKUAutoElective/issues/28) 所提的建议,引入了延迟规则的定义,如果你需要使用,请注意 [README.md](/README.md) 中 [延迟规则](/README.md#延迟规则) 小节 18 | 19 | 20 | v3.0.5 -> 3.0.6 21 | ------------------ 22 | - 修复了 [Issue #25](https://github.com/zhongxinghong/PKUAutoElective/issues/25) 所提的 bug,如果你在使用互斥规则,请你注意下 [README.md](/README.md) 中 [互斥规则](/README.md#互斥规则) 小节的更新 23 | 24 | 25 | v3.0.3 -> 3.0.5 26 | ------------------ 27 | - 现在定义在 `config.ini` 中的课程将会像原来那样保持其在文件中的先后顺序,相应的选课优先级按从上到下的顺序从高到低排 28 | 29 | 30 | v3.0.1 beta -> v3.0.3 31 | ------------------ 32 | - 旧版本的 iaaa 和 elective 的部分 API 已经失效,请赶紧更新到最新 v3.0.3 及以上版本,旧版本将不再维护 33 | 34 | 35 | v2.1.0 -> v3.0.1 beta 36 | ------------------ 37 | - 不再使用 `course.csv` 定义课程列表,而是合并到 `config.ini` 中,因此需要仔细查看手册的 [基本用法](/README.md#基本用法) 一节,以明确新的课程定义方法。最好对 `config.ini` 进行完全重写 38 | - 修改了 `config.ini` 中 `student_ID` 键名为 `student_id` 39 | - 新引入了自定义选课规则的功能,更多请查看手册的 [基本用法](/README.md#基本用法) 一节 40 | - 修改了监视器路由,详细查看手册的 [监视器](/README.md#监视器) 一节 41 | - 更多改动细节请查看 [HISTORY.md](/HISTORY.md) 并重新阅读手册 [README.md](/README.md) 42 | 43 | #### Development Related 44 | - 这版在诸多方面均有较大改动,详细请查看 [HISTORY.md](/HISTORY.md) 45 | - 这版开始我不打算再提供项目的架构细节说明,如果你需要了解项目的实现细节,可以自行阅读源码 46 | 47 | 48 | v2.0.9 -> v2.1.0 49 | ------------------ 50 | 51 | #### Development Related 52 | - Windows 在创建多进程时只能采用 `spawn` 方法,子进程创建后并不直接共享父进程已经设置好的的用户配置项,因此还需要将用户配置项 `userInfo` 在进程中共享。但是 `userInfo` 直接影响着最基本的 `config.py` ,为了让用户自定义配置 `userInfo` 能够在子进程中被正确应用,`userInfo` 的更新至子线程 `_internal.py` 和 `config` 单例的第一次初始化必须早于任何依赖于 `config` 单例的方法的调用。 53 | - 因此,这一版中对包调用的逻辑进行了大幅度的修改,删减了大部分包的在导入时即创建 `_config` 全局变量的逻辑,改成将 `config` 变量在函数使用时才创建,并且将 `loop.py` 和 `monitor.py` 中的所有全局变量和全局函数声明为局部 54 | - 个人觉得这个改动很丑陋,但是由于我的开发经验有限,一时想不到其他的写法,如果你对这个问题有更好的解决方法,欢迎发 Issue ! 55 | - 个人的一个改进想法是把多进程全部换成多线程,这样就不需要考虑资源共享的问题 56 | 57 | 58 | v2.0.6 -> v2.0.7 59 | ------------------ 60 | - monitor 进程中 `/loop` 路由细分为 `/main_loop` 和 `/login_loop` 两个路由 61 | - monitor 进程中 `/all` 路由添加了与错误捕获记录相关的键 62 | - monitor 进程中添加了 `/errors` 路由 63 | 64 | #### Development Related 65 | - 进程共享的 status 对象中的 `loop` 项细分为了 `main_loop` 和 `login_loop` 两个值,并添加了与错误捕获记录相关的键 66 | 67 | 68 | v2.0.4 -> v2.0.5 69 | ------------------ 70 | 71 | #### Development Related 72 | 73 | - 修改了错误类 `IAAANotSuccessError` 的全局错误码 74 | 75 | 76 | v2.0.3 -> v2.0.4 77 | ------------------ 78 | - `config.ini` 内添加了 elective 多会话相关的配置 79 | - `config.ini` 内删除了 `iaaa_relogin_interval` 字段 80 | 81 | #### Development Related 82 | 83 | - 为了应对选课网偶发的会话过期问题,为 elective 客户端引入了多会话机制,并删除了旧有的定时重登机制。具体见 README 中的 [运行流程](/README.md#运行流程) 小节 84 | 85 | 86 | v2.0.1 -> v2.0.2 87 | ------------------ 88 | - `config.ini` 内添加了 `client/supply_cancel_page` 值,以支持不处于选课计划第一页的课程 89 | 90 | 91 | v1.0.4 -> v2.0.1 92 | ------------------ 93 | - 新版对 `config.ini` 内的绝大多数配置项名称做了修改,需要用新提供 `config.sample.ini` 重新填写一遍配置 94 | - 添加了自定义 `config.ini` 和 `course.csv` 95 | - 添加了对 `Flask` 库的依赖,对于尚未安装该依赖的环境,还需额外运行 `pip3 install flask` 96 | 97 | #### For 'git pull' Comment 98 | 99 | 如果你使用了 `git` 命令更新该项目,在输入 `git pull origin master` 后,可能会报错 `error: Your local changes to the following files would be overwritten by merge:` ,这是因为新版删除了 `config.ini` 和 `course.*.csv` 文件,而改用 `config.sample.ini` 和 `course.*.sample.csv` 代替。只需要输入以下命令即可消除冲突: 100 | 101 | 在项目根目录下: 102 | ```console 103 | $ git checkout config.ini 104 | $ git checkout course.utf-8.csv 105 | $ git checkout course.gbk.csv 106 | ``` 107 | 108 | #### Development Related 109 | 110 | - 在 `BaseClient` 内添加了 `persist_cookies` 方法,会在 `hooks.py` 内被调用,以确保在一些特定的错误下仍然可以保持会话 111 | - 在 `elective.py` 的 `sso_login` 的请求头中添加了一个固定的无效 `Cookie` 以防止登录时报 `101` 状态码 112 | - 修改了 `IAAA` 的重新登录逻辑,由原来的遇到错误重登,变为每隔一段时间重登 113 | - 在 `loop.py` 中对 `elective.py` 的 `get_Validate` 方法的调用结果添加了一层错误捕获,以应对非 JSON 格式的响应体 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PKUAutoElective_updated 2 | 3 | 北大选课网 **补退选** 阶段自动选课小工具 v7.0.0.2 (2022.02.20) 4 | 5 | upd: 重新适配验证码,利用ddddocr库,似乎有的OCR会识别错误,不过好像没什么关系,原作者的鲁棒性好强诶 6 | 7 | ---------------------------------------------------------- 8 | 9 | 适配2022年秋季学期北大选课网环境,目前支持 `本科生(含辅双)` 和 `研究生` 选课 10 | 11 | 再次鸣谢 [zhongxinghong/PKUAutoElective](https://github.com/zhongxinghong/PKUAutoElective) 的杰出工作,这一项目使得第一周认真听课成为了可能 12 | 13 | ## 重要注意事项 14 | 15 | 特地将一些重要的说明提前写在这里,希望能得到足够的重视。 16 | 17 | 1. 不要使用过低的刷新间隔,以免对选课网服务器造成压力。建议时间间隔不小于 4 秒。(参见`refresh_interval`的参数说明) 18 | 2. 选课网存在 IP 级别的限流,**访问过于频繁可能会导致 IP 被封禁**。单个IP最多建立5个有效的会话。(参见`elective_client_pool_size`的参数说明) 19 | 3. ***不要*** 把**同班号、有空位,但是不想选**的课放入选课计划;***不要*** 在选课网遭遇突发流量的时候**拥挤选课**。否则很可能遭遇 ***未知错误*** ! 20 | 21 | ## 2021秋季学期v7.0.0.1版本 22 | 23 | 24 | ## 简单配置说明 25 | 26 | 这一部分内容简化自[zhongxinghong/PKUAutoElective/README.md](https://github.com/zhongxinghong/PKUAutoElective/blob/master/README.md),您也可以在本fork的`README_origin.md`中找到这一文件以获得详尽说明。 27 | 28 | ### 安装与环境配置 29 | 30 | #### 下载PKUAutoElective 31 | 32 | 点击右上角的 `Code -> Download ZIP` 即可下载这个项目至本地。对于 git 命令行: 33 | 34 | ```console 35 | git clone https://github.com/kylelv2000/PKUAutoElective_updated.git 36 | ``` 37 | 38 | #### 安装Python 39 | 40 | 可以从 [Python 官网](https://www.python.org/) 下载并安装。本项目的开发环境为Python 3.7.3。对于Linux系统: 41 | 42 | ```console 43 | $ apt-get install python3 44 | ``` 45 | 46 | #### 安装依赖的Packages 47 | 48 | 复制并以**管理员身份**运行命令,一次性安装运行包。示例使用**清华镜像源**进行下载加速: 49 | 50 | ```console 51 | pip3 install requests lxml Pillow ddddocr numpy flask -i https://pypi.tuna.tsinghua.edu.cn/simple 52 | ``` 53 | 54 | **或者使用requirements**,安装对应开发环境所使用包的版本 55 | 56 | ```console 57 | pip3 install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple 58 | ``` 59 | 60 | 61 | 62 | 63 | ### 基础配置与使用 64 | 65 | 1. 复制 `config.sample.ini` 文件,所得的新文件重命名为 `config.ini`。用文本编辑器打开 `config.ini` 66 | - 直接复制文件,不要新建一个文件叫 `config.ini`,然后复制粘贴内容,否则可能会遇到编码问题。 67 | 2. 配置 `[user]`,填写`student_id`和`password`,详细见注释。 68 | - 替换` = `后的数据为真实值后,请妥善保管`config.ini`文件,避免泄露个人校园网账户密码。 69 | - 如果是双学位账号,设置 `dual_degree` 为 `true`,同时需要设置登录身份 `identity`,非双学位账号两者均保持默认即可。 70 | 3. 在选课网上,将待选课程手动添加到选课网的 `补退选` 页 `选课计划` 中。 71 | - 将待选课程在 `补退选` 页 `选课计划` 列表中的 **对应页数** 作为 `supply_cancel_page`的值 。 72 | - 该项目无法检查选课计划的合理性,请自行**确保填写的课程在有名额的时候可以被选上**,以免浪费时间。 73 | 4. 配置 `[course]` 定义待选课程,详细见注释。 74 | - `[course:]` 冒号后可定义唯一的课程标识符号。 75 | - 中间请间隔一个空白行。 76 | - `name` 为补退选页面中的`课程名称`。 77 | - `class` 为补退选页面中的`班号`。 78 | - `school` 为补退选页面中的`开课单位`。 79 | 5. 如果有需要使用高级功能,请务必仔细阅读详细的项目说明 [README_orgin.md](https://github.com/Chiyun-CHU/PKUAutoElective_updated/blob/master/README_origin.md)。 80 | 6. 进入项目根目录,运行 `python3 main.py`,即可开始自动选课。 81 | 82 | 83 | ## 一点额外陈述 84 | 85 | 计算中心对抗PKUAutoElective的方法相当简单粗暴,那就是每学期稍稍修改选课网的逻辑,使得程序的代码逻辑无法得到正确的请求响应而报错,破坏程序的正常工作。 86 | 87 | 除去2021年春季学期引入动态验证码的重大更新,选课网的这些小动作实际上并不涉及复杂的代码修改,基本可以在当时的补退选第一阶段开始,服务器宕机结束后的一小时到一天内完成更新。这意味着每学期选课网补退选阶段的更新,并不能支撑到选课网真正的压力峰值,即补退选第二阶段跨院系选课开始。 88 | 89 | 感谢PKUAutoElective的原作者在2021年春季几乎以一己之力标注了大量动态验证码,并完成了卷积神经网络[zhongxinghong/PKUElectiveCaptcha2021Spring](https://github.com/zhongxinghong/PKUElectiveCaptcha2021Spring)的训练,使得验证码的识别可以继续在本地进行,不再需要按照此前的解决方法调用商业性云的API,大大降低了高达几分钱的成本并节约了长达数分钟的配置时间。 90 | 91 | 此外,由于2021年春季学期更新同时引入了新的补退选第一阶段,即现在的候补选课阶段,这使得PKUAutoElective的维护者可以提前了解本学期的补退选页面逻辑,更早地对程序进行相应的更新。 92 | 93 | 然而,随着PKUAutoElective的原作者毕业离校,失去了访问选课网进行测试的权限,这一项目的维护必须由在校的更低年级学生接手,否则随着选课网的迭代,这些代码最终将变为一些无意义的文件。目前来看,至少2021年秋季学期的选课网更新,以笔者的代码水平,还能够通过对原作者代码上小修小补完成应对。 94 | 95 | **致计算中心** 基于前面的陈述,如果计算中心想要进一步制裁PKUAutoElective以降低补退选各个阶段开始时选课网服务器的压力,请将补退选三个阶段的操作逻辑进行分离,让可用版本的发布晚于选课网的崩溃。但有一点需要着重声明,无论是原作者2021年春季学期故意在跨院系选课开始数天后才放出更新的尝试,还是2021年秋季学期候补选课开始时选课网的崩溃,都毫无疑问地证明了以下事实:PKUAutoElective并没有对选课网每学期如约而至的崩溃做出决定性的贡献。 96 | 97 | **致反感PKUAutoElective的同学** 综上所述,即使您厌恶甚至痛恨这一项目,也请以某种意义上破坏了选课公平为理由进行讨伐,而不要再以导致选课网崩溃为帽子攻击PKUAutoElective的原作者和维护者。此外,考虑到信息科学与技术的迅速发展,无论您对于这一项目的态度如何,笔者也强烈建议您尝试一下完成PKUAutoElective的配置。即使您不愿意或者不屑于使用,配置一个开源项目也是相当有意义的一件事情:它至少可以带给您一套功能基本齐全的Python环境,并且还可以为您今后学习或工作中遇到的相似情景积累宝贵经验。 98 | 99 | > ## 原作者[Rabbit](https://github.com/zhongxinghong)的停更说明 100 | > 101 | > 感谢大家两年多来对这个项目的支持!我已经大四了,这学期结束后我将毕业,那之后我将不再更新这个项目。由于测试较为困难,这个项目一直以来基本都没有接受过 PR,如果有更改和扩充功能的需要,建议你 fork 一个自己的分支。停更后该项目不会 archive,有任何问题仍然可以在 Issue 里共同讨论,如果你想宣传自己的改版后的分支,也可以在 Issue 里分享 102 | > 103 | > 小白兔 写于 2021.03.12 104 | 105 | > qaz 修改于 2023.02.19 106 | -------------------------------------------------------------------------------- /config.sample.ini: -------------------------------------------------------------------------------- 1 | # filename: config.ini 2 | # coding: utf-8 3 | 4 | [user] 5 | 6 | ; student_id string 学号 7 | ; password string 密码 8 | ; dual_degree boolean 是否为双学位账号,可选 (true, false, True, False, 1, 0) 9 | ; 住:只要你的账号在登录时需要选择 "主修/辅双" 身份,此处就需要设为 true 10 | ; identity string 双学位账号登录身份,可选 ("bzx","bfx") 对应于 "主修/辅双" 11 | 12 | student_id = 1x000xxxxx 13 | password = xxxxxxxx 14 | dual_degree = false 15 | identity = bzx 16 | 17 | [client] 18 | 19 | ; supply_cancel_page int 待刷课程处在 "补退选" 选课计划的第几页 20 | ; refresh_interval float 每次循环后的暂停时间,单位 s 21 | ; random_deviation float 偏移量分数,如果设置为 <= 0 的值,则视为 0 22 | ; iaaa_client_timeout float IAAA 客户端最长请求超时 23 | ; elective_client_timeout float elective 客户端最长请求超时 24 | ; elective_client_pool_size int 最多同时保持几个 elective 的有效会话(同一 IP 下最多为 5) 25 | ; elective_client_max_life int elvetive 客户端的存活时间,单位 s(设置为 -1 则存活时间为无限长) 26 | ; login_loop_interval float IAAA 登录线程每回合结束后的等待时间 27 | ; print_mutex_rules boolean 是否在每次循环时打印完整的互斥规则列表 28 | ; debug_print_request boolean 是否打印请求细节 29 | ; debug_dump_request boolean 是否将重要接口的请求以日志的形式记录到本地(包括补退选页、提交选课等接口) 30 | ; 31 | ; 关于刷新间隔的配置示例: 32 | ; 33 | ; refresh_interval = 8 34 | ; random_deviation = 0.2 35 | ; 36 | ; 则每两个循环的间隔时间为 8 * (1.0 ± 0.2) s 37 | 38 | supply_cancel_page = 1 39 | refresh_interval = 8 40 | random_deviation = 0.2 41 | iaaa_client_timeout = 30 42 | elective_client_timeout = 60 43 | elective_client_pool_size = 2 44 | elective_client_max_life = 600 45 | login_loop_interval = 2 46 | print_mutex_rules = true 47 | debug_print_request = false 48 | debug_dump_request = false 49 | 50 | [monitor] 51 | 52 | ; host str 53 | ; port int 54 | 55 | host = 127.0.0.1 56 | port = 7074 57 | 58 | ;---------------- course ----------------; 59 | ; 60 | ; 课程结构定义: 61 | ; 62 | ; [course:${id}] ; 用户为该课程定义的 id 63 | ; 64 | ; name = ${name} ; elective 中的 `课程名` 65 | ; class = ${class} ; elective 中的 `班号` 66 | ; school = ${school} ; elective 中的 `开课单位` 67 | ; 68 | ; 69 | ; 例如: 70 | ; 71 | ; [course:math_3] 72 | ; 73 | ; name = 集合论与图论 74 | ; class = 3 75 | ; school = 信息科学技术学院 76 | ; 77 | ; 可以解析出: 78 | ; 79 | ; id = "math_3" 80 | ; name = "集合论与图论" 81 | ; class = 3 82 | ; school = "信息科学技术学院" 83 | ; 84 | ; 85 | ; 更多例子: 86 | ; 87 | ; [course:db] 88 | ; 89 | ; name = 数据库概论 90 | ; class = 1 91 | ; school = 信息科学技术学院 92 | ; 93 | ; [course:0] 94 | ; 95 | ; name = 概率统计 (A) 96 | ; class = 1 97 | ; school = 信息科学技术学院 98 | ; 99 | ; 100 | ; 注意: 101 | ; 102 | ; 1. [course:${id}] 中可以带空格,但是不推荐 103 | ; 例如 [course: 1], [course:math 1] [ course : hello world ] 104 | ; 可以解析出: "1", "math 1", "hello world" 105 | ; 2. [course:${id}] 中不要带有 ',' 否则会在后续规则定义中引入混乱! 不接受 '\,' 转义 106 | ; 例如 [course:Hai,Alice] 是非法的,在解析时会被忽略 107 | ; 3. [course:${id}] 中可以带有 ':',但是不推荐 108 | ; 4. 该文件中课程的优先级按照从上到下的顺序从高到低排序,如果在同一循环中同时出现多个有空名额的课,会从上到下依次提交选课请求, 109 | ; 高优先级的课会先被提交,例如上述案例中,数据库概率比概率统计(A)的优先级高,如果这两个课同时出现空名额,会先提交数据库 110 | ; 概率的选课请求 111 | ; 112 | ;----------------------------------------; 113 | 114 | ; [course:sample] 115 | ; 116 | ; name = class_name_here 117 | ; class = class_no_here 118 | ; school = class_school_here 119 | 120 | ;---------------- mutex ----------------; 121 | ; 122 | ; 互斥规则结构定义: 123 | ; 124 | ; [mutex:${id}] ; 用户为该互斥规则定义的 id 125 | ; 126 | ; courses = ${cid1},${cid2},... ; 用户定义的多个课程的 id,以 ',' 分隔 127 | ; 128 | ; 129 | ; 例如: 130 | ; 131 | ; [course:math_1] 132 | ; ... 133 | ; 134 | ; [course:math_2] 135 | ; ... 136 | ; 137 | ; [course:math_3] 138 | ; ... 139 | ; 140 | ; 141 | ; [mutex:0] 142 | ; 143 | ; courses = math_1,math_2,math_3 144 | ; 145 | ; 可以解析出 146 | ; 147 | ; id = "0" 148 | ; courses = ["math_1", "math_2", "math_3"] 149 | ; 150 | ; 151 | ; 解释: 152 | ; 153 | ; 同一个互斥规则内的课程一旦有一门课已经被选上,其他课程将会被自动忽略。 154 | ; 例如,对于上述例子,如果 math_1, math_2, math_3 有任何一门课已经被选上,其它两门课将会被自动忽略 155 | ; 例如,当 math_1 被选上时,math_2, math_3 会被自动忽略 156 | ; 157 | ; 158 | ; 注意: 159 | ; 160 | ; 1. [mutex:${id}] 的命名注意事项同 course 161 | ; 2. courses 中可以有空格,但是不推荐 162 | ; 例如 courses = math_1, math_2 , math_3 163 | ; 仍可以解析出 ["math_1", "math_2", "math_3"] 164 | ; 3. 如果互斥的几门课在同一回合内同时出现空位,优先级高的课会被首先提交,而优先级低的课会被忽略, 165 | ; 关于课程优先级的概念,参看 [course] 下的相关注释 166 | ; 167 | ;---------------------------------------; 168 | 169 | ; [mutex:sample] 170 | ; 171 | ; courses = course_id_1,course_id_2 172 | 173 | ;---------------- delay ----------------; 174 | ; 175 | ; 延迟规则结构定义: 176 | ; 177 | ; [delay:${id}] ; 用户为该延迟规则定义的 id 178 | ; 179 | ; course = ${course} ; 用户定义的课程的 id 180 | ; threshold = ${threshold} ; 触发选课的剩余名额的阈值,剩余名额小于等于该值的时候才会触发选课 181 | ; 182 | ; 183 | ; 例如: 184 | ; 185 | ; [course:math_1] 186 | ; ... 187 | ; 188 | ; [delay:0] 189 | ; 190 | ; course = math_1 191 | ; threshold = 10 192 | ; 193 | ; 可以解析出 194 | ; 195 | ; id = "0" 196 | ; course = "math_1" 197 | ; threshold = 10 198 | ; 199 | ; 200 | ; 解释: 201 | ; 202 | ; 被定义了延迟规则的课程,即使当回合它可以被选上时,也只有当该课程的剩余名额数小于等于设定的阈值时才会触发提交选课, 203 | ; 例如,对于上述例子,假设 math_1 的总名额是 240 人,如果当回合 math_1 的选课状况是 229/240,将不会 204 | ; 触发选课,因为剩余名额 = 240 - 229 = 11 > 10,而如果当回合 math_1 的选课状态是 230/240,将会触发选课, 205 | ; 因为剩余名额 = 240 - 230 = 10 <= 10,同理,诸如 235/240 这样的状态也会触发选课 206 | ; 207 | ; 208 | ; 注意: 209 | ; 1. [delay:${id}] 的命名注意事项同 course 210 | ; 2. threshold 必须是正整数,否则会报错 211 | ; 3. 使用前请务必查看 README.md 中与之相关的说明 212 | ; 213 | ;---------------------------------------; 214 | 215 | ; [delay:sample] 216 | ; 217 | ; course = course_id_1 218 | ; threshold = a_positive_int 219 | -------------------------------------------------------------------------------- /HISTORY_origin.md: -------------------------------------------------------------------------------- 1 | Release History 2 | =============== 3 | 4 | v6.0.0 (2021-03-12) 5 | ------------------- 6 | - 实现了对选课网新验证码的识别 7 | - 更新了对选课网部分 API 的请求方法 (get_supplement, get_Validate) 8 | - 修复了无法获取到位于补退选列表第一页之后课程的问题,详见 [Issue #54](https://github.com/zhongxinghong/PKUAutoElective/issues/54), [Issue #55](https://github.com/zhongxinghong/PKUAutoElective/issues/55) 9 | - 修复了无法正确捕获会话超时的错误信息导致一直报错 UnexceptedHTMLFormat 的问题,详见 [Issue #56](https://github.com/zhongxinghong/PKUAutoElective/issues/56) 10 | - 修复了无法正确识别补选课程成功时所返回的提示信息的问题 11 | 12 | 13 | v5.0.1 (2020-09-25) 14 | ------------------- 15 | - User-Agent 池的大小扩充至 6000+ 16 | - sso_login 时将使用随机生成的 dummy cookie 17 | - 删除所有 `Sec-Fetch-*` 请求头 18 | - 现在 elective 客户端可以设置存活时间,到期后 elective 会话将自动登出,以期解决 [Issue #47](https://github.com/zhongxinghong/PKUAutoElective/issues/47) 19 | - 每次重新登录 elective 时都将更换 User-Agent 20 | - 现在可以选择是否打印完整的互斥规则列表 21 | - 修正了配置文件和用户手册中关于 `supply_cancel_page` 选项的说明 22 | - 修改配置文件中 `elective_client_pool_size` 选项的默认值为 `2` 23 | - 程序启动时将首先打印重要的配置信息 24 | - 对 elective 可能返回的错误页面加以捕获,以期解决 [Issue #44](https://github.com/zhongxinghong/PKUAutoElective/issues/44) 25 | - 修改 sso_login 接口的参数名 `rand` 为 `_rand` 26 | - 更改识别 elective 响应结果的正则表达式,以确保包含空格的课程名也能被正确解析 27 | 28 | 29 | v4.0.1 (2020-05-30) 30 | ------------------- 31 | - 修复了 IAAA 登录报 500 状态码的问题,详见 [Issue #35](https://github.com/zhongxinghong/PKUAutoElective/issues/35) 32 | 33 | 34 | v3.0.9 (2020-02-20) 35 | ------------------- 36 | - 对相传可能出现的莫名其妙退课的情况做了防护,详见 [Issue #30](https://github.com/zhongxinghong/PKUAutoElective/issues/30) 37 | 38 | 39 | v3.0.8 (2020-02-20) 40 | ------------------- 41 | - 在 elective 两个刷新接口的 headers 中添加了 `Cache-Control: max-age=0` 42 | - 现在可以为课程定义延迟规则,详见 [Issue #28](https://github.com/zhongxinghong/PKUAutoElective/issues/28) 43 | - 修改了部分代码风格 44 | 45 | 46 | v3.0.7 (2020-02-18) 47 | ------------------- 48 | - 现在可以识别出因一学期选多门体育课而收到的来自选课网的错误提示 49 | - 同一回合中出现多门可选的课,并且低优先级待选的课与高优先级已选的课因为 mutex rules 冲突,那么低优先级的课将会被提前忽略,详见 [Issue #25](https://github.com/zhongxinghong/PKUAutoElective/issues/25) 50 | 51 | 52 | v3.0.6 (2020-02-18) 53 | ------------------- 54 | - 修正了 `config.ini` 注释中把 `班号` 写成 `课号` 的笔误 55 | - 选课网有的时候会突然显示某门课的已选人数为 0,而实际选课人数已满,此时会报一个 `Unknown tips` 的异常,现在程序可以对这种情况做出识别 56 | 57 | 58 | v3.0.5 (2020-02-17) 59 | ------------------- 60 | - 现在通过 `config.ini` 定义的课程列表可以像原来那样保持其在文件中的先后顺序,如果在同一循环中遇到同一列表中有多个课可选,将会按照从上往下的顺序依次提交 61 | - 现在会捕获 `Ctrl + C` 在 `main.py` 中引发的 `KeyboardInterrupt`,这样 `Ctrl + C` 将不会再打印 traceback 而是正常退出 62 | 63 | 64 | v3.0.4 (2020-02-17) 65 | ------------------- 66 | - 修改了 `TypeError: argmax() got an unexcepted keyword argument 'axis'` 的错误 67 | 68 | 69 | v3.0.3 (2020-02-17) 70 | ------------------- 71 | - 修改了 iaaa 和 elective 相关接口的请求细节,包括更换某些 url,修改 headers,修改替换 scheme 为 https 等 72 | - 修复了相同 Course 调用 `__eq__` 和 `__hash__` 得到不同值的 bug 73 | - 修复了 `assert self._status is not None` 引发的 `AssertionError` 74 | - 修复了 `mutexes` 在无规则时仍然 print 列表的 bug 75 | 76 | 77 | v3.0.2 beta (2020-02-17) 78 | ------------------- 79 | - 修复了 Windows 下 `Ctrl + C` 失效的问题 80 | 81 | 82 | v3.0.1 beta (2020-02-17) 83 | ------------------- 84 | - 改用 pytorch 训练的 CNN 模型进行验证码识别,提高了识别的准确率 85 | - 优化了验证码图像处理函数的执行效率 86 | - 将多进程架构重写为多线程架构,监控进程现在变为和主进程下的一个子线程 87 | - 允许自定义 User-Agent 列表 88 | - 配置文件中 `student_ID` 键名改成 `student_id` 89 | - 不再使用 `course.csv` 文件配置课程列表,而是统一归入 `config.ini` 中 90 | - 允许用户自定义互斥规则,详见 [Issue #8](https://github.com/zhongxinghong/PKUAutoElective/issues/8) 91 | - 重新设计了 monitor 的路由 92 | - 现在 monitor 不会在 iaaa_loop / elective_loop 正常退出的时候自动退出 93 | - 修改了多处代码风格和设计细节,删除了大量冗余设计 94 | 95 | 96 | v2.1.1 (2019-09-13) 97 | ------------------- 98 | - 修复了 `OperationFailedError` 使用错误的父类派生而导致不能正常初始化的问题 99 | 100 | 101 | v2.1.0 (2019-09-13) 102 | ------------------- 103 | - 修复了 Windows 下自定义参数不生效的问题 104 | 105 | 106 | v2.0.9 (2019-09-12) 107 | ------------------- 108 | - 对 v2.0.8 版本的完善,现在删除了与 `signal` 相关的逻辑,统一了两种运行模式下主进程退出的方式,确保了 `Ctrl + C` 的信号和子进程内部发出的终止信号均能使主进程正常退出 109 | 110 | 111 | v2.0.8 (2019-09-11) 112 | ------------------- 113 | - 对 v2.0.6 版本的完善,该版本在不带 `--with-monitor` 运行的情况下,也可以正确地接收到来自 `Ctrl + C` 的终止命令 114 | 115 | 116 | v2.0.7 (2019-09-11) 117 | ------------------- 118 | - 为 monitor 添加了与错误捕获记录相关的路由 119 | 120 | 121 | v2.0.6 (2019-09-11) 122 | ------------------- 123 | - 修复了在 Windows 下 `Ctrl + C` 无法退出程序的问题 124 | 125 | 126 | v2.0.5 (2019-09-11) 127 | ------------------- 128 | - 可以捕获 IAAA 登录时的密码错误和多次登录失败导致账号已被封禁的错误 129 | - 完善了对多进程/线程下进程死亡的处理,以确保主进程在遇到错误时可以完全退出 130 | - 现在 monitor 进程会在 loop 进程结束后自动退出 131 | 132 | 133 | v2.0.4 (2019-09-10) 134 | ------------------- 135 | - elective 客户端采用多会话机制 136 | 137 | 138 | v2.0.3 (2019-09-09) 139 | ------------------- 140 | - 可以捕获来自 IAAA 的错误 141 | - 丰富了部分错误的提示信息 142 | 143 | 144 | v2.0.2 (2019.09.09) 145 | ------------------- 146 | - 添加了对处于选课计划第一页之后的课程的支持 147 | 148 | 149 | v2.0.1 (2019.09.09) 150 | ------------------- 151 | - 代码重构,删减大量冗余设计 152 | - 新增监视器进程,开启后可以通过特定端口监听运行状态 153 | - 添加多账号支持,去除 cookies / token 本地共享的逻辑,并可以手动指定 config.ini / course.csv 文件的路径 154 | - 修复了在一些情况下会话无法保持的错误 155 | - 可以捕获几个新遇到的系统异常/错误提示 156 | - 美化了终端的输出格式 157 | 158 | 159 | v1.0.4 (2019.02.22) 160 | ------------------- 161 | - 修复了一处语法错误,位于 **main.py** 第 216-235 行的 `ignored.append` 处 162 | - 纠正了一些变量名的拼写错误 163 | - 可以捕获多选英语课引起的错误 164 | 165 | 166 | v1.0.3 (2019.02.20) 167 | ------------------- 168 | - 兼容了本科生辅双的登录界面,主修身份选课测试通过,辅双身份选课支持第一页 169 | - 可以捕获共享回话引起的系统异常 170 | - 可以捕获辅双登录无验证信息的系统异常 171 | 172 | 173 | v1.0.2 (2019.02.19) 174 | ------------------- 175 | - 研究生选课测试通过 176 | - 兼容了部分页面没有 `.//head/title` 标签的情况 177 | - 修改 `Course` 类的 `classNo` 属性为 int 类型,确保 `01` 与 `1` 为同班号 178 | - 主程序开始的第一个循环回合更改为首先主动登录一次,以免旧缓存导致无法切换账号 179 | - 重新登录时会率先删除客户端已有的 cookies ,修复了一次重新登录需要花费两回合的问题 180 | - 更改单一 `User-Agent` 为 `User-Agent` 池 181 | - 可以捕获课程互斥引起的错误提示 182 | 183 | 184 | v1.0.1 (2019.02.18) 185 | ------------------- 186 | - 上线版本,支持非辅双本科生选课 187 | 188 | -------------------------------------------------------------------------------- /autoelective/exceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # filename: exceptions.py 4 | # modified: 2019-09-13 5 | 6 | __all__ = [ 7 | 8 | "AutoElectiveException", 9 | 10 | "UserInputException", 11 | 12 | "AutoElectiveClientException", 13 | 14 | "StatusCodeError", 15 | "ServerError", 16 | "OperationFailedError", 17 | "UnexceptedHTMLFormat", 18 | 19 | "IAAAException", 20 | "IAAANotSuccessError", 21 | "IAAAIncorrectPasswordError", 22 | "IAAAForbiddenError", 23 | 24 | "ElectiveException", 25 | 26 | "SystemException", 27 | "CaughtCheatingError", 28 | "InvalidTokenError", 29 | "SessionExpiredError", 30 | "NotInOperationTimeError", 31 | "CourseIndexError", 32 | "CaptchaError", 33 | "NoAuthInfoError", 34 | "SharedSessionError", 35 | "NotAgreedToSelectionAgreement", 36 | 37 | "TipsException", 38 | "ElectionSuccess", 39 | "ElectionRepeatedError", 40 | "TimeConflictError", 41 | "OperationTimeoutError", 42 | "ElectionPermissionError", 43 | "ElectionFailedError", 44 | "CreditsLimitedError", 45 | "MutexCourseError", 46 | "MultiEnglishCourseError", 47 | "ExamTimeConflictError", 48 | "QuotaLimitedError", 49 | "MultiPECourseError", 50 | 51 | ] 52 | 53 | 54 | class AutoElectiveException(Exception): 55 | """ Abstract Exception for AutoElective """ 56 | 57 | class UserInputException(AutoElectiveException, ValueError): 58 | """ 由于用户的输入数据不当而引发的错误 """ 59 | 60 | 61 | class AutoElectiveClientException(AutoElectiveException): 62 | 63 | code = -1 64 | desc = "AutoElectiveException" 65 | 66 | def __init__(self, *args, **kwargs): 67 | response = kwargs.pop("response", None) 68 | self.response = response 69 | msg = "[%d] %s" % ( 70 | self.__class__.code, 71 | kwargs.pop("msg", self.__class__.desc) 72 | ) 73 | super().__init__(msg, *args, **kwargs) 74 | 75 | 76 | class StatusCodeError(AutoElectiveClientException): 77 | code = 101 78 | desc = "response.status_code != 200" 79 | 80 | def __init__(self, *args, **kwargs): 81 | r = kwargs.get("response") 82 | if r is not None and "msg" not in kwargs: 83 | kwargs["msg"] = "%s. response status code: %s" % (self.__class__.code, r.status_code) 84 | super().__init__(*args, **kwargs) 85 | 86 | class ServerError(AutoElectiveClientException): 87 | code = 102 88 | desc = r"response.status_code in (500, 501, 502, 503)" 89 | 90 | def __init__(self, *args, **kwargs): 91 | r = kwargs.get("response") 92 | if r is not None and "msg" not in kwargs: 93 | kwargs["msg"] = "%s. response status_code: %s" % (self.__class__.code, r.status_code) 94 | super().__init__(*args, **kwargs) 95 | 96 | class OperationFailedError(AutoElectiveClientException): 97 | code = 103 98 | desc = r"some operations failed for unknown reasons" 99 | 100 | class UnexceptedHTMLFormat(AutoElectiveClientException): 101 | code = 104 102 | desc = r"unable to parse HTML content" 103 | 104 | 105 | class IAAAException(AutoElectiveClientException): 106 | code = 200 107 | desc = "IAAAException" 108 | 109 | 110 | class IAAANotSuccessError(IAAAException): 111 | code = 210 112 | desc = "response.json()['success'] == False" 113 | 114 | def __init__(self, *args, **kwargs): 115 | r = kwargs.get("response") 116 | if r is not None and "msg" not in kwargs: 117 | kwargs["msg"] = "%s. response JSON: %s" % (self.__class__.code, r.json()) 118 | super().__init__(*args, **kwargs) 119 | 120 | class IAAAIncorrectPasswordError(IAAANotSuccessError): 121 | code = 211 122 | desc = "User ID or Password is incorrect" 123 | 124 | class IAAAForbiddenError(IAAANotSuccessError): 125 | code = 212 126 | desc = "You are FORBIDDEN. Please sign in after a half hour" 127 | 128 | 129 | class ElectiveException(AutoElectiveClientException): 130 | code = 300 131 | desc = "ElectiveException" 132 | 133 | 134 | class SystemException(ElectiveException): 135 | code = 310 136 | desc = "