diff --git a/README.md b/README.md
index 7e5e9f8..95f3996 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,22 @@
-# USTC-EPC-BOT
+# EPC-BOT for USTC
中国科学技术大学EPC系统自动抢课脚本.
+## 功能列表
+
+待更新.
+
## 使用说明
待更新.
+## 实现原理
+待更新.
-## 算法逻辑
+## 选课逻辑
### 未达预约上限(4学时)的情况
@@ -29,43 +35,32 @@
- 比较可预约课程与已预约课程的时间先后. 若存在可预约课程的日期早于已预约课程中的最晚日期, 则考虑进行一次课程替换. 若存在1学时课程与2学时课程的日期相同, 优先选择2学时课程.
-
-
-| I | II | III | IV | V |
-| - | - | - | - | - |
-| 1 | 1 | 1 | 2 | 2 |
-| 1 | 1 | 2 | 1 | 2 |
-| 1 | 2 | 1 | 1 | |
-| 1 | | | | |
-
-
-
-- 将已预约课程按照日期先后排列, 共有以上五种学时组合, 分别讨论之:
- + 组合I:
+- 将已预约课程按照日期先后排列, 共有五种学时组合, 分别讨论之:
+ + 组合I (1, 1, 1, 1):
- 可预约课程为2学时:
+ 可预约课程日期早于已预约的倒数第2节课, 将倒数第1-2节已预约课程替换为可预约课程.
+ 可预约课程日期晚于已预约的倒数第2节课, 不替换.
- 可预约课程为1学时:
+ 将倒数第1节已预约课程替换为可预约课程.
- + 组合II:
+ + 组合II (1, 1, 2):
- 可预约课程为2学时:
+ 将倒数第1节已预约课程替换为可预约课程.
- 可预约课程为1学时:
+ 可预约课程日期早于已预约的倒数第2节课, 将倒数第1-2节已预约课程替换为可预约课程.
+ 可预约课程日期晚于已预约的倒数第2节课, 不替换.
- + 组合III:
+ + 组合III (1, 2, 1):
- 可预约课程为2学时:
+ 可预约课程日期早于已预约的倒数第2节课, 将倒数第2节已预约课程替换为可预约课程.
+ 可预约课程日期晚于已预约的倒数第2节课, 不替换.
- 可预约课程为1学时:
+ 将倒数第1节已预约课程替换为可预约课程.
- + 组合IV:
+ + 组合IV (2, 1, 1):
- 可预约课程为2学时:
+ 可预约课程日期早于已预约的倒数第2节课, 将倒数第1-2节已预约课程替换为可预约课程.
+ 可预约课程日期晚于已预约的倒数第2节课, 不替换.
- 可预约课程为1学时:
+ 将倒数第1节已预约课程替换为可预约课程.
- + 组合V:
+ + 组合V (2, 2):
- 可预约课程为2学时:
+ 将倒数第1节已预约课程替换为可预约课程.
- 可预约课程为1学时:
@@ -74,3 +69,26 @@
- 循环上述操作, 直至可预约的课程列表为空.
+
+## Q & A
+
+### 使用 PyInstaller 重新打包后, Selenium 运行时有黑色空职台弹出, 如何避免?
+打开 Python\Lib\site-packages\selenium\webdriver\common\service.py 文件, 将
+```
+self.process = subprocess.Popen(cmd, env=self.env, close_fds=platform.system() != 'Windows', stdout=self.log_file, stderr=self.log_file, stdin=PIPE)
+```
+改写成
+```
+self.process = subprocess.Popen(cmd, stdin=PIPE, stdout=PIPE ,stderr=PIPE, shell=False, creationflags=0x08000000)
+```
+或直接用工程目录中的 Selenium\service.py 文件替换原生的 service.py 文件
+并重新使用 PyInstaller 打包.
+
+
+## 参考文献
+
+[1] 木华生. 中科大EPC课程爬取\[OL\]. https://blog.csdn.net/qq_28491207/article/details/84261732, 2018.
+
+[2] David Cortesi, William Caban. PyInstaller Manual\[OL\]. https://pyinstaller.readthedocs.io/.
+
+[3] AhmedWas. Getting Rid of ChromeDirver Console Window with PyInstaller\[OL\]. https://stackoverflow.com/questions/52643556/getting-rid-of-chromedirver-console-window-with-pyinstaller, 2018.
diff --git a/epc_bot.py b/epc_bot.py
index 947d84b..32cea23 100644
--- a/epc_bot.py
+++ b/epc_bot.py
@@ -1,4 +1,4 @@
-import requests, time, re, json, threading
+import os, inspect, requests, time, re, json, threading
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
@@ -49,9 +49,11 @@ def __init__(self, sid, passwd, filtr, email_addr, email_pwd, gui=None):
options.add_argument("--no-sandbox")
options.add_argument("--headless")
options.add_argument("--disable-gpu")
- self.WEBDRIVER = webdriver.Chrome(chrome_options=options,
- executable_path="./selenium/chromedriver.exe"
- )
+ work_dir = os.path.realpath(os.path.abspath(
+ os.path.split(inspect.getfile(inspect.currentframe()))[0]
+ ))
+ driver_dir = os.path.join(work_dir, "selenium", "chromedriver.exe")
+ self.WEBDRIVER = webdriver.Chrome(chrome_options=options, executable_path=driver_dir)
def stop(self):
self._STOP_FLAG.set()
diff --git a/graphic.py b/graphic.py
index f043560..3ca77c5 100644
--- a/graphic.py
+++ b/graphic.py
@@ -1,7 +1,7 @@
from tkinter import *
from tkinter.scrolledtext import *
from tkinter.messagebox import showinfo
-import json, threading
+import os, inspect, json, threading, traceback
from epc_bot import *
class GUI:
@@ -11,6 +11,10 @@ def __init__(self, master):
self.master.title("EPC-BOT for USTC")
self.master.protocol('WM_DELETE_WINDOW', self.on_gui_destroy)
+ self.work_dir = os.path.realpath(os.path.abspath(
+ os.path.split(inspect.getfile(inspect.currentframe()))[0]
+ ))
+
self.frame_basic = Frame(master)
self.frame_basic.grid(row=0, pady=10)
Label(self.frame_basic, text="--- Basic Settings ---").grid(row=0, columnspan=4)
@@ -34,22 +38,27 @@ def __init__(self, master):
self.frame_filter = Frame(master)
self.frame_filter.grid(row=1, pady=10)
Label(self.frame_filter, text="--- Filter Settings ---").grid(row=0, columnspan=5)
- with open("config.template.json", "r", encoding="utf-8") as template_file:
- self.filters_all = json.load(template_file)["filter"]
- self.filters_var = list()
- self.cbtn_filters = list()
- row, col = 1, 0
- for i in range(len(self.filters_all)):
- if (i%5 == 0):
- row = row + 1
- col = -1
- col = col + 1
- filter_str = self.filters_all[i]["wday"][0:3] + ".\t" + self.filters_all[i]["time"]
- self.filters_var.append(IntVar())
- cbtn_filter = Checkbutton(self.frame_filter, text=filter_str, variable=self.filters_var[i])
- cbtn_filter.grid(row=row, column=col, padx=5, pady=2)
- cbtn_filter.deselect()
- self.cbtn_filters.append(cbtn_filter)
+ template_dir = os.path.join(self.work_dir, "config.template.json")
+ if os.path.exists(template_dir):
+ with open(template_dir, "r", encoding="utf-8") as template_file:
+ self.filters_all = json.load(template_file)["filter"]
+ self.filters_var = list()
+ self.cbtn_filters = list()
+ row, col = 1, 0
+ for i in range(len(self.filters_all)):
+ if (i%5 == 0):
+ row = row + 1
+ col = -1
+ col = col + 1
+ filter_str = self.filters_all[i]["wday"][0:3] + ".\t" + self.filters_all[i]["time"]
+ self.filters_var.append(IntVar())
+ cbtn_filter = Checkbutton(self.frame_filter, text=filter_str, variable=self.filters_var[i])
+ cbtn_filter.grid(row=row, column=col, padx=5, pady=2)
+ cbtn_filter.deselect()
+ self.cbtn_filters.append(cbtn_filter)
+ else:
+ showerror(title="Error", message="Configuration template is missing!")
+ self.master.destroy()
self.btn_start = Button(master, text="Start", command=self.start_bot)
self.btn_start.grid(row=2, ipadx=self.btn_start.winfo_width()*5, pady=10)
@@ -59,7 +68,8 @@ def __init__(self, master):
## 读取工作目录下存储的配置文件
def read_config(self):
try:
- with open("config.json", "r", encoding="utf-8") as config:
+ config_dir = os.path.join(self.work_dir, "config.json")
+ with open(config_dir, "r", encoding="utf-8") as config:
config = json.load(config)
self.sid = config["sid"]
self.passwd = config["passwd"]
@@ -105,6 +115,7 @@ def start_bot(self):
self.write_config()
else:
showinfo(title="Alert", message="Please fill the blanks in basic settings module!")
+ return
self.master.resizable(False, False)
self.frame_basic.grid_forget()
@@ -112,10 +123,16 @@ def start_bot(self):
self.btn_start.grid_forget()
self.console = ScrolledText(self.master)
self.console.pack()
+ self.update_log("Working Directory:\n%s\n" % self.work_dir)
- self.bot = EPCBot(self.sid, self.passwd, self.filters_json, \
- self.email_addr, self.email_pwd, self)
- self.bot.start()
+ try:
+ self.bot = EPCBot(self.sid, self.passwd, self.filters_json, \
+ self.email_addr, self.email_pwd, self)
+ self.bot.start()
+ self.update_log("EPC-Bot is running...\n")
+ except Exception as e:
+ self.update_log("Chrome driver is not installed!")
+ self.update_log(traceback.format_exc())
## 更新EPC-BOT日志到GUI
def update_log(self, text):
@@ -127,6 +144,8 @@ def update_log(self, text):
## 关闭GUI时杀死子线程
def on_gui_destroy(self):
self.master.destroy()
- if self.bot:
+ try:
self.bot.stop()
+ except:
+ pass
\ No newline at end of file
diff --git a/py2exe.bat b/py2exe.bat
new file mode 100644
index 0000000..8017d4f
--- /dev/null
+++ b/py2exe.bat
@@ -0,0 +1,6 @@
+pyinstaller --onedir --windowed ^
+ --add-data="config.template.json;." ^
+ --add-data="selenium\chromedriver.exe;selenium" ^
+ --name="epc_bot" ^
+ main_gui.py
+rd /s /q __pycache__
\ No newline at end of file
diff --git a/selenium/service.py b/selenium/service.py
index a10868d..2bd5653 100644
--- a/selenium/service.py
+++ b/selenium/service.py
@@ -69,9 +69,7 @@ def start(self):
try:
cmd = [self.path]
cmd.extend(self.command_line_args())
- self.process = subprocess.Popen(cmd, env=self.env,
- close_fds=platform.system() != 'Windows',
- stdout=self.log_file, stderr=self.log_file, creationflags=134217728)
+ self.process = subprocess.Popen(cmd, stdin=PIPE, stdout=PIPE ,stderr=PIPE, shell=False, creationflags=0x08000000)
except TypeError:
raise
except OSError as err: