├── .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)
4 | [](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 |
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 |
--------------------------------------------------------------------------------