├── .github └── screenshot.png ├── README.md ├── .gitignore ├── main.py ├── LICENSE └── seatkiller.py /.github/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeliudev/SeatKiller/HEAD/.github/screenshot.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SeatKiller 2 | 3 | [![License](https://img.shields.io/badge/license-MPL--2.0-red.svg)](LICENSE) 4 | [![Language](https://img.shields.io/badge/python-3.6-blue.svg)](https://www.python.org/) 5 | 6 | 本工具仅供学习交流,因使用本工具而造成的一切不良后果由使用者自行承担,与作者无关 7 | 8 | 该仓库已停止维护,所有功能已移植到了用 C# 重写的 GUI 版本:[SeatKiller-GUI](https://github.com/yeliudev/SeatKiller-GUI) 9 | 10 | ## 功能列表 11 | 12 | * 获取用户的信息,包括姓名、当前状态(未入馆、已进入某分馆)和累计违约次数 13 | * 晚上 22:15 定时抢座(可自行选择是否指定区域、座位号) 14 | * 检测是否已有有效预约,区分状态(预约、履约中、暂离),并可取消或释放座位 15 | * 预约成功后连接邮件服务器发送邮件提醒 16 | * 捡漏模式可用于抢当天座位 17 | * 改签模式可用于在已有有效预约的情况下,更换座位或改签预约时间(若空闲),确保新座位可用的情况下再释放原座位,防止座位丢失 18 | 19 | ## 使用方法 20 | 21 | 1. 配置 [Python3](https://www.python.org/) 运行环境 22 | 2. Terminal 或 CMD 执行 `pip install requests` 安装 requests 库 23 | 3. 运行 `main.py` ,按提示输入相关参数,即可实现自动抢座 24 | 25 | ## 软件截图 26 | 27 |

28 |  screenshot 29 |

30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | /__pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .vscode/ 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 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 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # Environments 83 | .env 84 | .venv 85 | env/ 86 | venv/ 87 | ENV/ 88 | 89 | # Spyder project settings 90 | .spyderproject 91 | .spyproject 92 | 93 | # Rope project settings 94 | .ropeproject 95 | 96 | # mkdocs documentation 97 | /site 98 | 99 | # mypy 100 | .mypy_cache/ 101 | *.xml 102 | 103 | .idea/ 104 | ._main.py 105 | ._SeatKiller.py 106 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Ye Liu. All rights reserved. 2 | 3 | import datetime 4 | import getpass 5 | import random 6 | import re 7 | import sys 8 | import time 9 | import warnings 10 | 11 | import seatkiller 12 | 13 | XT_LITE = ('9', '11', '8', '10', '6', '7', '16') 14 | XT = ('6', '7', '8', '9', '10', '11', '12', '16', '4', '5', '14', '15') 15 | GT = ('19', '29', '31', '32', '33', '34', '35', '37', '38') 16 | YT = ('20', '21', '23', '24', '26', '27') 17 | ZT = ('39', '40', '51', '52', '56', '59', '60', '61', '62', '65', '66') 18 | 19 | if __name__ == '__main__': 20 | warnings.filterwarnings('ignore') 21 | enableLoop = True 22 | exchange = False 23 | 24 | while True: 25 | username = input('请输入学号:') 26 | password = getpass.getpass('请输入图书馆密码:') 27 | 28 | SK = seatkiller.SeatKiller(username, password) 29 | 30 | if SK.get_token(): 31 | SK.get_user_info() 32 | break 33 | 34 | print('') 35 | 36 | while True: 37 | res = SK.check_res_info() 38 | if res == 'using': 39 | if input('\n是否释放此座位(1.是 2.否):') == '1': 40 | if not SK.stop_using(): 41 | print('\n释放座位失败,请稍等后重试') 42 | enableLoop = False 43 | break 44 | else: 45 | enableLoop = False 46 | break 47 | elif res: 48 | if input('\n是否取消预约此座位(1.是 2.否):') == '1': 49 | if not SK.cancel_res(res): 50 | print('\n预约取消失败,请稍等后重试') 51 | enableLoop = False 52 | break 53 | else: 54 | enableLoop = False 55 | break 56 | else: 57 | break 58 | 59 | while True: 60 | buildingId = input('\n请输入分馆编号(1.信息科学分馆 2.工学分馆 3.医学分馆 4.总馆):') 61 | if buildingId == '1': 62 | rooms = XT 63 | if input('若抢到的座位位于\'一楼3C创客空间\',是否尝试换座(1.是 2.否):') == '1': 64 | exchange = True 65 | break 66 | elif buildingId == '2': 67 | rooms = GT 68 | elif buildingId == '3': 69 | rooms = YT 70 | break 71 | elif buildingId == '4': 72 | rooms = ZT 73 | break 74 | else: 75 | print('分馆编号输入不合法') 76 | 77 | while True: 78 | startTime = input('请输入开始时间(以分钟为单位,从0点开始计算,以半小时为间隔):') 79 | if startTime in map(str, range(480, 1321, 30)): 80 | break 81 | else: 82 | print('开始时间输入不合法') 83 | 84 | while True: 85 | endTime = input('请输入结束时间(以分钟为单位,从0点开始计算,以半小时为间隔):') 86 | if endTime in map(str, range(int(startTime), 1351, 30)): 87 | break 88 | else: 89 | print('结束时间输入不合法') 90 | 91 | mail_addr = r'^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+){0,4}@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+){0,4}$' # noqa 92 | 93 | while True: 94 | SK.to_addr = input('请输入邮箱地址,抢座成功之后将发送邮件提醒(若不需要邮件提醒,此项可放空):') 95 | if not SK.to_addr: 96 | print('未输入邮箱地址,将不发送邮件提醒') 97 | SK.to_addr = '' 98 | break 99 | elif re.match(mail_addr, SK.to_addr): 100 | print('邮箱地址正确,可以发送邮件提醒') 101 | break 102 | else: 103 | print('邮箱地址有误') 104 | 105 | if enableLoop: 106 | if input('是否进入捡漏模式(1.是 2.否):') == '1': 107 | response = SK.loop(buildingId, rooms, startTime, endTime) 108 | if response[0] in map(str, range(10)) and exchange: 109 | SK.exchange_loop('1', XT_LITE, startTime, endTime, response) 110 | sys.exit() 111 | else: 112 | if input('是否进入改签模式(1.是 2.否):') == '1': 113 | SK.exchange_loop(buildingId, rooms, startTime, endTime, res) 114 | sys.exit() 115 | 116 | if buildingId == '1': 117 | roomId = input('已获取区域列表:\n' 118 | '4.一楼3C创客空间\n' 119 | '5.一楼创新学习讨论区\n' 120 | '6.二楼西自然科学图书借阅区\n' 121 | '7.二楼东自然科学图书借阅区\n' 122 | '8.三楼西社会科学图书借阅区\n' 123 | '9.四楼西图书阅览区\n' 124 | '10.三楼东社会科学图书借阅区\n' 125 | '11.四楼东图书阅览区\n' 126 | '12.三楼自主学习区\n' 127 | '14.3C创客-双屏电脑(20台)\n' 128 | '15.创新学习-MAC电脑(12台)\n' 129 | '16.创新学习-云桌面(42台)\n' 130 | '请输入房间编号(若由系统自动选择请输入\'0\'):') 131 | elif buildingId == '2': 132 | roomId = input('已获取区域列表:\n' 133 | '19.201室-东部自科图书借阅区\n' 134 | '29.2楼-中部走廊\n' 135 | '31.205室-中部电子阅览室笔记本区\n' 136 | '32.301室-东部自科图书借阅区\n' 137 | '33.305室-中部自科图书借阅区\n' 138 | '34.401室-东部自科图书借阅区\n' 139 | '35.405室中部期刊阅览区\n' 140 | '37.501室-东部外文图书借阅区\n' 141 | '38.505室-中部自科图书借阅区\n' 142 | '请输入区域编号(若由系统自动选择请输入\'0\'):') 143 | elif buildingId == '3': 144 | roomId = input('已获取区域列表:\n' 145 | '20.204教学参考书借阅区\n' 146 | '21.302中文科技图书借阅B区\n' 147 | '23.305科技期刊阅览区\n' 148 | '24.402中文文科图书借阅区\n' 149 | '26.502外文图书借阅区\n' 150 | '27.506医学人文阅览区\n' 151 | '请输入房间编号(若由系统自动选择请输入\'0\'):') 152 | else: 153 | roomId = input('已获取区域列表:\n' 154 | '39.A1-座位区\n' 155 | '40.C1自习区\n' 156 | '51.A2\n' 157 | '52.A3\n' 158 | '56.B3\n' 159 | '59.B2\n' 160 | '60.A4\n' 161 | '61.A5\n' 162 | '62.A1-沙发区\n' 163 | '65.B1\n' 164 | '66.A1-苹果区\n' 165 | '请输入房间编号(若由系统自动选择请输入\'0\'):') 166 | 167 | if roomId == '0': 168 | seatId = '0' 169 | else: 170 | date = datetime.date.today() 171 | date = date.strftime('%Y-%m-%d') 172 | if SK.get_seats(roomId, date): 173 | seatName = input('请输入座位ID(范围:' + min(SK.allSeats.keys()) + '~' + 174 | max(SK.allSeats.keys()) + ' 若由系统自动选择请输入\'0\'):') 175 | seatId = SK.allSeats.get(seatName) 176 | else: 177 | print('座位ID输入有误,将由系统自动选择') 178 | seatId = '0' 179 | 180 | while True: 181 | try_booking = True 182 | if datetime.datetime.now() < datetime.datetime.replace( 183 | datetime.datetime.now(), hour=22, minute=44, second=40): 184 | print( 185 | '\n------------------------准备获取token------------------------') 186 | SK.wait(22, 44, 40) 187 | else: 188 | print( 189 | '\n------------------------开始获取token------------------------') 190 | date = datetime.date.today() + datetime.timedelta(days=1) 191 | date = date.strftime('%Y-%m-%d') 192 | print('\ndate:' + date) 193 | 194 | if SK.get_token(): 195 | SK.get_buildings() 196 | SK.get_rooms(buildingId) 197 | if roomId != '0': 198 | SK.get_seats(roomId, date) 199 | 200 | if datetime.datetime.now() < datetime.datetime.replace( 201 | datetime.datetime.now(), hour=22, minute=45, second=0): 202 | SK.wait(22, 45, 0) 203 | elif datetime.datetime.now() > datetime.datetime.replace( 204 | datetime.datetime.now(), hour=23, minute=45, second=0): 205 | print('\n预约系统开放时间已过,准备进入捡漏模式') 206 | SK.wait(0, 59, 59, nextDay=True) 207 | response = SK.loop(buildingId, rooms, startTime, endTime) 208 | if response[0] in map(str, range(10)) and exchange: 209 | SK.exchange_loop('1', XT_LITE, startTime, endTime, 210 | response) 211 | sys.exit() 212 | print('\n------------------------开始预约次日座位------------------------') 213 | while try_booking: 214 | if seatId != '0': 215 | if SK.book_seat(seatId, date, startTime, 216 | endTime) not in ('Failed', 217 | 'Connection lost'): 218 | break 219 | else: 220 | print('\n指定座位预约失败,尝试检索其他空位...') 221 | seatId = '0' 222 | elif datetime.datetime.now() < datetime.datetime.replace( 223 | datetime.datetime.now(), hour=23, minute=45, second=0): 224 | SK.freeSeats = [] 225 | if roomId == '0': 226 | for i in rooms: 227 | if SK.search_free_seat( 228 | buildingId, i, date, startTime, 229 | endTime) == 'Connection lost': 230 | print('\n连接丢失,30秒后尝试继续检索空位') 231 | time.sleep(30) 232 | else: 233 | print('\n尝试检索同区域其他座位...') 234 | if SK.search_free_seat(buildingId, roomId, date, 235 | startTime, 236 | endTime) != 'Success': 237 | print('\n当前区域暂无空位,尝试全馆检索空位...') 238 | for i in rooms: 239 | if SK.search_free_seat( 240 | buildingId, i, date, startTime, 241 | endTime) == 'Connection lost': 242 | print('\n连接丢失,30秒后尝试继续检索空位') 243 | time.sleep(30) 244 | 245 | if not SK.freeSeats: 246 | print('\n当前全馆暂无空位,3-5秒后尝试继续检索空位') 247 | time.sleep(random.uniform(3, 5)) 248 | continue 249 | 250 | for freeSeatId in SK.freeSeats: 251 | response = SK.book_seat(freeSeatId, date, startTime, 252 | endTime) 253 | if response == 'Success': 254 | try_booking = False 255 | break 256 | elif response[0] in map(str, range(10)) and exchange: 257 | SK.exchange_loop( 258 | '1', 259 | XT_LITE, 260 | startTime, 261 | endTime, 262 | response, 263 | nextDay=True) 264 | try_booking = False 265 | break 266 | elif response[0] in map(str, 267 | range(10)) and not exchange: 268 | try_booking = False 269 | break 270 | elif response == 'Failed': 271 | time.sleep(random.uniform(1, 3)) 272 | else: 273 | ddl = datetime.datetime.replace( 274 | datetime.datetime.now(), 275 | hour=23, 276 | minute=45, 277 | second=0) 278 | delta = ddl - datetime.datetime.now() 279 | print('\n连接丢失,1分钟后重新尝试抢座,系统开放时间剩余' + 280 | str(delta.seconds) + '秒\n') 281 | time.sleep(60) 282 | else: 283 | print('\n抢座失败,座位预约系统已关闭,开始尝试捡漏') 284 | SK.wait(0, 59, 59, nextDay=True) 285 | response = SK.loop(buildingId, rooms, startTime, endTime) 286 | if response[0] in map(str, range(10)) and exchange: 287 | SK.exchange_loop('1', XT_LITE, startTime, endTime, 288 | response) 289 | print('\n抢座运行结束,2小时后进入下一轮循环') 290 | time.sleep(7200) 291 | else: 292 | print('\n登录失败,等待5秒后重试') 293 | time.sleep(5) 294 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /seatkiller.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Ye Liu. All rights reserved. 2 | 3 | import datetime 4 | import json 5 | import random 6 | import socket 7 | import sys 8 | import time 9 | 10 | import requests 11 | 12 | API_ROOT = 'https://seat.lib.whu.edu.cn:8443/rest/' 13 | 14 | LOGIN_URL = API_ROOT + 'auth' 15 | USER_URL = API_ROOT + 'v2/user' 16 | FILTERS_URL = API_ROOT + 'v2/free/filters' 17 | STATS_URL = API_ROOT + 'v2/room/stats2/' 18 | LAYOUT_URL = API_ROOT + 'v2/room/layoutByDate/' 19 | SEARCH_URL = API_ROOT + 'v2/searchSeats/' 20 | STARTTIME_URL = API_ROOT + 'v2/startTimesForSeat/' 21 | ENDTIME_URL = API_ROOT + 'v2/endTimesForSeat/' 22 | HISTORY_URL = API_ROOT + 'v2/history/1/' 23 | BOOK_URL = API_ROOT + 'v2/freeBook' 24 | CANCEL_URL = API_ROOT + 'v2/cancel/' 25 | STOP_URL = API_ROOT + 'v2/stop' 26 | 27 | 28 | class SeatKiller(object): 29 | 30 | def __init__(self, username='', password=''): 31 | self.allSeats = {} 32 | self.freeSeats = [] 33 | self.startTimes = [] 34 | self.endTimes = [] 35 | self.token = '' 36 | self.username = username 37 | self.password = password 38 | self.to_addr = '' 39 | self.headers = { 40 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 41 | 'User-Agent': 'doSingle/11 CFNetwork/893.14.2 Darwin/17.3.0', 42 | 'token': self.token 43 | } 44 | self.state = {'RESERVE': '预约', 'CHECK_IN': '履约中', 'AWAY': '暂离'} 45 | 46 | def wait(self, hour, minute, second, nextDay=False): 47 | if nextDay: 48 | time_run = datetime.datetime.replace( 49 | datetime.datetime.now() + datetime.timedelta(days=1), 50 | hour=hour, 51 | minute=minute, 52 | second=second) 53 | else: 54 | time_run = datetime.datetime.replace( 55 | datetime.datetime.now(), 56 | hour=hour, 57 | minute=minute, 58 | second=second) 59 | print('\n', end='') 60 | while True: 61 | delta = time_run - datetime.datetime.now() 62 | if delta.total_seconds() <= 0: 63 | print('\n', end='') 64 | break 65 | print( 66 | '\r正在等待系统开放...剩余' + str(int(delta.total_seconds())) + '秒', 67 | end=' ') 68 | time.sleep(0.05) 69 | 70 | def get_token(self): 71 | datas = {'username': self.username, 'password': self.password} 72 | print('\nTry getting token...Status: ', end='') 73 | try: 74 | response = requests.get( 75 | LOGIN_URL, 76 | params=datas, 77 | headers=self.headers, 78 | verify=False, 79 | timeout=5) 80 | json = response.json() 81 | print(json['status']) 82 | if json['status'] == 'success': 83 | self.token = json['data']['token'] 84 | self.headers['token'] = self.token 85 | print('token:' + json['data']['token']) 86 | return json['data']['token'] 87 | else: 88 | print(json['message']) 89 | return False 90 | except Exception: 91 | print('Connection lost') 92 | return False 93 | 94 | def get_user_info(self): 95 | try: 96 | response = requests.get( 97 | USER_URL, headers=self.headers, verify=False, timeout=5) 98 | json = response.json() 99 | if json['status'] == 'success': 100 | self.name = json['data']['name'] 101 | print('\n你好,' + self.name + ' 上次入馆时间:' + json['data'] 102 | ['lastLogin'].replace('T', ' ').rstrip('.000') + ' 状态:' + 103 | ('已进入' + json['data']['lastInBuildingName'] + ' 入馆时间:' + 104 | json['data']['lastIn'] 105 | if json['data']['checkedIn'] else '未入馆') + ' 违约记录:' + 106 | str(json['data']['violationCount']) + '次') 107 | return True 108 | else: 109 | print('\nTry getting user information...Status: failed') 110 | return False 111 | except Exception: 112 | print('\nTry getting user information...Status: Connection lost') 113 | return False 114 | 115 | def get_buildings(self): 116 | print('\nTry getting building information...Status: ', end='') 117 | try: 118 | response = requests.get( 119 | FILTERS_URL, headers=self.headers, verify=False, timeout=5) 120 | json = response.json() 121 | print(json['status']) 122 | if json['status'] == 'success': 123 | return json 124 | else: 125 | print(json) 126 | return False 127 | except Exception: 128 | print('Connection lost') 129 | return False 130 | 131 | def get_rooms(self, buildingId): 132 | url = STATS_URL + buildingId 133 | print('\nTry getting room information...Status: ', end='') 134 | try: 135 | response = requests.get( 136 | url, headers=self.headers, verify=False, timeout=5) 137 | json = response.json() 138 | print(json['status']) 139 | if json['status'] == 'success': 140 | print('\n当前座位状态:') 141 | for room in json['data']: 142 | print('\n' + room['room'] + '\n楼层:' + str(room['floor']) + 143 | '\n总座位数:' + str(room['totalSeats']) + '\n已预约:' + 144 | str(room['reserved']) + '\n正在使用:' + 145 | str(room['inUse']).ljust(3) + '\n暂离:' + 146 | str(room['away']) + '\n空闲:' + str(room['free'])) 147 | return True 148 | else: 149 | print(json) 150 | return False 151 | except Exception: 152 | print('Connection lost') 153 | return False 154 | 155 | def check_res_info(self): 156 | url = HISTORY_URL + '30' 157 | print('\nTry getting reservation information...Status: ', end='') 158 | try: 159 | response = requests.get( 160 | url, headers=self.headers, verify=False, timeout=5) 161 | json = response.json() 162 | print(json['status']) 163 | if json['status'] == 'success': 164 | for reservation in json['data']['reservations']: 165 | if reservation['stat'] in ['RESERVE', 'CHECK_IN', 'AWAY']: 166 | print( 167 | '\n--------------------已检测到有效预约---------------------' # noqa 168 | ) 169 | print('ID:' + str(reservation['id'])) 170 | print('时间:' + reservation['date'] + ' ' + 171 | reservation['begin'] + '~' + reservation['end']) 172 | if reservation['awayBegin'] and reservation['awayEnd']: 173 | print('暂离时间:' + reservation['awayBegin'] + '~' + 174 | reservation['awayEnd']) 175 | elif reservation[ 176 | 'awayBegin'] and not reservation['awayEnd']: 177 | print('暂离时间:' + reservation['awayBegin']) 178 | print('状态:' + self.state.get(reservation['stat'])) 179 | print('地址:' + reservation['loc']) 180 | print( 181 | '------------------------------------------------------' # noqa 182 | ) 183 | 184 | if '3C创客空间' in reservation['loc'] and reservation[ 185 | 'stat'] == 'RESERVE': 186 | if input('\n该座位位于\'一楼3C创客空间\',是否进入改签模式(1.是 2.否):' 187 | ) == '1': 188 | self.exchange_loop( 189 | '1', XT_LITE, 190 | str( 191 | int(reservation['begin'][:2]) * 60 + 192 | int(reservation['begin'][-2:])), 193 | str( 194 | int(reservation['end'][:2]) * 60 + 195 | int(reservation['end'][-2:])), 196 | str(reservation['id'])) 197 | sys.exit() 198 | 199 | return str( 200 | reservation['id'] 201 | ) if reservation['stat'] == 'RESERVE' else 'using' 202 | print('\n未检测到有效预约') 203 | return False 204 | else: 205 | print('\n检测有效预约失败') 206 | return False 207 | except Exception: 208 | print('Connection lost') 209 | return False 210 | 211 | def get_seats(self, roomId, date): 212 | url = LAYOUT_URL + roomId + '/' + date 213 | print('\nTry getting seat information...Status: ', end='') 214 | try: 215 | response = requests.get( 216 | url, headers=self.headers, verify=False, timeout=5) 217 | json = response.json() 218 | print(json['status']) 219 | if json['status'] == 'success': 220 | self.allSeats = {} 221 | for seat in json['data']['layout']: 222 | if json['data']['layout'][seat]['type'] == 'seat': 223 | self.allSeats[json['data']['layout'][seat] 224 | ['name']] = str( 225 | json['data']['layout'][seat]['id']) 226 | return True 227 | else: 228 | print(json) 229 | return False 230 | except Exception: 231 | print('Connection lost') 232 | return False 233 | 234 | def search_free_seat(self, 235 | buildingId, 236 | roomId, 237 | date, 238 | startTime, 239 | endTime, 240 | batch='9999', 241 | page='1'): 242 | url = SEARCH_URL + date + '/' + startTime + '/' + endTime 243 | datas = { 244 | 't': '1', 245 | 'roomId': roomId, 246 | 'buildingId': buildingId, 247 | 'batch': batch, 248 | 'page': page, 249 | 't2': '2' 250 | } 251 | print( 252 | '\nTry searching for free seats in room ' + roomId + '...Status: ', 253 | end='') 254 | try: 255 | response = requests.post( 256 | url, headers=self.headers, data=datas, verify=False, timeout=5) 257 | json = response.json() 258 | if json['data']['seats'] != {}: 259 | print('success') 260 | for num in json['data']['seats']: 261 | self.freeSeats.append( 262 | str(json['data']['seats'][num]['id'])) 263 | return 'Success' 264 | else: 265 | print('failed') 266 | return 'Failed' 267 | except Exception: 268 | print('Connection lost') 269 | return 'Connection lost' 270 | 271 | def check_start_time(self, seatId, date, startTime): 272 | url = STARTTIME_URL + seatId + '/' + date 273 | print( 274 | '\nTry checking startTimes of seat No.' + seatId + '...Status: ', 275 | end='') 276 | try: 277 | response = requests.get( 278 | url, headers=self.headers, verify=False, timeout=5) 279 | json = response.json() 280 | if json['status'] == 'success': 281 | self.startTimes.clear() 282 | for startTimeJson in json['data']['startTimes']: 283 | self.startTimes.append(startTimeJson['id']) 284 | if startTime in self.startTimes: 285 | print('success') 286 | return True 287 | else: 288 | print('fail') 289 | return False 290 | else: 291 | print('fail') 292 | return False 293 | except Exception: 294 | print('Connection lost') 295 | return False 296 | 297 | def check_end_time(self, seatId, date, startTime, endTime): 298 | url = ENDTIME_URL + seatId + '/' + date + '/' + startTime 299 | print( 300 | '\nTry checking endTimes of seat No.' + seatId + '...Status: ', 301 | end='') 302 | try: 303 | response = requests.get( 304 | url, headers=self.headers, verify=False, timeout=5) 305 | json = response.json() 306 | if json['status'] == 'success': 307 | self.endTimes.clear() 308 | for endTimeJson in json['data']['endTimes']: 309 | self.endTimes.append(endTimeJson['id']) 310 | if endTime in self.endTimes: 311 | print('success') 312 | return True 313 | else: 314 | print('fail') 315 | return False 316 | else: 317 | print('fail') 318 | return False 319 | except Exception: 320 | print('Connection lost') 321 | return False 322 | 323 | def book_seat(self, seatId, date, startTime, endTime): 324 | datas = { 325 | 't': '1', 326 | 'startTime': startTime, 327 | 'endTime': endTime, 328 | 'seat': seatId, 329 | 'date': date, 330 | 't2': '2' 331 | } 332 | print('\nTry booking seat...Status: ', end='') 333 | try: 334 | response = requests.post( 335 | BOOK_URL, 336 | headers=self.headers, 337 | data=datas, 338 | verify=False, 339 | timeout=5) 340 | json = response.json() 341 | print(json['status']) 342 | if json['status'] == 'success': 343 | self.print_book_info(json) 344 | if self.to_addr: 345 | self.send_mail(json) 346 | if '3C创客空间' in json['data']['location']: 347 | return str(json['data']['id']) 348 | else: 349 | return 'Success' 350 | else: 351 | print(json) 352 | return 'Failed' 353 | except Exception: 354 | print('Connection lost') 355 | return 'Connection lost' 356 | 357 | def cancel_res(self, id): 358 | url = CANCEL_URL + id 359 | print('\nTry cancelling reservation...Status: ', end='') 360 | try: 361 | response = requests.get( 362 | url, headers=self.headers, verify=False, timeout=5) 363 | json = response.json() 364 | print(json['status']) 365 | if json['status'] == 'success': 366 | return True 367 | else: 368 | print(json) 369 | return False 370 | except Exception: 371 | print('Connection lost') 372 | return False 373 | 374 | def stop_using(self): 375 | print('\nTry releasing seat...Status: ', end='') 376 | try: 377 | response = requests.get( 378 | STOP_URL, headers=self.headers, verify=False, timeout=5) 379 | json = response.json() 380 | print(json['status']) 381 | if json['status'] == 'success': 382 | return True 383 | else: 384 | print(json) 385 | return False 386 | except Exception: 387 | print('Connection lost') 388 | return False 389 | 390 | def print_book_info(self, json): 391 | print('\n--------------------座位预约成功!--------------------') 392 | print('ID:' + str(json['data']['id'])) 393 | print('凭证号码:' + json['data']['receipt']) 394 | print('时间:' + json['data']['onDate'] + ' ' + json['data']['begin'] + 395 | '~' + json['data']['end']) 396 | print('状态:' + ('已签到' if json['data']['checkedIn'] else '预约')) 397 | print('地址:' + json['data']['location']) 398 | print('---------------------------------------------------') 399 | 400 | def send_mail(self, jsonCode): 401 | jsonCode['username'] = self.username 402 | jsonCode['name'] = self.name 403 | jsonCode['client'] = 'Python' 404 | print('\n正在尝试发送邮件提醒至\'' + self.to_addr + '\'...') 405 | try: 406 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 407 | s.settimeout(5) 408 | s.connect(('134.175.186.17', 5210)) 409 | 410 | if s.recv(5).decode() == 'hello': 411 | s.send( 412 | bytes( 413 | 'json ' + str( 414 | json.dumps(jsonCode, ensure_ascii=False, 415 | indent=2)), 'utf-8')) 416 | 417 | if s.recv(7).decode() == 'success': 418 | print( 419 | '邮件提醒发送成功,若接收不到提醒,请将\'seatkiller@outlook.com\'添加至邮箱白名单' 420 | ) 421 | else: 422 | print('邮件提醒发送失败') 423 | 424 | s.close() 425 | except Exception: 426 | return 427 | 428 | def loop(self, buildingId, rooms, startTime, endTime): 429 | print('\n-------------------------捡漏模式开始--------------------------') 430 | date = datetime.date.today() 431 | date = date.strftime('%Y-%m-%d') 432 | while True: 433 | self.freeSeats = [] 434 | if self.get_token(): 435 | for i in rooms: 436 | if self.search_free_seat(buildingId, i, date, startTime, 437 | endTime) == 'Connection lost': 438 | print('\n连接丢失,30秒后尝试继续检索空位') 439 | time.sleep(30) 440 | for freeSeatId in self.freeSeats: 441 | response = self.book_seat(freeSeatId, date, startTime, 442 | endTime) 443 | if response[0] in map(str, 444 | range(10)) or response == 'Success': 445 | print('\n捡漏成功') 446 | print( 447 | '\n-------------------------捡漏模式结束--------------------------' # noqa 448 | ) 449 | return response 450 | elif response == 'Failed': 451 | time.sleep(random.uniform(1, 3)) 452 | continue 453 | else: 454 | print('\n连接丢失,1分钟后尝试继续抢座') 455 | time.sleep(60) 456 | continue 457 | else: 458 | ddl = datetime.datetime.replace( 459 | datetime.datetime.now(), hour=20, minute=0, second=0) 460 | delta = ddl - datetime.datetime.now() 461 | print('\n循环结束,3秒后进入下一个循环,运行时间剩余' + str(delta.seconds) + 462 | '秒\n') 463 | time.sleep(3) 464 | else: 465 | print('\n获取token失败,1分钟后再次尝试') 466 | time.sleep(60) 467 | 468 | if datetime.datetime.now() >= datetime.datetime.replace( 469 | datetime.datetime.now(), hour=20, minute=0, second=0): 470 | print('\n捡漏失败,超出运行时间') 471 | print( 472 | '\n-------------------------捡漏模式结束--------------------------' # noqa 473 | ) 474 | return False 475 | 476 | def exchange_loop(self, 477 | buildingId, 478 | rooms, 479 | startTime, 480 | endTime, 481 | id, 482 | nextDay=False): 483 | print('\n-------------------------改签模式开始--------------------------') 484 | if nextDay: 485 | date = datetime.date.today() + datetime.timedelta(days=1) 486 | else: 487 | date = datetime.date.today() 488 | date = date.strftime('%Y-%m-%d') 489 | cancelled = False 490 | while True: 491 | self.freeSeats = [] 492 | if self.get_token(): 493 | for i in rooms: 494 | res = self.search_free_seat(buildingId, i, date, startTime, 495 | endTime) 496 | if res == 'Connection lost': 497 | print('\n连接丢失,30秒后尝试继续检索空位') 498 | time.sleep(30) 499 | for freeSeatId in self.freeSeats: 500 | if not self.check_start_time(freeSeatId, date, startTime): 501 | continue 502 | if not self.check_end_time(freeSeatId, date, startTime, 503 | endTime): 504 | continue 505 | if self.cancel_res(id): 506 | cancelled = True 507 | response = self.book_seat(freeSeatId, date, startTime, 508 | endTime) 509 | if response == 'Success': 510 | print('\n改签成功') 511 | print( 512 | '\n-------------------------改签模式结束--------------------------' # noqa 513 | ) 514 | return True 515 | elif response == 'Failed': 516 | time.sleep(random.uniform(1, 3)) 517 | continue 518 | else: 519 | print('\n连接丢失,1分钟后尝试继续抢座') 520 | time.sleep(60) 521 | continue 522 | else: 523 | ddl = datetime.datetime.replace( 524 | datetime.datetime.now(), hour=20, minute=0, second=0) 525 | delta = ddl - datetime.datetime.now() 526 | print('\n循环结束,3秒后进入下一个循环,运行时间剩余' + str(delta.seconds) + 527 | '秒\n') 528 | time.sleep(3) 529 | else: 530 | print('\n获取token失败,1分钟后再次尝试') 531 | time.sleep(60) 532 | 533 | if datetime.datetime.now() >= datetime.datetime.replace( 534 | datetime.datetime.now(), hour=20, minute=0, second=0): 535 | if cancelled: 536 | print('\n改签失败,原座位已丢失') 537 | else: 538 | print('\n改签失败,原座位未丢失') 539 | print( 540 | '\n-------------------------改签模式结束--------------------------' # noqa 541 | ) 542 | return False 543 | --------------------------------------------------------------------------------