├── requirements.txt ├── img ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png ├── 9.png ├── 10.png ├── 11.png ├── 12.png ├── 13.png ├── 14.png ├── 15.png └── 16.png ├── .gitignore ├── real_README.md ├── PostJson_multi.py ├── README.md └── ui.py /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | ttkbootstrap 3 | pyinstaller -------------------------------------------------------------------------------- /img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RwandanMtGorilla/ZJGSU_spider/HEAD/img/1.png -------------------------------------------------------------------------------- /img/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RwandanMtGorilla/ZJGSU_spider/HEAD/img/2.png -------------------------------------------------------------------------------- /img/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RwandanMtGorilla/ZJGSU_spider/HEAD/img/3.png -------------------------------------------------------------------------------- /img/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RwandanMtGorilla/ZJGSU_spider/HEAD/img/4.png -------------------------------------------------------------------------------- /img/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RwandanMtGorilla/ZJGSU_spider/HEAD/img/5.png -------------------------------------------------------------------------------- /img/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RwandanMtGorilla/ZJGSU_spider/HEAD/img/6.png -------------------------------------------------------------------------------- /img/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RwandanMtGorilla/ZJGSU_spider/HEAD/img/7.png -------------------------------------------------------------------------------- /img/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RwandanMtGorilla/ZJGSU_spider/HEAD/img/8.png -------------------------------------------------------------------------------- /img/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RwandanMtGorilla/ZJGSU_spider/HEAD/img/9.png -------------------------------------------------------------------------------- /img/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RwandanMtGorilla/ZJGSU_spider/HEAD/img/10.png -------------------------------------------------------------------------------- /img/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RwandanMtGorilla/ZJGSU_spider/HEAD/img/11.png -------------------------------------------------------------------------------- /img/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RwandanMtGorilla/ZJGSU_spider/HEAD/img/12.png -------------------------------------------------------------------------------- /img/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RwandanMtGorilla/ZJGSU_spider/HEAD/img/13.png -------------------------------------------------------------------------------- /img/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RwandanMtGorilla/ZJGSU_spider/HEAD/img/14.png -------------------------------------------------------------------------------- /img/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RwandanMtGorilla/ZJGSU_spider/HEAD/img/15.png -------------------------------------------------------------------------------- /img/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RwandanMtGorilla/ZJGSU_spider/HEAD/img/16.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /workbench.md 2 | /.python-version 3 | /pyproject.toml 4 | /dist/ 5 | /build/ 6 | /ui.spec 7 | /uv.lock 8 | /JSONs/ 9 | /.idea/ 10 | -------------------------------------------------------------------------------- /real_README.md: -------------------------------------------------------------------------------- 1 | 2 | ## 环境准备 3 | ```shell 4 | # 下载uv 5 | powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" 6 | 7 | # 克隆项目 8 | git clone https://github.com/RwandanMtGorilla/ZJGSU_spider.git 9 | 10 | # 创建虚拟环境 11 | uv venv --python 3.9 12 | uv init 13 | 14 | # 启动虚拟环境 15 | .venv\Scripts\activate 16 | uv pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple 17 | ``` 18 | 19 | ## 启动 20 | ```shell 21 | uv run ui.py 22 | ``` 23 | 24 | ## 打包 25 | ```shell 26 | pyinstaller --onefile --noconsole ui.py 27 | ``` 28 | -------------------------------------------------------------------------------- /PostJson_multi.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import requests 4 | from glob import glob 5 | import random 6 | import time 7 | 8 | def load_json_files(folder_path): 9 | """ 加载指定文件夹中的所有JSON文件 """ 10 | file_paths = glob(os.path.join(folder_path, '*.json')) 11 | json_data = [] 12 | for file_path in file_paths: 13 | with open(file_path, 'r', encoding='utf-8') as file: 14 | json_data.append(json.load(file)) 15 | return json_data 16 | 17 | def send_requests_until_success(json_requests): 18 | """ 发送请求直到flag为1 """ 19 | for request_info in json_requests: 20 | url = request_info.get("raw_url") 21 | method = request_info.get("method", 'get').lower() 22 | headers = request_info.get("headers") 23 | cookies = request_info.get("cookies") 24 | data = request_info.get("data") 25 | 26 | 27 | while True: 28 | response = requests.request(method, url, headers=headers, cookies=cookies, data=data) 29 | localtime = time.asctime(time.localtime(time.time())) 30 | try: 31 | response_json = response.json() 32 | if response_json.get("flag") == "1": 33 | print(f"{localtime} 成功: {response.text}") 34 | break 35 | else: 36 | print(f"{localtime} 尝试中, 响应: {response.text}") 37 | except json.JSONDecodeError: 38 | print(f"{localtime} 无法解析JSON响应: {response.text}") 39 | break 40 | 41 | wait_time = random.uniform(0.9, 1.3) 42 | time.sleep(wait_time) 43 | 44 | # 指定JSONs文件夹的路径 45 | folder_path = 'JSONs' 46 | json_requests = load_json_files(folder_path) 47 | send_requests_until_success(json_requests) 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zjgsu抢课脚本 2 | ## 使用教程(*更新 2025 - 2026 - 1 ) 3 | 4 | ## 观前提醒: 5 | 6 | 此项目仅用于python学习交流,请勿用于非法用途[doge]。 7 | 8 | 如果你有python代码能力,想要二次开发,请移步[二次开发指南](real_README.md) 9 | 10 | 使用过程略有些复杂,请耐心按照以下教程逐步操作。 11 | 12 | ## 背景 13 | 14 | 一个倒霉的开发者在某次第一轮选课时候一门课没摇上,本着我命由我不由天的精神,怒写抢课脚本,然后就有了本仓库。 15 | 脚本效果拔群,想要的好课都抢到了:P 16 | 17 | 18 | ## 简介 19 | - 脚本会模拟你点击选课的操作 20 | - 直到接收到抢课成功后停止运行。 21 | - 这个脚本理论上可以适配大多数比较简单的抢课系统 22 | 23 | - 因为需要开始后才能配置 所以本脚本适用于捡漏他人退课的情景 而不适合秒杀场景 24 | 25 | ## 文件构成: 26 | 27 | 使用此脚本,必要的文件有两项: 28 | - `JSONs` 文件夹以及其中的`.json`文件(你需要手动配置的抢课信息 请自行创建) 29 | - `ui_2.0.exe`(信息配置完成后执行抢课程序的脚本 版本号可能有变化,请以最新版为准)[下载地址](https://github.com/RwandanMtGorilla/ZJGSU_spider/releases) 30 | 31 | 使用前请检查两项项文件是否完整。 32 | 33 | ## 如何使用: 34 | ### 1. 课程信息写入(在选课期间操作) 35 | 36 | - 确认并记录(Ctrl+c)你要抢的课程名称,确保无课程时间冲突(如果有 将以最先 37 | 抢到的为准) 38 | 39 | - 登录并进入[选课界面](http://124.160.64.163/jwglxt/)(网址) 40 | 页面任意位置右键单击并下拉找到 *检查* 并点击(或按F 12 ) 41 | 42 | 43 | 44 | - 在右侧弹出的开发者模式界面中找到 *network*(或*网络*)并点击 45 | 46 | 47 | 48 | 49 | - 点击Fetch/XHR 50 | 51 | 52 | 53 | - 在左侧选课界面找到 你想要的课 进行 *选课* (可以是满课的课程 只需要点击一次 用以获取自己的账号信息) 54 | 55 | 56 | 57 | - 完成上一步操作后,右侧将会出现数据包 58 | 59 | 60 | 61 | - 右键单击 > Copy > Copy as cURL(bash) 62 | 63 | 64 | 65 | - 进入[curlconverter网站](https://curlconverter.com/) (转换curl请求为JSON) 66 | - 将之前复制的curl内容复制入curl command,language选择JSON 67 | 68 | 69 | 70 | *(例图中信息已经过混淆处理)* 71 | 72 | - 在`JSONs`文件夹中新建一个`.json`文件 将刚刚的信息复制进去并保存 (用一个你记得住的名字)(可以新建txt然后改后缀)(如果需要抢多门课,则创建多个json文件) 73 | 74 | 75 | 76 | 77 | 78 | 79 | ### 2. 脚本,启动! 80 | 81 | - 双击 `UI_2.0.exe` 82 | 83 | 84 | 85 | - 点击`浏览`,选择你存放 `.json` 文件的文件夹 86 | - 点击`开始` 如果不断弹出运行信息 即说明运行成功 87 | 88 | ## 完整代码 89 | 完整代码请查看 [`PostJson_multi.py`](PostJson_multi.py) -------------------------------------------------------------------------------- /ui.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import requests 4 | from glob import glob 5 | import random 6 | import time 7 | from multiprocessing import Process, Manager, freeze_support 8 | import ttkbootstrap as ttk 9 | from ttkbootstrap.constants import * 10 | from tkinter import filedialog, messagebox, Canvas, Scrollbar, Listbox, Frame 11 | from collections import defaultdict 12 | 13 | 14 | def load_json_files(folder_path): 15 | """ 加载指定文件夹中的所有JSON文件 """ 16 | file_paths = glob(os.path.join(folder_path, '*.json')) 17 | json_data = [] 18 | file_names = [] # 存储文件名 19 | for file_path in file_paths: 20 | with open(file_path, 'r', encoding='utf-8') as file: 21 | json_data.append(json.load(file)) 22 | file_names.append(os.path.basename(file_path)) 23 | return json_data, file_names 24 | 25 | 26 | def send_requests_until_success(request_info, log_queue, wait_time): 27 | """ 发送请求直到flag为1 """ 28 | url = request_info.get("raw_url") 29 | method = request_info.get("method", 'get').lower() 30 | headers = request_info.get("headers") 31 | cookies = request_info.get("cookies") 32 | data = request_info.get("data") 33 | 34 | while True: 35 | try: 36 | response = requests.request(method, url, headers=headers, cookies=cookies, data=data) 37 | localtime = time.strftime("%H:%M:%S", time.localtime(time.time())) 38 | except Exception as e: 39 | localtime = time.strftime("%H:%M:%S", time.localtime(time.time())) 40 | log_queue.put(f"请求失败,返回: {e}||{localtime}") 41 | continue 42 | 43 | try: 44 | response_json = response.json() 45 | if response_json.get("flag") == "1": 46 | log_queue.put(f"成功: {response.text}||{localtime}") 47 | time.sleep(1) 48 | break 49 | elif response_json.get("flag") == "0": 50 | log_queue.put(f"响应: {response.text} 请先退选当前时段。||{localtime}") 51 | time.sleep(1) 52 | break 53 | else: 54 | log_queue.put(f"尝试中, 响应: {response.text}||{localtime}") 55 | except json.JSONDecodeError: 56 | log_queue.put(f"无法解析JSON响应: {response.text}||{localtime}") 57 | 58 | time.sleep(random.uniform(wait_time, wait_time + 0.3)) 59 | 60 | 61 | class App: 62 | def __init__(self, root): 63 | self.root = root 64 | self.root.title("JSON 请求多进程管理器") 65 | self.root.geometry('2000x800') 66 | self.root.resizable(False, False) 67 | 68 | self.folder_path = './JSONs' # 默认的JSON文件夹路径 69 | self.json_requests = [] 70 | self.file_names = [] # 保存每个 JSON 文件的文件名 71 | self.processes = [] 72 | self.log_queues = [] 73 | self.log_counts = [] # 存储每个日志框的响应计数 74 | self.log_times = [] # 存储每个日志的最新时间 75 | self.listboxes = [] # 存储Listbox组件 76 | 77 | self.create_widgets() 78 | 79 | def create_widgets(self): 80 | # 创建主框架 81 | main_frame = ttk.PanedWindow(self.root, orient=HORIZONTAL) 82 | main_frame.pack(fill=BOTH, expand=True, padx=10, pady=10) 83 | 84 | # 左侧控制区 85 | control_frame = ttk.Frame(main_frame, padding=(10, 10), bootstyle=SECONDARY) 86 | main_frame.add(control_frame, weight=1) 87 | 88 | # 右侧日志区 89 | log_frame = ttk.Labelframe(main_frame, text="日志输出", padding=(10, 10), bootstyle=INFO) 90 | main_frame.add(log_frame, weight=4) 91 | 92 | # 创建一个容器框架来包含Canvas和滚动条 93 | canvas_container = ttk.Frame(log_frame) 94 | canvas_container.pack(fill=BOTH, expand=True) 95 | 96 | # 创建垂直滚动条 97 | self.main_scrollbar = ttk.Scrollbar(canvas_container, orient=VERTICAL) 98 | self.main_scrollbar.pack(side=RIGHT, fill=Y) 99 | 100 | # 创建Canvas 101 | self.canvas = Canvas(canvas_container, yscrollcommand=self.main_scrollbar.set) 102 | self.canvas.pack(side=LEFT, fill=BOTH, expand=True) 103 | 104 | # 配置滚动条 105 | self.main_scrollbar.configure(command=self.canvas.yview) 106 | 107 | # 创建可滚动的框架 108 | self.scrollable_frame = ttk.Frame(self.canvas) 109 | 110 | # 将scrollable_frame添加到canvas中 111 | self.canvas_window = self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") 112 | 113 | # 路径选择 114 | path_label = ttk.Label(control_frame, text="选择 JSON 文件夹路径:") 115 | path_label.pack(anchor="w", pady=5) 116 | 117 | self.path_entry = ttk.Entry(control_frame, width=25) 118 | self.path_entry.insert(0, self.folder_path) 119 | self.path_entry.pack(fill=X, pady=5) 120 | 121 | browse_button = ttk.Button(control_frame, text="浏览", command=self.browse_folder, bootstyle=PRIMARY) 122 | browse_button.pack(fill=X, pady=5) 123 | 124 | # 等待时间设置 125 | wait_time_label = ttk.Label(control_frame, text="设置等待时间 (秒,最少为0.3):") 126 | wait_time_label.pack(anchor="w", pady=5) 127 | 128 | self.wait_time_var = ttk.DoubleVar(value=0.5) 129 | self.wait_time_entry = ttk.Spinbox(control_frame, from_=0.3, to=10.0, increment=0.1, 130 | textvariable=self.wait_time_var, width=10) 131 | self.wait_time_entry.pack(fill=X, pady=5) 132 | 133 | # 控制按钮 134 | start_button = ttk.Button(control_frame, text="开始", command=self.start_processes, bootstyle=SUCCESS) 135 | start_button.pack(fill=X, pady=5) 136 | 137 | stop_button = ttk.Button(control_frame, text="停止", command=self.stop_processes, bootstyle=DANGER) 138 | stop_button.pack(fill=X, pady=5) 139 | 140 | info_button = ttk.Button(control_frame, text="信息", command=self.show_info, bootstyle=INFO) 141 | info_button.pack(fill=X, pady=5) 142 | 143 | def browse_folder(self): 144 | # 选择JSON文件夹 145 | selected_folder = filedialog.askdirectory(initialdir=self.folder_path, title="选择JSON文件夹") 146 | if selected_folder: 147 | self.folder_path = selected_folder 148 | self.path_entry.delete(0, ttk.END) 149 | self.path_entry.insert(0, self.folder_path) 150 | self.load_json_requests() 151 | 152 | def load_json_requests(self): 153 | # 加载JSON文件 154 | try: 155 | self.json_requests, self.file_names = load_json_files(self.folder_path) 156 | self.clear_logs() 157 | self.create_log_windows() 158 | messagebox.showinfo("加载成功", f"已加载 {len(self.json_requests)} 个请求") 159 | except Exception as e: 160 | messagebox.showerror("加载错误", f"无法加载JSON文件: {str(e)}") 161 | 162 | def create_log_windows(self): 163 | # 清除现有的日志框架 164 | self.listboxes.clear() 165 | self.log_counts.clear() 166 | self.log_times.clear() # 清除时间记录 167 | for widget in self.scrollable_frame.winfo_children(): 168 | widget.destroy() 169 | 170 | # 动态调整每个日志框的高度 171 | num_logs = len(self.file_names) 172 | base_height = 8 173 | adjusted_height = max(5, int(base_height * (1 / (num_logs * 0.3 + 1)))) 174 | 175 | # 绑定Canvas大小变化事件 176 | def configure_canvas(event): 177 | # 获取Canvas的实际宽度 178 | canvas_width = event.width 179 | # 更新scrollable_frame的宽度以匹配Canvas宽度 180 | self.canvas.itemconfig(self.canvas_window, width=canvas_width) 181 | # 强制更新scrollable_frame的布局 182 | self.scrollable_frame.update_idletasks() 183 | # 更新滚动区域 184 | self.canvas.configure(scrollregion=self.canvas.bbox("all")) 185 | 186 | self.canvas.bind('', configure_canvas) 187 | 188 | # 使用鼠标滚轮滚动 189 | def on_mousewheel(event): 190 | self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") 191 | 192 | self.canvas.bind_all("", on_mousewheel) 193 | 194 | for i, file_name in enumerate(self.file_names): 195 | log_queue = Manager().Queue() 196 | self.log_queues.append(log_queue) 197 | self.log_counts.append(defaultdict(int)) 198 | self.log_times.append(defaultdict(str)) # 添加时间记录 199 | 200 | # 创建日志框架,使用ttk.Frame以保持一致的样式 201 | log_frame = ttk.Frame(self.scrollable_frame, relief="ridge", borderwidth=1) 202 | log_frame.pack(fill=BOTH, expand=True, padx=5, pady=5) 203 | 204 | log_label = ttk.Label(log_frame, text=file_name) 205 | log_label.pack(anchor="w", padx=5, pady=2) 206 | 207 | # 创建Listbox容器,关键修改:使用BOTH fill和expand=True 208 | listbox_frame = ttk.Frame(log_frame) 209 | listbox_frame.pack(fill=BOTH, expand=True, padx=5, pady=5) 210 | 211 | # 创建Listbox,关键修改:去掉固定宽度限制 212 | listbox = Listbox(listbox_frame, height=adjusted_height, font=("Arial", 9)) 213 | scrollbar_listbox = ttk.Scrollbar(listbox_frame, orient=VERTICAL, command=listbox.yview) 214 | 215 | listbox.configure(yscrollcommand=scrollbar_listbox.set) 216 | # 关键修改:确保Listbox完全填充其容器 217 | listbox.pack(side=LEFT, fill=BOTH, expand=True) 218 | scrollbar_listbox.pack(side=RIGHT, fill=Y) 219 | 220 | self.listboxes.append(listbox) 221 | 222 | # 强制更新布局 223 | self.root.update_idletasks() 224 | # 更新Canvas配置 225 | self.canvas.configure(scrollregion=self.canvas.bbox("all")) 226 | 227 | # 确保Canvas窗口宽度正确设置 228 | def update_canvas_width(): 229 | canvas_width = self.canvas.winfo_width() 230 | if canvas_width > 1: # 确保Canvas已经被渲染 231 | self.canvas.itemconfig(self.canvas_window, width=canvas_width) 232 | 233 | # 延迟执行宽度更新,确保所有组件都已完成初始化 234 | self.root.after(100, update_canvas_width) 235 | 236 | def clear_logs(self): 237 | # 清除所有日志队列 238 | self.log_queues.clear() 239 | 240 | def start_processes(self): 241 | # 启动所有进程 242 | if not self.json_requests: 243 | messagebox.showwarning("警告", "请先选择包含JSON文件的文件夹并加载请求") 244 | return 245 | 246 | wait_time = max(0.3, self.wait_time_var.get()) 247 | for i, request_info in enumerate(self.json_requests): 248 | process = Process(target=send_requests_until_success, args=(request_info, self.log_queues[i], wait_time)) 249 | process.start() 250 | self.processes.append(process) 251 | self.update_log(i) 252 | 253 | def stop_processes(self): 254 | # 停止所有进程 255 | for process in self.processes: 256 | process.terminate() 257 | self.processes = [] 258 | messagebox.showinfo("停止", "所有进程已停止") 259 | 260 | def show_info(self): 261 | # 创建一个新的信息窗口 262 | info_window = ttk.Toplevel(self.root) 263 | info_window.title("信息") 264 | info_window.geometry("400x200") 265 | 266 | # 显示当前正在运行的进程数 267 | running_processes = len([p for p in self.processes if p.is_alive()]) 268 | process_label = ttk.Label(info_window, text=f"当前正在运行的进程数: {running_processes}") 269 | process_label.pack(pady=10) 270 | 271 | # 创建一个可点击的 GitHub 链接 272 | github_link = ttk.Label(info_window, text="GitHub 链接", foreground="blue", cursor="hand2") 273 | github_link.pack(pady=10) 274 | 275 | # 绑定点击事件打开链接 276 | github_link.bind("", lambda e: self.open_github_link()) 277 | 278 | def open_github_link(self): 279 | # 打开 GitHub 链接 280 | import webbrowser 281 | webbrowser.open("https://github.com/RwandanMtGorilla/ZJGSU_spider/tree/main") 282 | 283 | def update_log(self, index): 284 | # 更新日志显示 285 | if not self.processes or not self.processes[index].is_alive(): 286 | return 287 | try: 288 | log_with_time = self.log_queues[index].get_nowait() 289 | 290 | # 分离日志内容和时间 291 | if "||" in log_with_time: 292 | log, current_time = log_with_time.rsplit("||", 1) 293 | else: 294 | log = log_with_time 295 | current_time = time.strftime("%H:%M:%S", time.localtime(time.time())) 296 | 297 | # 更新计数和时间 298 | self.log_counts[index][log] += 1 299 | self.log_times[index][log] = current_time # 更新为最新时间 300 | count = self.log_counts[index][log] 301 | latest_time = self.log_times[index][log] 302 | 303 | # 在列表中查找是否已存在该日志 304 | found = False 305 | for i in range(self.listboxes[index].size()): 306 | item = self.listboxes[index].get(i) 307 | # 提取原始日志内容(去掉计数和时间部分) 308 | if ' (×' in item: 309 | original_log = item.split(' (×')[0] 310 | elif '[' in item and ']' in item: 311 | # 处理只有时间没有计数的情况 312 | original_log = item.split(' [')[0] 313 | else: 314 | original_log = item 315 | 316 | if original_log == log: 317 | # 更新现有项目 318 | self.listboxes[index].delete(i) 319 | if count > 1: 320 | display_text = f"{log} (×{count}) [{latest_time}]" 321 | else: 322 | display_text = f"{log} [{latest_time}]" 323 | self.listboxes[index].insert(i, display_text) 324 | found = True 325 | break 326 | 327 | if not found: 328 | # 添加新项目 329 | if count > 1: 330 | display_text = f"{log} (×{count}) [{latest_time}]" 331 | else: 332 | display_text = f"{log} [{latest_time}]" 333 | self.listboxes[index].insert('end', display_text) 334 | self.listboxes[index].see('end') 335 | 336 | except: 337 | pass 338 | self.root.after(500, lambda: self.update_log(index)) 339 | 340 | 341 | if __name__ == '__main__': 342 | freeze_support() 343 | root = ttk.Window(themename="litera") 344 | app = App(root) 345 | root.mainloop() 346 | --------------------------------------------------------------------------------