东北大学(NEU)羽毛球场地预约工具详解

注:本文详细介绍并说明仓库中的脚本 0.9.py(也可认为是 v4.0 的实现版本)。我会从功能、原理、安装依赖、配置示例、运行方式、关键代码讲解、常见问题排查、安全与合规等方面把项目写成一篇可以直接发布的技术博客。


项目简介

一键场地预约 v4.0(文件名示例 0.9.py)是一个基于 Selenium 的自动化脚本,目标是帮助用户在东北大学(示例站点 book.neu.edu.cn)抢/预约体育场地。项目提供了带交互式 Tkinter GUI 的桌面启动器,并集成了邮件与 PushPlus 推送通知、日志输出和简单的防多开机制(锁文件 + 进程检查)。脚本尽量保留人工可配置项(时间段、场地号、刷新间隔、抖动、验证码支持等),并对常见页面差异与弹窗做了稳健处理。


功能亮点

  • 自动化流程:Selenium 自动登录并在页面上寻找可预约时段。
  • 多目标监控:支持多个目标时段和多个场地同时监控。
  • 验证码支持:启动时可手动填写验证码,脚本自动提交。
  • 智能结果检测:预约提交后,自动通过 URL 变化、DOM 元素(如支付页或“预约成功”提示)判断是否成功。
  • 多通道通知:支持 SMTP 邮件通知和 PushPlus 微信推送(支持发送 HTML 格式的彩色日志)。
  • GUI 图形界面:提供 Tkinter 界面,方便配置账号、时段、场地及邮件设置,支持“自动倒计时启动”。
  • 日志系统:日志文件保存在 logs/,每次运行生成独立日志文件;支持 ANSI 色彩转 HTML。
  • 防多开机制:通过文件锁(booking_app.lock)和进程扫描防止脚本重复运行。

运行环境与依赖

推荐在 Python 3.8+ 环境下运行。主要第三方依赖包括:

  • selenium:核心自动化库
  • psutil:用于进程检查
  • colorama:控制台颜色输出
  • requests:(可选)用于 PushPlus 推送
  • tkinter:(Python 自带)用于 GUI 界面
  • smtplib:(Python 自带)用于邮件发送

安装依赖示例(建议放在 virtualenv 中):

1
2
3
4
5
6
7
8
# 创建虚拟环境
python -m venv venv
# 激活虚拟环境
source venv/bin/activate # macOS / Linux
# venv\Scripts\activate # Windows PowerShell

# 安装依赖
pip install selenium psutil colorama requests

另外需要下载与您 Chrome 浏览器版本匹配的 ChromeDriver,可从 https://chromedriver.chromium.org/ 下载并将其路径加入环境变量或放在脚本同目录下。


准备工作

  1. Chrome / ChromeDriver

    • 查 Chrome 版本:地址栏输入 chrome://settings/help
    • 下载对应版本的 chromedriver 并解压。
    • Windows 用户建议把 chromedriver.exe 放到脚本同目录或 C:\Windows\
  2. 账号信息(NEU 预约站点)

    • 确保账号能正常在浏览器中登录。
    • 若站点有验证码,脚本提供了“手动在启动时填写验证码并提交”的方式(会等待并填写 ID 为 PM1 的验证码输入框)。
  3. SMTP(若使用邮件通知)

    • 准备 SMTP 服务器/账号/密码与接收邮箱。
    • 脚本使用 TLS(server.starttls()),端口通常为 587。
  4. PushPlus(可选)

    • 登录 PushPlus 获取 token,并在配置中填入 PUSHPLUS_TOKEN

配置说明

脚本会读取同目录下的 config.json(如果不存在,GUI 运行后会自动生成)。下面给出一个完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"USERNAME": "your_username",
"PASSWORD": "your_password",
"VERIFICATION_CODE": "",
"MEMBER_NAME": "张三",
"MEMBER_ID": "12345678",
"TARGET_SLOTS": ["18:00-20:00", "19:00-20:00"],
"SELECTED_COURTS": ["1","2"],
"REFRESH_TIME": 5,
"JITTER_TIME": 1,
"SMTP_SERVER": "smtp.example.com",
"SMTP_PORT": "587",
"EMAIL_USER": "notify@example.com",
"EMAIL_PASSWORD": "your_email_password",
"RECIPIENT_EMAIL": "you@example.com",
"PUSHPLUS_TOKEN": "xxxxxx",
"SHOW_BROWSER_DEBUG": true,
"AUTO_START": false,
"AUTO_START_SECONDS": 10
}

字段说明:

  • USERNAME / PASSWORD:登录站点的账号/密码。
  • TARGET_SLOTS:目标时间段字符串列表,需严格匹配页面显示文本(如 “18:00-20:00”)。
  • SELECTED_COURTS:场地编号列表(字符串)。
  • REFRESH_TIME / JITTER_TIME:默认刷新间隔(秒)及其随机抖动范围。
  • SHOW_BROWSER_DEBUG:是否显示浏览器窗口(true 显示,false 为 headless 模式)。

启动与使用

GUI 启动(推荐)

双击或在命令行运行:

1
python 0.9.py

程序会弹出 Tkinter 界面,功能包括:

  1. 填写账号/密码、会员姓名/证件。
  2. 勾选目标时段与场地。
  3. 配置通知服务(SMTP 或 PushPlus)。
  4. 设置刷新频率。
  5. 自动倒计时启动:勾选后倒计时结束自动开始运行,适合定时抢票。
  6. 开始预约:手动立即启动。

GUI 会在开始前把当前配置保存到 config.json

命令行 / 自动启动

如果需要无头模式或通过计划任务启动,可以编写一个简单的 Python wrapper 调用核心函数,或者直接运行脚本(脚本会读取上次保存的 config.json)。


关键模块与代码说明

本部分将结合代码片段讲解脚本的核心实现。

1. ANSI 与日志可视化

为了让邮件和 PushPlus 推送能显示带有颜色的日志(例如绿色的“可用”状态),我们需要将控制台的 ANSI 颜色码转换为 HTML。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def ansi_to_html_br(text):
"""
把包含 ANSI 颜色序列的文本转换为 HTML(保留颜色),并把换行显式替换成 <br/>。
适用于 PushPlus 等可能会过滤 <pre> 的推送端。
"""
if text is None:
return ""
text = text.replace('\r\n', '\n').replace('\r', '\n')

parts = []
# ... (省略部分正则解析代码) ...
# 解析 ANSI color code 并转换为 <span style="color:...">

html_inner = ''.join(parts)
html_with_br = html_inner.replace('\n', '<br/>')
return f'<div style="font-family:monospace;white-space:normal;">{html_with_br}</div>'

2. 浏览器初始化与登录

init_driver 函数负责启动 Chrome 并完成登录。它支持 Headless 模式切换,并包含处理验证码的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def init_driver(username, password, verification_code=None, show_browser_debug=True):
try:
opts = Options()
if not show_browser_debug:
try:
opts.add_argument("--headless=new")
except Exception:
opts.headless = True
# ... (添加其他 options) ...

driver = webdriver.Chrome(options=opts)
driver.get('http://book.neu.edu.cn/booking/page/rule/stadium/ypg_ymb')

# 等待并填写用户名/密码
w = WebDriverWait(driver, 12)
w.until(EC.visibility_of_element_located((By.ID, 'un'))).send_keys(username)
driver.find_element(By.ID, 'pd').send_keys(password)
driver.find_element(By.ID, 'index_login_btn').click()

# 如果提供了验证码,等待输入并再次提交
if verification_code:
verification_input = WebDriverWait(driver, 15).until(
EC.visibility_of_element_located((By.ID, 'PM1'))
)
verification_input.clear()
verification_input.send_keys(verification_code)
driver.find_element(By.ID, 'index_login_btn').click()

return driver
except Exception:
logger.exception("初始化 WebDriver 失败")
sys.exit(1)

3. 可用性读取

get_availability_str 函数会抓取页面上的时间段信息,并用绿色高亮显示“可用”的时段,生成日志字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def get_availability_str(driver):
panels = driver.find_elements(By.XPATH, "//div[contains(@class,'selectList')...]")
# ...
# 构造输出:仅当 any_available 为 True 时,对包含关键字的时段染色
for idx, li_texts in enumerate(panels_slot_texts, start=1):
slots = []
for s in li_texts:
if any_available and any(kw in s for kw in available_keywords):
# 使用 ANSI \033[92m 亮绿色
m = re.match(r"^(\d{1,2}:\d{2}-\d{1,2}:\d{2})(.*)$", s)
if m:
colored = "\033[92m" + m.group(1) + "\033[0m" + m.group(2)
slots.append(colored)
else:
slots.append(s)
return "\n".join(lines)

4. 预约核心逻辑

check_and_book_court 是最关键的函数。它遍历场地,找到可用时段后执行点击、填表、提交、同意协议、去支付等一系列操作,并智能判断结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def check_and_book_court(driver, court_no, target_slots, member_name, member_id):
# ... (定位 Panel 代码) ...
for slot_el in panel.find_elements(By.XPATH, ".//div[contains(@class,'TimeDiv')]//li"):
# 只有同时包含目标时段字符串且标为“可用”时才去尝试
if any(t in text and '可用' in text for t in target_slots):
# 1. 调用页面 JS 打开预约面板
driver.execute_script(f"openBook({court_no-1});")

# 2. 填写表单并提交
# ... (省略 DOM 操作细节) ...
submit = panel.find_element(By.CLASS_NAME, 'enter_button')
driver.execute_script("arguments[0].click();", submit)

# 3. 尝试点击“我同意”和“去支付”
# ...

# 4. 结果判定:轮询检测是否进入支付页或预约成功页
payment_or_success_confirmed = False
for _ in range(int(wait_total / poll)):
# 检测 URL 变化或特定 DOM 元素 (如 id="book_no", id="payment")
if ("PayPreService" in cur_url) or len(driver.find_elements(By.ID, "book_no")) > 0:
payment_or_success_confirmed = True
break
time.sleep(poll)

if payment_or_success_confirmed:
return (court_no, chosen)
else:
return None
return None

日志、通知与结果发送

脚本内置了完善的通知系统:

  • 日志文件:保存到 logs/,文件名包含时间戳。
  • PushPlus 推送:优先使用 requests 库发送,失败后回退到 urllib
  • 邮件通知:使用 SMTP 发送,支持将 ANSI 日志转换为 HTML 表格发送。
1
2
3
4
5
6
7
# PushPlus 推送示例
def send_pushplus(token, title, content, channel="mail", webhook=""):
url = f"http://www.pushplus.plus/send/{token}"
# ... (构建 payload) ...
import requests
resp = requests.post(url, json=payload)
# ... (处理响应) ...

常见问题与排错

  1. 登录失败

    • 检查 init_driver 中查找用户名/密码输入框的 ID(目前脚本使用 un / pd)。若网站改版,需更新选择器。
    • 确保没有触发额外的滑块验证码。
  2. ChromeDriver 错误

    • 报错 executable needs to be in PATH:请确保 chromedriver.exe 在脚本目录或系统 PATH 中。
  3. 页面元素找不到

    • 网络延迟或页面动态加载可能导致元素未就绪,可适当增加 WebDriverWait 的时间。
  4. 多开警告

    • 脚本启动时会检查 logs/booking_app.lock 文件和系统进程。如果上次非正常退出导致锁文件残留,脚本会尝试自动清理或需手动删除。

进阶使用

  1. 作为系统服务运行:在 Linux 下编写 systemd unit,配合 headless 模式实现无人值守抢票。
  2. 计划任务:使用 Windows Task Scheduler 设置在开放预约前 1 分钟启动脚本(配合 GUI 的自动倒计时功能)。
  3. 容器化:编写 Dockerfile,包含 Python 环境和 Chrome/Chromedriver,部署到服务器。

隐私 / 合法性 与 使用协议

  • 本脚本仅作学习与研究 Selenium 自动化技术之用。
  • 严禁用于商业化用途(如代抢服务)或以违反目标网站服务条款的方式运行。
  • 请妥善保管 config.json,其中包含您的账号密码和 Token 信息,不要将其上传到公共代码仓库。

致谢与贡献

  • 感谢 Selenium 社区提供的强大工具。
  • 如果您在使用过程中发现问题或有改进建议,欢迎 Fork 本仓库并提交 PR。