306 lines
12 KiB
Python
306 lines
12 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
精确时间选课自动化脚本
|
||
在指定时间的抢课时段,进行高频请求;其余时间则低频监控。
|
||
"""
|
||
|
||
import requests
|
||
import time
|
||
import json
|
||
import threading
|
||
from datetime import datetime, timedelta
|
||
import schedule
|
||
import signal
|
||
import sys
|
||
|
||
# ====================================================================================
|
||
# ██████████████████████████████ 用户配置区域 ██████████████████████████████
|
||
#
|
||
# 使用前请务必更新这里的 Cookie 和 data 信息!
|
||
#
|
||
# ====================================================================================
|
||
CONFIG = {
|
||
# 目标URL(注意更改profileId!)
|
||
'url': 'https://jwxt.neuq.edu.cn/eams/stdElectCourse!batchOperator.action?profileId=1422',
|
||
|
||
# 请求头 (一般无需修改)
|
||
'headers': {
|
||
'Accept': 'text/html, */*; q=0.01',
|
||
'Accept-Language': 'en-US,en;q=0.9',
|
||
'Connection': 'keep-alive',
|
||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||
'Origin': 'https://jwxt.neuq.edu.cn',
|
||
'Referer': 'https://jwxt.neuq.edu.cn/eams/stdElectCourse!defaultPage.action?electionProfile.id=1422',
|
||
'Sec-Fetch-Dest': 'empty',
|
||
'Sec-Fetch-Mode': 'cors',
|
||
'Sec-Fetch-Site': 'same-origin',
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
|
||
'X-Requested-With': 'XMLHttpRequest',
|
||
},
|
||
|
||
# Cookie (!!重要!! 必须替换为你的有效Cookie)
|
||
'cookies': {
|
||
'semester.id': '85',
|
||
'JSESSIONID': 'YOURCOOKIE1',
|
||
'SERVERNAME': 'c1',
|
||
'JSESSIONID': 'YOURCOOKIE2',
|
||
'GSESSIONID': 'YOURCOOKIE3'
|
||
|
||
},
|
||
|
||
# 请求数据 (!!重要!! 必须替换为你要选择的课程ID)
|
||
'data': '',
|
||
|
||
# 时间配置,替换为你课程开始抢的时间
|
||
'target_date': "2025-08-05", # 目标日期 (格式: YYYY-MM-DD)
|
||
'rush_hours': ["3:15", "10:00", "13:00", "14:00", "15:00", "16:00"], # 抢课时间点
|
||
'rush_interval': 0.6, # 抢课时请求间隔(秒)
|
||
'normal_interval': 10.0, # 平时监控间隔(秒)
|
||
'rush_duration': 10, # 每次抢课持续时间(分钟)
|
||
'rush_start_offset': 5 # 提前几分钟开始抢课(分钟)
|
||
}
|
||
|
||
|
||
class PreciseCourseScheduler:
|
||
def __init__(self, config):
|
||
self.config = config
|
||
self.url = self.config['url']
|
||
self.headers = self.config['headers']
|
||
self.cookies = self.config['cookies']
|
||
self.data = self.config['data']
|
||
self.target_date = self.config['target_date']
|
||
self.rush_hours = self.config['rush_hours']
|
||
self.rush_interval = self.config['rush_interval']
|
||
self.normal_interval = self.config['normal_interval']
|
||
|
||
self.is_running = False
|
||
self.current_mode = "normal"
|
||
self.rush_thread = None
|
||
self.normal_thread = None
|
||
self.stop_event = threading.Event()
|
||
self.rush_active = threading.Event() # 用于在抢课时暂停正常模式
|
||
|
||
self.total_requests = 0
|
||
self.success_count = 0
|
||
self.course_closed_count = 0
|
||
self.already_selected_count = 0
|
||
self.error_count = 0
|
||
self.session = requests.Session()
|
||
|
||
def make_request(self) -> dict:
|
||
"""执行单次请求"""
|
||
start_time = time.time()
|
||
request_time = datetime.now()
|
||
|
||
try:
|
||
response = self.session.post(
|
||
self.url, headers=self.headers, cookies=self.cookies,
|
||
data=self.data, timeout=5
|
||
)
|
||
response_time = time.time() - start_time
|
||
content = response.text
|
||
status, is_success = self._analyze_response(content)
|
||
|
||
self.total_requests += 1
|
||
if is_success:
|
||
self.success_count += 1
|
||
if status == 'course_closed':
|
||
self.course_closed_count += 1
|
||
elif status == 'already_selected':
|
||
self.already_selected_count += 1
|
||
else:
|
||
self.error_count += 1
|
||
|
||
return {
|
||
'timestamp': request_time.isoformat(), 'status_code': response.status_code,
|
||
'response_time': response_time, 'content': content, 'status': status,
|
||
'is_success': is_success, 'mode': self.current_mode
|
||
}
|
||
except Exception as e:
|
||
self.total_requests += 1
|
||
self.error_count += 1
|
||
return {
|
||
'timestamp': request_time.isoformat(), 'status_code': 0,
|
||
'response_time': time.time() - start_time, 'content': f"请求异常: {str(e)}",
|
||
'status': 'error', 'is_success': False, 'mode': self.current_mode
|
||
}
|
||
|
||
def _analyze_response(self, content: str) -> tuple:
|
||
"""分析响应内容"""
|
||
if '当前选课不开放' in content: return 'course_closed', True
|
||
if '已投放' in content: return 'already_selected', True
|
||
if '请不要过快点击' in content or '点击过快' in content: return 'too_fast', False
|
||
if not content.strip(): return 'empty', False
|
||
return 'unknown', False
|
||
|
||
def _log_request(self, result: dict):
|
||
"""记录请求日志"""
|
||
status_emoji = {'course_closed': '🔒', 'already_selected': '✅', 'too_fast': '⚡️',
|
||
'error': '❌', 'empty': '📄', 'unknown': '❓'}.get(result['status'], '❓')
|
||
mode_emoji = "🚀" if result['mode'] == "rush" else "🐌"
|
||
success_mark = "✔️" if result['is_success'] else "✖️"
|
||
|
||
print(f"{result['timestamp']} {mode_emoji} {result['mode'].upper()} {success_mark} "
|
||
f"Code:{result['status_code']} RTT:{result['response_time']:.3f}s "
|
||
f"{status_emoji} {result['status']}")
|
||
|
||
if result['status'] in ['error', 'unknown', 'too_fast'] and result['content']:
|
||
preview = result['content'][:100].replace('\n', ' ').replace('\r', ' ')
|
||
print(f" └── 内容: {preview}...")
|
||
|
||
if self.total_requests % 10 == 0: self._print_stats()
|
||
|
||
def _print_stats(self):
|
||
"""打印统计信息"""
|
||
success_rate = (self.success_count / self.total_requests * 100) if self.total_requests > 0 else 0
|
||
print(f"\n📊 统计 (总计 {self.total_requests} 次请求):")
|
||
print(f" 成功率: {success_rate:.1f}% ({self.success_count}/{self.total_requests}) | "
|
||
f"不开放: {self.course_closed_count} | 已投放: {self.already_selected_count} | "
|
||
f"错误: {self.error_count}\n")
|
||
|
||
def rush_mode_worker(self):
|
||
"""抢课模式工作线程"""
|
||
duration = self.config['rush_duration']
|
||
print(f"🚀 进入抢课模式,持续 {duration} 分钟,间隔 {self.rush_interval}s")
|
||
self.rush_active.set()
|
||
self.current_mode = "rush"
|
||
|
||
end_time = time.time() + (duration * 60)
|
||
while time.time() < end_time and not self.stop_event.is_set():
|
||
result = self.make_request()
|
||
self._log_request(result)
|
||
if result['status'] == 'already_selected':
|
||
print("✅ 选课成功!已投放!脚本将继续运行,可按 Ctrl+C 停止。")
|
||
|
||
if self.stop_event.wait(self.rush_interval): break
|
||
|
||
print("🚀 抢课模式结束")
|
||
self.current_mode = "normal"
|
||
self.rush_active.clear()
|
||
|
||
def normal_mode_worker(self):
|
||
"""正常模式工作线程"""
|
||
print(f"🐌 正常监控模式已启动,间隔 {self.normal_interval}s")
|
||
self.current_mode = "normal"
|
||
|
||
# 同步到下一个10秒周期
|
||
now = datetime.now()
|
||
wait_seconds = self.normal_interval - (now.timestamp() % self.normal_interval)
|
||
if self.stop_event.wait(wait_seconds): return
|
||
|
||
while not self.stop_event.is_set():
|
||
if self.rush_active.is_set():
|
||
if self.stop_event.wait(1): break
|
||
continue
|
||
|
||
result = self.make_request()
|
||
self._log_request(result)
|
||
if self.stop_event.wait(self.normal_interval): break
|
||
|
||
print("🐌 正常监控模式结束")
|
||
|
||
def schedule_rush_session(self, target_time: str):
|
||
"""安排一次抢课会话"""
|
||
|
||
def start_rush():
|
||
if self.rush_thread and self.rush_thread.is_alive():
|
||
print(f"⚠️ 上一个抢课会话仍在进行中,跳过 {target_time}")
|
||
return
|
||
|
||
print(f"⏸️ 抢课时间到,正常监控将自动暂停...")
|
||
self.rush_thread = threading.Thread(target=self.rush_mode_worker, daemon=True)
|
||
self.rush_thread.start()
|
||
|
||
offset = self.config['rush_start_offset']
|
||
target_dt = datetime.strptime(f"{self.target_date} {target_time}", "%Y-%m-%d %H:%M")
|
||
start_dt = target_dt - timedelta(minutes=offset)
|
||
start_time_str = start_dt.strftime("%H:%M")
|
||
|
||
print(f"🗓️ 已安排抢课任务: {target_time} (将于 {start_time_str} 启动)")
|
||
schedule.every().day.at(start_time_str).do(start_rush)
|
||
|
||
def start_scheduler(self):
|
||
"""启动调度器"""
|
||
print("✅ 精确时间选课调度器启动")
|
||
print(f"🎯 目标日期: {self.target_date}")
|
||
|
||
today = datetime.now().strftime("%Y-%m-%d")
|
||
if today != self.target_date:
|
||
print(f"⚠️ 注意: 当前日期({today})不是目标日期({self.target_date})。脚本将在目标日期自动激活。")
|
||
|
||
for rush_hour in self.rush_hours: self.schedule_rush_session(rush_hour)
|
||
print("-" * 60)
|
||
|
||
self.is_running = True
|
||
self.normal_thread = threading.Thread(target=self.normal_mode_worker, daemon=True)
|
||
self.normal_thread.start()
|
||
|
||
try:
|
||
while self.is_running:
|
||
if datetime.now().strftime("%Y-%m-%d") == self.target_date:
|
||
schedule.run_pending()
|
||
time.sleep(1)
|
||
except KeyboardInterrupt:
|
||
print("\n🛑 收到中断信号,正在停止...")
|
||
self.stop()
|
||
|
||
def stop(self):
|
||
"""停止调度器"""
|
||
self.is_running = False
|
||
self.stop_event.set()
|
||
|
||
print("...正在停止所有线程...")
|
||
if self.normal_thread and self.normal_thread.is_alive(): self.normal_thread.join(timeout=5)
|
||
if self.rush_thread and self.rush_thread.is_alive(): self.rush_thread.join(timeout=5)
|
||
|
||
schedule.clear()
|
||
self._print_final_stats()
|
||
print("✅ 调度器已安全停止")
|
||
|
||
def _print_final_stats(self):
|
||
"""打印最终统计"""
|
||
print("\n" + "=" * 25 + " 最终统计报告 " + "=" * 25)
|
||
print(f"总请求数: {self.total_requests}, 成功: {self.success_count}, 错误: {self.error_count}")
|
||
if self.total_requests > 0:
|
||
print(f"成功率: {self.success_count / self.total_requests * 100:.2f}%")
|
||
print(f"选课结果: [不开放: {self.course_closed_count}次], [已投放: {self.already_selected_count}次]")
|
||
print("=" * 68)
|
||
|
||
|
||
def signal_handler(signum, frame):
|
||
"""优雅地处理退出信号"""
|
||
# 这个函数现在是备用的,因为主循环的 KeyboardInterrupt 处理更直接
|
||
print(f"\n🚨 收到信号 {signum},准备退出...")
|
||
# 全局变量 `scheduler` 可能还未定义,所以需要检查
|
||
if 'scheduler' in globals() and scheduler.is_running:
|
||
scheduler.stop()
|
||
sys.exit(0)
|
||
|
||
|
||
def main():
|
||
global scheduler
|
||
|
||
# 检查依赖
|
||
try:
|
||
import schedule
|
||
except ImportError:
|
||
print("❌ 缺少依赖包 `schedule`,请运行: pip install schedule")
|
||
sys.exit(1)
|
||
|
||
scheduler = PreciseCourseScheduler(CONFIG)
|
||
|
||
print("=" * 60)
|
||
print(" 欢迎使用精确时间选课自动化脚本")
|
||
print(" 作者: Galaxy")
|
||
print(" 按 Ctrl+C 可随时停止运行")
|
||
print("=" * 60)
|
||
signal.signal(signal.SIGINT, signal_handler)
|
||
signal.signal(signal.SIGTERM, signal_handler)
|
||
|
||
scheduler.start_scheduler()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main() |