最近一段时间以来,sdcme.net.cn山东省继续医学教育服务平台开始了学习,但是网站三天两头维护,不维护的时间还卡的要死。
本人实在无奈,委托AI帮我写了这段自动化学习脚本。需要的朋友可以自取。但是关于动态验证码的识别接口为本人自建,服务器性能有限,故不能随时免费提供。需要的可以联系我索要源码。下方为自动化学习脚本源代码(不带记录核查功能)记录核查版本的已经封存,需要的可以联系。
MAX_CONCURRENT_USERS = 25
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
山东医学教育平台自动化学习系统
"""
import os
import json
import time
import random
import string
import logging
import sqlite3
import threading
from datetime import datetime
from typing import Dict, List, Optional, Tuple
from urllib.parse import quote
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# ==================== 配置区 ====================
class Config:
BASE_URL = "https://course.sdcme.net.cn"
# --- OCR 接口配置区 ---
OCR_URL = "http://您自己的ocr识别接口"
# (注:卡密和账号名全部改为动态传入,不在代码里写死任何校验信息)
# ----------------------
HEARTBEAT_INTERVAL = 60
ROLL_CALL_DELAY = (5.0, 15.0)
REQUEST_TIMEOUT = 60
MAX_RETRIES = 5
RETRY_DELAY = 5
MIN_WAIT_TIME = 180
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
SKELETON_FILE = os.path.join(BASE_DIR, "global_skeleton.json")
COURSE_LEARN_ORDER = ['18', '12', '13']
SKELETON_LOCK = threading.Lock()
# ==================== 日志配置(双通道分离) ====================
logger = logging.getLogger('sdcme_learner')
logger.setLevel(logging.DEBUG)
logger.handlers.clear()
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s', datefmt='%H:%M:%S')
console_handler.setFormatter(console_formatter)
logger.addHandler(console_handler)
log_file_path = os.path.join(Config.BASE_DIR, 'sdcme.log')
file_handler = logging.FileHandler(log_file_path, encoding='utf-8', mode='a')
file_handler.setLevel(logging.DEBUG)
file_formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)
logging.getLogger('urllib3').setLevel(logging.CRITICAL)
# ==================== 数据库管理器 ====================
class DBManager:
def __init__(self, username: str):
self.db_file = os.path.join(Config.BASE_DIR, f"{username}_sdcme.db")
self.conn = sqlite3.connect(self.db_file, check_same_thread=False)
try:
self.conn.execute('PRAGMA journal_mode=DELETE;')
except:
pass
self._init_tables()
def _init_tables(self):
cursor = self.conn.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, cookies TEXT, cookies_expire DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP)''')
cursor.execute('''CREATE TABLE IF NOT EXISTS global_courses (course_id TEXT PRIMARY KEY, title TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)''')
cursor.execute('''CREATE TABLE IF NOT EXISTS global_videos (video_id TEXT PRIMARY KEY, course_id TEXT, title TEXT, duration INTEGER, sort_index INTEGER, FOREIGN KEY(course_id) REFERENCES global_courses(course_id))''')
cursor.execute('''CREATE TABLE IF NOT EXISTS video_progress (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, video_id TEXT NOT NULL, course_id TEXT, progress INTEGER DEFAULT 0, has_checkin BOOLEAN DEFAULT 0, has_anomaly BOOLEAN DEFAULT 0, status TEXT DEFAULT 'pending', updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id), FOREIGN KEY(video_id) REFERENCES global_videos(video_id), UNIQUE(user_id, video_id))''')
cursor.execute('''CREATE TABLE IF NOT EXISTS heartbeat_trace (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, video_id TEXT, request TEXT, response TEXT, has_anomaly BOOLEAN DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id))''')
self.conn.commit()
def add_user(self, username: str, password: str) -> int:
cursor = self.conn.cursor()
try:
cursor.execute('INSERT INTO users (username, password) VALUES (?, ?)', (username, password))
self.conn.commit()
return cursor.lastrowid
except sqlite3.IntegrityError:
cursor.execute('SELECT id FROM users WHERE username = ?', (username,))
return cursor.fetchone()[0]
def get_user(self, username: str) -> Optional[Dict]:
cursor = self.conn.cursor()
cursor.execute('SELECT * FROM users WHERE username = ?', (username,))
result = cursor.fetchone()
if result:
return {'id': result[0], 'username': result[1], 'password': result[2], 'cookies': result[3]}
return None
def update_user_cookies(self, user_id: int, cookies: str, expire_hours: int = 24):
expire_time = datetime.now().timestamp() + expire_hours * 3600
cursor = self.conn.cursor()
cursor.execute('UPDATE users SET cookies = ?, cookies_expire = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', (cookies, expire_time, user_id))
self.conn.commit()
def check_global_skeleton_exists(self) -> bool:
cursor = self.conn.cursor()
cursor.execute('SELECT COUNT(*) FROM global_videos')
return cursor.fetchone()[0] > 0
def save_global_skeleton(self, course_id: str, course_title: str, videos: List[Dict]):
cursor = self.conn.cursor()
cursor.execute('INSERT OR IGNORE INTO global_courses (course_id, title) VALUES (?, ?)', (course_id, course_title))
for idx, v in enumerate(videos):
cursor.execute('INSERT OR IGNORE INTO global_videos (video_id, course_id, title, duration, sort_index) VALUES (?, ?, ?, ?, ?)',
(v['videoId'], course_id, v['title'], v['duration'], idx))
self.conn.commit()
def get_all_global_video_ids(self) -> List[str]:
cursor = self.conn.cursor()
cursor.execute('SELECT video_id FROM global_videos')
return [row[0] for row in cursor.fetchall()]
def get_all_global_videos_info(self) -> List[Tuple[str, str]]:
cursor = self.conn.cursor()
cursor.execute('SELECT video_id, course_id FROM global_videos')
return cursor.fetchall()
def save_video_progress(self, user_id: int, video_id: str, course_id: str, progress: int):
status = 'completed' if progress >= 100 else 'pending'
cursor = self.conn.cursor()
cursor.execute('''INSERT OR REPLACE INTO video_progress (user_id, video_id, course_id, progress, status, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)''', (user_id, video_id, course_id, progress, status))
self.conn.commit()
def get_pending_tasks_ordered(self, user_id: int, order_list: List[str]) -> List[Dict]:
cursor = self.conn.cursor()
cursor.execute('''
SELECT p.video_id, p.course_id, p.progress, v.title, v.duration, v.sort_index
FROM video_progress p
JOIN global_videos v ON p.video_id = v.video_id
WHERE p.user_id = ? AND p.progress < 100
''', (user_id,))
tasks = []
for row in cursor.fetchall():
tasks.append({
'video_id': row[0], 'course_id': str(row[1]), 'progress': row[2],
'title': row[3], 'duration': row[4], 'sort_index': row[5]
})
def sort_key(task):
try:
c_order = order_list.index(task['course_id'])
except ValueError:
c_order = 9999
return (c_order, task['sort_index'])
tasks.sort(key=sort_key)
return tasks
def get_user_videos(self, user_id: int) -> List[Dict]:
cursor = self.conn.cursor()
cursor.execute('SELECT * FROM video_progress WHERE user_id = ?', (user_id,))
results = []
for row in cursor.fetchall():
results.append({'video_id': row[2], 'title': row[3], 'duration': row[7], 'progress': row[6]})
return results
def save_heartbeat(self, user_id: int, video_id: str, request: Dict, response: Dict):
cursor = self.conn.cursor()
cursor.execute('INSERT INTO heartbeat_trace (user_id, video_id, request, response, has_anomaly) VALUES (?, ?, ?, ?, ?)', (user_id, video_id, json.dumps(request), json.dumps(response), False))
self.conn.commit()
def generate_session_id(length: int = 8) -> str:
return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(length))
def get_timestamp() -> int:
return int(time.time() * 1000)
# ==================== 学习系统类 ====================
class SDCMEEAutoLearn:
def __init__(self, username: str, password: str, token: str):
self.username = username
self.password = password
self.token = token # 接收调度器传来的专属卡密
self.db = DBManager(self.username)
self.user_id = self.db.add_user(username, password)
self.has_local_cookies = False
self.session = self._create_session()
self.course_tree_cached = False
logger.info(f"[{self.username}] 系统初始化完成,专属数据库 {self.username}_sdcme.db 就绪。")
def _create_session(self) -> requests.Session:
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
"Referer": f"{Config.BASE_URL}/"
})
retry_strategy = Retry(total=Config.MAX_RETRIES, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504])
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
user = self.db.get_user(self.username)
if user and user.get('cookies'):
try:
cookies_dict = json.loads(user['cookies'])
if cookies_dict and isinstance(cookies_dict, dict):
for name, value in cookies_dict.items():
session.cookies.set(name, value, domain=".sdcme.net.cn")
self.has_local_cookies = True
logger.info(f"[{self.username}] 已从专属数据库加载历史Cookie。")
except Exception as e:
logger.warning(f"[{self.username}] 历史Cookie解析失败: {e}")
return session
def _save_cookies(self):
try:
cookies_dict = self.session.cookies.get_dict()
self.db.update_user_cookies(self.user_id, json.dumps(cookies_dict))
except Exception as e:
logger.error(f"[{self.username}] 保存Cookie失败: {e}")
def _check_login_status(self) -> bool:
if not self.has_local_cookies:
return False
logger.info(f"[{self.username}] 正在验证本地凭证是否过期...")
try:
req_start = time.time()
resp = self.session.get(f"{Config.BASE_URL}/", timeout=Config.REQUEST_TIMEOUT, allow_redirects=False)
cost = round(time.time() - req_start, 2)
if resp.status_code == 200 and "登录" not in resp.text[:2000]:
logger.info(f"[{self.username}] 凭证有效校验完成 (耗时: {cost}s)")
return True
return False
except Exception:
return False
def _retry_on_failure(self, func, *args, **kwargs):
for attempt in range(Config.MAX_RETRIES):
try:
return func(*args, **kwargs)
except (requests.exceptions.RequestException, requests.exceptions.JSONDecodeError) as e:
wait_time = min(Config.RETRY_DELAY * (attempt + 1) * 5, Config.MIN_WAIT_TIME)
err_str = str(e)
if "Timeout" in err_str or "timed out" in err_str:
clean_msg = "网络连接超时,平台服务器卡顿"
elif "10061" in err_str or "积极拒绝" in err_str:
clean_msg = "平台服务器宕机或拒绝连接"
elif "502" in err_str or "503" in err_str or "504" in err_str:
clean_msg = f"平台网关异常 ({err_str[:15]}...)"
elif "Expecting value" in err_str:
clean_msg = "接口返回了空白或无效数据"
else:
clean_msg = "网络请求波动"
logger.warning(f"[{self.username}] ⚡ {clean_msg},等待 {wait_time}秒后重试...")
logger.debug(f"[{self.username}] 详细报错追踪: {err_str}", exc_info=True)
time.sleep(wait_time)
raise Exception(f"[{self.username}] 多次重试后仍然失败,终止当前请求。")
def login(self) -> bool:
logger.info(f"[{self.username}] 开始执行账号密码登录流程...")
for attempt in range(Config.MAX_RETRIES):
try:
session_id = generate_session_id()
captcha_url = f"{Config.BASE_URL}/login/captcha?sessionId={session_id}&t={get_timestamp()}"
response = self.session.get(captcha_url, timeout=Config.REQUEST_TIMEOUT)
# 【核心修复】:对中文账号进行安全的 URL 编码,防止 Header 报 latin-1 错误!
ocr_headers = {
"X-Token": self.token,
"X-User-ID": quote(self.username)
}
files = {'file': ('captcha.gif', response.content, 'image/gif')}
try:
ocr_resp = requests.post(Config.OCR_URL, files=files, headers=ocr_headers, timeout=Config.REQUEST_TIMEOUT)
if ocr_resp.status_code == 200:
captcha_code = ocr_resp.json().get("result", "")
else:
logger.warning(f"[{self.username}] OCR拦截或失败 | 状态码: {ocr_resp.status_code} | 详情: {ocr_resp.text}")
time.sleep(Config.RETRY_DELAY)
continue
except Exception as e:
logger.error(f"[{self.username}] OCR网络请求异常: {e}")
time.sleep(Config.RETRY_DELAY)
continue
if not captcha_code:
time.sleep(Config.RETRY_DELAY)
continue
logger.info(f"[{self.username}] 登录验证码识别结果: 【{captcha_code}】")
login_data = {'username': self.username, 'password': self.password, 'captchaCode': captcha_code, 'captchaSessionId': session_id}
headers = {'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json'}
req_start = time.time()
response = self.session.post(f"{Config.BASE_URL}/login", json=login_data, headers=headers, timeout=Config.REQUEST_TIMEOUT)
cost = round(time.time() - req_start, 2)
result = response.json()
if result.get('success') or result.get('code') == 200:
self._save_cookies()
logger.info(f"[{self.username}] ✓ 登录成功并更新本地Cookie库!(耗时: {cost}s)")
return True
time.sleep(Config.RETRY_DELAY)
except Exception as e:
pass
return False
def confirm_agreement(self) -> bool:
try:
self.session.get(f"{Config.BASE_URL}/agreement", timeout=Config.REQUEST_TIMEOUT)
return True
except:
return False
def get_course_categories(self) -> List[Dict]:
def do_req():
response = self.session.get(f"{Config.BASE_URL}/courses", timeout=Config.REQUEST_TIMEOUT)
response.raise_for_status()
return response.json().get('data', [])
return self._retry_on_failure(do_req)
def get_course_videos(self, course_id: str) -> List[Dict]:
def do_req():
response = self.session.get(f"{Config.BASE_URL}/coursewares/course/{course_id}", timeout=Config.REQUEST_TIMEOUT)
response.raise_for_status()
return response.json().get('data', [])
return self._retry_on_failure(do_req)
def get_batch_progress(self, video_ids: List[str]) -> Dict:
if not video_ids:
return {}
chunk_size = 30
all_progress_data = {}
for i in range(0, len(video_ids), chunk_size):
chunk = video_ids[i:i + chunk_size]
def do_req():
url = f"{Config.BASE_URL}/video/play/progress/batch?videoIds={','.join(chunk)}"
response = self.session.get(url, timeout=Config.REQUEST_TIMEOUT)
response.raise_for_status()
return response.json()
try:
result = self._retry_on_failure(do_req)
chunk_data = result.get('data', {}) if isinstance(result, dict) else {}
if isinstance(chunk_data, dict):
all_progress_data.update(chunk_data)
except Exception as e:
logger.error(f"[{self.username}] 批量获取进度(部分批次)失败,将跳过本批次。")
logger.debug(f"[{self.username}] 批次获取失败详情: {e}", exc_info=True)
return {'data': all_progress_data}
def load_or_fetch_skeleton(self):
if self.db.check_global_skeleton_exists():
return
with SKELETON_LOCK:
if os.path.exists(Config.SKELETON_FILE):
try:
with open(Config.SKELETON_FILE, 'r', encoding='utf-8') as f:
skeleton_data = json.load(f)
logger.info(f"[{self.username}] 🌍 检测到共享 JSON 图纸,直接复用本地公共骨架...")
except Exception:
skeleton_data = []
else:
logger.info(f"[{self.username}] ✨ 暂无公共图纸!作为开拓者,开始向云端拉取全量课程骨架...")
courses = self.get_course_categories()
skeleton_data = []
total_saved = 0
for course in courses:
course_id = str(course.get('id', ''))
course_title = course.get('title', '未知')
videos = self.get_course_videos(course_id)
if not videos: continue
v_list = []
for v in videos:
if v.get('fileName'):
v_list.append({'videoId': v.get('fileName'), 'title': v.get('title', v.get('name')), 'duration': v.get('duration', 0)})
skeleton_data.append({'course_id': course_id, 'course_title': course_title, 'videos': v_list})
total_saved += len(v_list)
with open(Config.SKELETON_FILE, 'w', encoding='utf-8') as f:
json.dump(skeleton_data, f, ensure_ascii=False, indent=2)
logger.info(f"[{self.username}] 🌍 共享图纸建立完成!(共 {total_saved} 个视频)")
for course_data in skeleton_data:
self.db.save_global_skeleton(course_data['course_id'], course_data['course_title'], course_data['videos'])
def sync_global_and_personal_progress(self):
self.confirm_agreement()
self.load_or_fetch_skeleton()
global_video_ids = self.db.get_all_global_video_ids()
if not global_video_ids:
return
logger.info(f"[{self.username}] 正在向云端发包对账最新进度 (分批请求)...")
req_start = time.time()
progress_map = self.get_batch_progress(global_video_ids)
cost = round(time.time() - req_start, 2)
actual_progress_map = progress_map.get('data', progress_map) if isinstance(progress_map, dict) else {}
if not actual_progress_map:
logger.warning(f"[{self.username}] 警告:未获取到有效的进度字典。")
return
global_v_data = self.db.get_all_global_videos_info()
for row in global_v_data:
vid, cid = row[0], row[1]
prog = actual_progress_map.get(vid, 0)
self.db.save_video_progress(self.user_id, vid, cid, prog)
logger.info(f"[{self.username}] ✓ 个人独立进度同步完成!(接口耗时: {cost}s)")
def _build_play_referer(self, video_id: str, title: str) -> str:
encoded_title = quote(title, safe='')
return f"https://course.sdcme.net.cn/play/index.html?vid={video_id}&title={encoded_title}"
def init_play_session(self, video_id: str, title: str) -> bool:
referer = self._build_play_referer(video_id, title)
self.session.headers.update({'Referer': referer})
def do_init():
self.session.get(f"{Config.BASE_URL}/playapi/me", timeout=Config.REQUEST_TIMEOUT).raise_for_status()
self.session.get(f"{Config.BASE_URL}/playapi/vod/playauth", params={'videoId': video_id}, timeout=Config.REQUEST_TIMEOUT).raise_for_status()
self.session.get(f"{Config.BASE_URL}/playapi/vod/progress", params={'videoId': video_id}, timeout=Config.REQUEST_TIMEOUT).raise_for_status()
return True
try:
return self._retry_on_failure(do_init)
except Exception:
return False
def send_heartbeat(self, video_id: str, progress_seconds: int, duration_seconds: int) -> Dict:
def do_req():
payload = {'videoId': video_id, 'progressSeconds': progress_seconds, 'durationSeconds': duration_seconds}
response = self.session.post(f"{Config.BASE_URL}/playapi/vod/progress", json=payload, timeout=Config.REQUEST_TIMEOUT)
response.raise_for_status()
return payload, response.json()
payload, result = self._retry_on_failure(do_req)
self.db.save_heartbeat(self.user_id, video_id, payload, result)
return result
def handle_roll_call(self, video_id: str, command_id: str) -> bool:
delay = random.uniform(*Config.ROLL_CALL_DELAY)
logger.info(f"[{self.username}] 模拟点名反应延迟: {delay:.1f}s")
time.sleep(delay)
def do_req():
response = self.session.post(f"{Config.BASE_URL}/playapi/vod/roll-call/checkin", json={'videoId': video_id, 'commandId': command_id}, timeout=Config.REQUEST_TIMEOUT)
return response.json()
try:
req_start = time.time()
result = self._retry_on_failure(do_req)
cost = round(time.time() - req_start, 2)
res_bool = bool(result.get('checkedIn'))
logger.info(f"[{self.username}] 点名提交反馈: {res_bool} (耗时: {cost}s)")
return res_bool
except:
return False
def play_video(self, video: Dict) -> bool:
video_id, title, duration = video['video_id'], video['title'], video['duration']
logger.info(f"[{self.username}] ▶ 开始学习: [课程:{video['course_id']}] {title} ({duration}秒)")
if not self.init_play_session(video_id, title): return False
current_seconds = video.get('progress', 0) * duration // 100
while current_seconds < duration:
next_seconds = min(current_seconds + Config.HEARTBEAT_INTERVAL, duration)
try:
req_start = time.time()
result = self.send_heartbeat(video_id, next_seconds, duration)
cost = round(time.time() - req_start, 2)
if result.get('command') == 'ROLL_CALL':
logger.info(f"[{self.username}] >>> 捕获到随机点名指令 (ROLL_CALL) <<<")
if not self.handle_roll_call(video_id, result.get('commandId')): return False
progress_percent = int(next_seconds * 100 / duration)
logger.info(f"[{self.username}] 进度: {progress_percent}% ({next_seconds}/{duration}秒) - 心跳发送成功 (耗时: {cost}s)")
self.db.save_video_progress(self.user_id, video_id, video['course_id'], progress_percent)
current_seconds = next_seconds
if current_seconds < duration:
time.sleep(Config.HEARTBEAT_INTERVAL)
except Exception as e:
logger.warning(f"[{self.username}] ⚠ 视频 [{title}] 学习意外中断。")
logger.debug(f"[{self.username}] 播放异常拦截: {e}", exc_info=True)
return False
logger.info(f"[{self.username}] ✓ 单个视频学习完成: {title}")
self.db.save_video_progress(self.user_id, video_id, video['course_id'], 100)
return True
def _execute_learning_tasks(self, tasks: List[Dict]):
for db_task in tasks:
success = self.play_video(db_task)
if not success:
logger.info(f"[{self.username}] ⚠ 本轮播放被中断,准备重新对账。")
break
def run(self):
logger.info(f"[{self.username}] === 山东医学教育平台自动化学习系统启动 ===")
if self._check_login_status():
logger.info(f"[{self.username}] ✓ 检测到有效Cookie,免密登录成功!")
else:
logger.info(f"[{self.username}] ⚠ 本地凭证无效或缺失,回退至账号密码登录...")
if not self.login():
logger.error(f"[{self.username}] ✗ 登录彻底失败,中断执行。")
return
loop_counter = 1
while True:
logger.info(f"[{self.username}] === [步骤 1] 获取/更新全量课程进度至专属数据库 (第 {loop_counter} 轮) ===")
self.sync_global_and_personal_progress()
pending_tasks = self.db.get_pending_tasks_ordered(self.user_id, Config.COURSE_LEARN_ORDER)
if not pending_tasks:
logger.info(f"[{self.username}] 🎉 恭喜!专属数据库内校验所有视频进度已 100%!")
logger.info(f"[{self.username}] >>> 学习圆满完成,安全退出。 <<<")
break
logger.info(f"[{self.username}] === [步骤 2] 按指定顺序执行学习任务 (剩余 {len(pending_tasks)} 个未满进度视频) ===")
self._execute_learning_tasks(pending_tasks)
logger.info(f"[{self.username}] === [休整] 准备重新对账 ===")
loop_counter += 1
time.sleep(3)
# ==================== 主入口 ====================
def main():
import sys
if len(sys.argv) < 4:
print("使用方法: python ceshi.py <用户名> <密码> <卡密>")
return
learner = SDCMEEAutoLearn(sys.argv[1], sys.argv[2], sys.argv[3])
learner.run()
if __name__ == '__main__':
main() 
184585333 · 1 个月前