教务系统接口逆向与Python会话管理实战
简介这是一份面向高校学生与Python初学者的教务系统抢课辅助工具包聚焦于解决热门课程名额秒光场景下的自动化选课需求。资源以轻量级Python脚本为核心通过模拟浏览器行为实现对教务页面的高频点击与请求提交适用于SUSTC南方科技大学和GDUFS广东外语外贸大学等采用类似架构的教务系统。压缩包共5个文件含2个核心脚本SUSTC-qiangke.py、GDUFS_version.py、2份说明文档README.md及1份LICENSE协议总大小仅8KB结构简洁、开箱即用。已有1050人学习下载适合希望快速上手自动化操作、理解HTTP请求模拟与基础网页交互逻辑的学习者。读者可直接复用脚本框架结合自身学校登录/选课接口进行适配改造并通过README中的使用说明掌握环境配置、参数调整与常见问题应对方法。1. 这不是“抢课外挂”而是一份教务系统交互逻辑的逆向工程笔记用 Python 拆解登录态维持、课程列表解析与原子化选课请求的完整链路某高校教务系统上线新学期选课时A同学连续三天凌晨蹲守页面手动刷新、点选、提交最终因网络抖动丢失一次关键操作——课程名额在 0.8 秒内被清空。这不是玄学是典型 Web 系统在高并发下暴露的接口设计边界状态强依赖 Cookie SessionID前端防重复提交靠 JS 锁按钮后端却未对同一用户高频选课做服务端限频。这份 Python 脚本本质是一套「可调试、可验证、可中断」的教务系统交互探针它不绕过身份认证而是完整复现浏览器从登录、查课、加课到结果校验的每一步 HTTP 请求它不模拟点击而是直击接口层把“点击”翻译成带签名参数的 POST 请求它不追求全自动抢课而是提供细粒度控制——比如只对某门课重试 5 次、每次间隔 1.2 秒、失败时自动保存响应体供人工分析。适合两类人一是想搞懂教务系统底层通信机制的计算机专业学生二是需要批量处理选课异常如退课重选、跨年级补选的教务助理。它不能帮你“秒杀”但能让你看清“为什么没抢到”。2. 教务系统交互建模为什么必须放弃 Selenium转向 Requests 手动会话管理2.1 教务系统典型架构特征决定技术选型某高校教务系统采用 Java Web 技术栈前端为 Vue.js 单页应用但核心选课逻辑仍由传统 Servlet 接口承载。其关键特征包括登录态强绑定 SessionID登录成功后返回JSESSIONIDABC123后续所有请求必须携带该 Cookie且服务端校验 Session 是否过期、是否被并发挤下线课程列表接口带时间戳签名/course/list?timestamp1715678901234signabcd1234其中sign是MD5(timestamp secret_key)secret_key 埋在前端 JS 中选课请求需二次校验 token提交选课前前端调用/course/token获取一次性 token该 token 有效期仅 30 秒且一次有效响应体含业务码而非 HTTP 状态码即使 HTTP 返回 200响应 JSON 中code: 5001表示“课程已满”code: 5003表示“选课冲突”code: 0才代表成功。Selenium 在此场景下天然失效它无法直接读取前端 JS 计算出的sign参数无法精准捕获/course/token返回的 token 并注入到下一个请求头更无法在页面未加载完成时提前拦截并解析响应体。而 Requests 手动会话管理让我们能把每个环节拆成独立可测单元。2.2 构建可复用的会话管理器Session 对象的初始化与生命周期控制核心是封装一个JWSession类继承自requests.Session并重写request()方法注入通用逻辑import requests import time import hashlib from urllib.parse import urlencode class JWSession(requests.Session): def __init__(self, base_urlhttps://jw.xxx.edu.cn): super().__init__() self.base_url base_url.rstrip(/) self._login_status False self._last_token None self._token_expire 0 def _gen_sign(self, timestamp: int, path: str) - str: 根据教务系统前端 JS 逆向得出的签名算法 secret_key jw_secret_v2_2023 # 实际需从登录后加载的 JS 文件中提取 raw f{path}?timestamp{timestamp}{secret_key} return hashlib.md5(raw.encode()).hexdigest()[:16] def request(self, method, url, **kwargs): # 自动拼接 base_url if not url.startswith((http://, https://)): url f{self.base_url}{url} # 强制添加通用 headers headers kwargs.get(headers, {}) headers.setdefault(User-Agent, Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36) headers.setdefault(X-Requested-With, XMLHttpRequest) kwargs[headers] headers # 自动注入 token如果存在且未过期 if self._last_token and time.time() self._token_expire: headers[X-Token] self._last_token return super().request(method, url, **kwargs)提示secret_key不是硬编码常量。实际项目中应在登录成功后通过GET /static/js/app.xxx.js下载前端 JS用正则rsecret_key\s*\s*[\]([^\])[\]提取。脚本中暂用占位符运行时需动态替换。2.3 登录流程POST 表单 vs JSON 接口如何识别真实登录入口教务系统登录页看似是表单提交但抓包发现真实请求是POST /login/check HTTP/1.1 Content-Type: application/json ... {username:20210001,password:e10adc3949ba59abbe56e057f20f883e,captcha:abcd}密码是 MD5(明文)验证码需 OCR 识别本脚本暂跳过验证码聚焦主干逻辑。登录代码如下def login(self, username: str, password_md5: str, captcha: str ) - bool: payload { username: username, password: password_md5, captcha: captcha } resp self.post(/login/check, jsonpayload) data resp.json() if data.get(code) 0: self._login_status True # 解析 Set-Cookie 中的 JSESSIONIDrequests.Session 已自动处理 print(f[✓] 登录成功SessionID: {self.cookies.get(JSESSIONID, N/A)}) return True else: print(f[✗] 登录失败code{data.get(code)}, msg{data.get(msg)}) return False关键点jsonpayload触发Content-Type: application/json而非datapayload表单格式。若用错服务端直接返回 400。2.4 课程列表获取时间戳签名与分页参数的协同构造接口/course/list要求timestamp和sign且支持分页def get_course_list(self, page: int 1, size: int 20) - list: timestamp int(time.time() * 1000) path /course/list sign self._gen_sign(timestamp, path) params { page: page, size: size, timestamp: timestamp, sign: sign } resp self.get(f{path}?{urlencode(params)}) data resp.json() if data.get(code) ! 0: raise RuntimeError(f获取课程列表失败: {data.get(msg)}) return data.get(data, [])注意urlencode(params)必须在get()之前拼接因为sign计算依赖原始 URL 查询字符串顺序。若用params参数传入Requests 会重新排序导致签名失效。3. 选课原子操作从 token 获取、课程锁定到结果确认的三段式闭环3.1 Token 获取一次性凭证的时效性与幂等性控制选课前必须调用/course/token响应体为{code:0,data:{token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...,expire:1715679200}}该 token 有效期 30 秒且服务端记录使用状态重复提交同一 token 会返回code: 5004token 已使用。因此必须严格管理def _fetch_token(self) - str: resp self.get(/course/token) data resp.json() if data.get(code) ! 0: raise RuntimeError(f获取token失败: {data.get(msg)}) token_data data[data] self._last_token token_data[token] self._token_expire token_data[expire] # Unix 时间戳秒级 return self._last_token def select_course(self, course_id: str, class_id: str) - dict: # 1. 先获取 token try: token self._fetch_token() except Exception as e: return {success: False, error: ftoken获取失败: {e}} # 2. 构造选课请求 payload { courseId: course_id, classId: class_id, token: token # 注意token 同时放 header 和 body } resp self.post(/course/select, jsonpayload) return resp.json()注意部分教务系统要求 token 同时出现在X-TokenHeader 和请求 Body 中缺一不可。这是典型的“双因子校验”设计防止 token 被截获后滥用。3.2 课程锁定机制为什么“选课成功”不等于“已选上”调用/course/select后响应code: 0仅代表请求被接收但课程名额可能在排队中被其他请求抢占。真实状态需轮询/course/status?courseIdxxxdef check_selection_status(self, course_id: str, max_retries: int 5, interval: float 1.0) - dict: for i in range(max_retries): resp self.get(f/course/status?courseId{course_id}) data resp.json() if data.get(code) ! 0: return {success: False, status: query_failed, detail: data.get(msg)} status data[data][status] # waiting, success, failed, full if status in [success, failed, full]: return {success: status success, status: status, data: data[data]} time.sleep(interval) return {success: False, status: timeout, detail: 轮询超时}这就是为什么脚本必须包含状态轮询——教务系统采用“异步选课”模型类似银行排队叫号提交只是取号结果需等待叫号。3.3 失败重试策略基于错误码的差异化重试逻辑不同错误码对应不同处理方式不能无脑重试错误码含义是否重试重试间隔说明5001课程已满否—名额真实耗尽无需重试5002未登录是0.5sSession 过期需重新登录5003选课冲突否—时间冲突需人工调整5004Token 已使用是1.0s需重新 fetch 新 token5005网络超时是2.0s服务端抖动可重试实现逻辑def robust_select(self, course_id: str, class_id: str, max_attempts: int 3) - dict: for attempt in range(max_attempts): try: result self.select_course(course_id, class_id) code result.get(code, -1) if code 0: # 成功提交进入状态轮询 status_result self.check_selection_status(course_id) return {submit_ok: True, **status_result} # 根据错误码决策 if code in [5002, 5004, 5005]: wait_time {5002: 0.5, 5004: 1.0, 5005: 2.0}[code] print(f[!] 尝试 {attempt1}/{max_attempts} 失败code{code}等待 {wait_time}s 后重试...) time.sleep(wait_time) continue else: return {submit_ok: False, code: code, msg: result.get(msg, )} except Exception as e: print(f[!] 尝试 {attempt1}/{max_attempts} 异常: {e}) if attempt max_attempts - 1: time.sleep(1.5) continue return {submit_ok: False, error: 达到最大重试次数}4. 避坑指南五个血泪经验换来的高频翻车点与排查路径4.1 现象登录成功后调用/course/list返回 403 Forbidden原因服务端校验 Referer 头。前端页面访问/course/list时Referer 是https://jw.xxx.edu.cn/course而脚本请求默认无 Referer 或为https://jw.xxx.edu.cn。解决在JWSession.request()中强制设置 Refererheaders.setdefault(Referer, https://jw.xxx.edu.cn/course)4.2 现象/course/select返回code: 5004token 已使用但确认未重复提交原因/course/token接口本身也受 Referer 限制且其返回的 token 与请求 Referer 绑定。若获取 token 时 Referer 是/login而选课时 Referer 是/coursetoken 会被拒绝。解决统一所有教务相关请求的 Referer 为/course并在_fetch_token()中显式指定resp self.get(/course/token, headers{Referer: https://jw.xxx.edu.cn/course})4.3 现象课程列表数据为空data: []但浏览器能正常显示原因前端 JS 动态拼接了额外参数如?deptIdALLyear2024term1而脚本只传了分页参数。解决用浏览器开发者工具 Network 面板筛选 XHR 请求找到真实发送的/course/list完整 URL复制全部 Query 参数转为字典传入params。切勿凭经验猜测。4.4 现象select_course()返回code: 0但轮询/course/status始终返回status: waiting超时后变成failed原因课程存在“预选阶段”和“正选阶段”脚本在预选阶段提交但系统只在正选开始后才处理队列。/course/status的waiting是正常状态不代表失败。解决增加阶段判断逻辑。先 GET/system/config获取当前选课阶段标识如phase: preselect若为预选则轮询逻辑改为等待阶段切换而非等待success。4.5 现象脚本运行数分钟后所有请求开始返回code: 5002未登录但 SessionID 未变原因服务端设置了 Session 最大活跃时间如 15 分钟且检测到无操作即主动销毁。浏览器因有心跳请求如定时拉取通知维持 Session脚本没有。解决在JWSession中添加后台心跳线程或在关键操作前插入保活请求def keep_alive(self): self.get(/user/info) # 一个轻量级、不改变状态的接口并在robust_select()开头调用self.keep_alive()。5. 生产级加固从“能跑”到“稳跑”的四个落地技巧5.1 请求节流用指数退避替代固定间隔避免触发服务端风控教务系统通常部署 WAF对同一 IP 短时高频请求如 1 秒内 5 次会返回 429 Too Many Requests。固定间隔如time.sleep(1)在高并发下极易撞线。改用指数退避import random def exponential_backoff(attempt: int) - float: 计算第 attempt 次重试的等待时间秒 base 0.5 max_wait 5.0 wait min(base * (2 ** attempt) random.uniform(0, 0.5), max_wait) return wait # 在 robust_select 的重试循环中 if code in [5002, 5004, 5005]: wait_time exponential_backoff(attempt) time.sleep(wait_time)这样第一次重试等 0.5~1.0 秒第二次等 1.0~1.5 秒第三次等 2.0~2.5 秒……既降低被限频概率又避免无限等待。5.2 响应快照自动保存失败请求的完整上下文告别“黑匣子”调试每次请求失败不仅打印错误更要保存原始请求和响应import json from datetime import datetime def safe_request(self, method, url, **kwargs): try: resp self.request(method, url, **kwargs) return resp except Exception as e: # 保存快照 snapshot { timestamp: datetime.now().isoformat(), method: method, url: url, kwargs: {k: v for k, v in kwargs.items() if k ! headers}, error: str(e), session_cookies: dict(self.cookies), } filename fdebug_{int(time.time())}_{method}_{hash(url) % 10000}.json with open(filename, w, encodingutf-8) as f: json.dump(snapshot, f, ensure_asciiFalse, indent2) print(f[DEBUG] 错误快照已保存至 {filename}) raise下次遇到问题直接打开 JSON 文件看到完整的 Cookie、时间戳、请求参数比看日志快十倍。5.3 课程匹配引擎用模糊匹配替代精确 ID解决前端展示名与后端 ID 不一致教务系统中前端显示“高等数学A”后端 courseId 可能是MATH101_2024A且每学期 ID 都变。脚本不应硬编码 ID而应构建匹配规则def find_course_by_name(self, keyword: str, max_results: int 5) - list: 按关键词模糊搜索课程返回匹配的 course_id 列表 all_courses [] page 1 while len(all_courses) 1000: # 防止无限分页 courses self.get_course_list(pagepage, size50) if not courses: break for c in courses: # 多字段匹配课程名、教师名、上课周次 match_score 0 if keyword.lower() in c.get(name, ).lower(): match_score 2 if keyword.lower() in c.get(teacher, ).lower(): match_score 1 if 周 in keyword and c.get(week_info, ): match_score 1 if match_score 0: all_courses.append({ id: c[id], name: c[name], teacher: c[teacher], score: match_score, capacity: c.get(capacity, 0), selected: c.get(selected, 0) }) page 1 # 按匹配分排序取前 max_results return sorted(all_courses, keylambda x: x[score], reverseTrue)[:max_results] # 使用示例 matches session.find_course_by_name(高数) if matches: target matches[0] print(f匹配到: {target[name]} ({target[id]}), 剩余名额: {target[capacity] - target[selected]})这样输入高数、王老师、周三都能命中大幅提升脚本鲁棒性。5.4 环境隔离用配置文件驱动行为一份代码适配多所高校不同学校接口路径、签名算法、错误码含义均不同。将差异点抽离为config.yamluniversity: xxx_uni base_url: https://jw.xxx.edu.cn login_endpoint: /login/check course_list_endpoint: /course/list sign_method: md5_timestamp_secret secret_key: jw_secret_v2_2023 error_codes: full: 5001 conflict: 5003 token_used: 5004加载逻辑import yaml def load_config(config_path: str) - dict: with open(config_path, r, encodingutf-8) as f: return yaml.safe_load(f) # 在 JWSession.__init__ 中 self.config load_config(config.yaml) self.base_url self.config[base_url] def _gen_sign(self, timestamp: int, path: str) - str: if self.config[sign_method] md5_timestamp_secret: secret self.config[secret_key] raw f{path}?timestamp{timestamp}{secret} return hashlib.md5(raw.encode()).hexdigest()[:16] # 可扩展其他算法...从那以后我每次接手新学校的教务系统第一件事就是抓包分析接口然后更新config.yaml从不碰核心JWSession代码——它已经成了我的教务系统交互标准库。这套模式让我在三个不同技术栈的教务系统Java/Spring、.NET、PHP上都实现了 1 小时内完成适配。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

教务系统自动化选课脚本:Requests+Selenium高并发调度实践

教务系统自动化选课脚本:Requests+Selenium高并发调度实践

简介:这是一份面向高校学生与Python初学者的教务系统抢课辅助工具包,聚焦于解决热门课程名额秒光场景下的自动化选课需求。资源基于Selenium等库实现网页级模拟点击,提供针对南方科技大学(SUSTC)和广东外语外贸大学&am…

2026/10/17 19:07:26 阅读更多 →
Java程序员转大模型开发:实操路线与高薪技能栈解析

Java程序员转大模型开发:实操路线与高薪技能栈解析

很多人问我,Java程序员现在还能不能上车大模型开发?我的回答是:不仅来得及,而且可能是最划算的一批人。过去几年我从Java后端一路做到大模型应用方向,带过不少转行的团队新人,也踩过不少坑。这篇内容不打算…

2026/10/17 19:06:24 阅读更多 →
数据库课程设计实战:VFP学校图书借阅管理系统完整方案

数据库课程设计实战:VFP学校图书借阅管理系统完整方案

简介:《学校图书借阅管理系统》是一份数据库系统设计方向的课程设计报告,适合高校计算机、信息管理等专业学生用于完成数据库课程设计、毕业设计或了解图书借阅系统的完整建设过程。报告内容系统性强,包含设计内容与要求、数据字典、数据流图…

2026/10/17 19:06:24 阅读更多 →

最新新闻

从零开发轻量记事本:IndexedDB存储、自动保存与同步实践

从零开发轻量记事本:IndexedDB存储、自动保存与同步实践

记事本这玩意儿,看着简单,做起来全是坑。之前我接了一个内部工具需求,要做一款能随手记录、随时翻查、离线也不慌的轻量记事本,产品代号叫“轻记”。从UI线框到功能实现,前后大概花了三周时间,中间踩了不少…

2026/10/18 0:23:12 阅读更多 →
SysML与UML核心区别:系统工程建模的底层逻辑

SysML与UML核心区别:系统工程建模的底层逻辑

简介:本资源是系统工程领域权威入门与进阶指南《Systems Engineering with SysML/UML》,面向系统工程师、嵌入式/航空航天/汽车/医疗设备领域研发人员及高校相关专业师生,聚焦OMG标准下SysML与UML协同建模方法,解决复杂系统需求追…

2026/10/18 0:23:12 阅读更多 →
Matplotlib三维绘图避坑指南:从空白图到工程级可视化

Matplotlib三维绘图避坑指南:从空白图到工程级可视化

简介:本资源是一份面向Python初学者与数据可视化学习者的matplotlib三维绘图实战指南,聚焦解决“如何用matplotlib快速绘制可复现、可定制的三维曲面图”这一核心问题。内容以两个完整可运行示例为核心:其一基于笛卡尔坐标系,使用…

2026/10/18 0:22:12 阅读更多 →
智谱清华唐杰:LLM Memory最新全景综述——用TaoToken统一Key复现记忆模块评测

智谱清华唐杰:LLM Memory最新全景综述——用TaoToken统一Key复现记忆模块评测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:21:11 阅读更多 →
跳出代码,彻底看懂 MCP 与 Function Calling 的底层关系:TaoToken 统一 Key 通道下的调用链路拆解

跳出代码,彻底看懂 MCP 与 Function Calling 的底层关系:TaoToken 统一 Key 通道下的调用链路拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:21:11 阅读更多 →
地理空间坐标系统实战:用TaoToken统一Key打通七参数坐标转换链路

地理空间坐标系统实战:用TaoToken统一Key打通七参数坐标转换链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:21:11 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →