news 2026/9/17 16:32:36

Python解析PPTX薪酬白皮书:表格图表抽取与数据清洗

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python解析PPTX薪酬白皮书:表格图表抽取与数据清洗

简介:本资源为《2024年毕业生薪酬白皮书》配套演示文稿,面向即将毕业的高校学生、就业指导教师及职业规划从业者,帮助读者系统了解当前就业市场的薪酬水平、影响因素与行业趋势,为求职定位与薪资谈判提供数据参考。资源包含1个pptx文件,压缩包约1001KB,以图文幻灯片形式组织章节内容,便于课堂展示或个人快速浏览。已有294人学习下载,具备一定的参考热度。内容覆盖毕业生总体薪酬概况与城市差异、学历与专业对薪酬的影响、热门专业与人文社科专业的起薪对比、个人能力与院校背景的作用,以及2024年薪酬预期与实际薪酬的差异分析;同时对金融、科技、医疗等行业前景及数据分析、数字化、人工智能等技能带来的薪酬变化作出预测,并给出提升竞争力的建议,适合用于就业指导课件制作与个人职业方向研判。

1. 别再用截图和手敲:一份薪酬白皮书其实是结构化数据源

很多人拿到《2024年毕业生薪酬白皮书.pptx》,动作是打开、翻到第 12 页、截图、再对着 Excel 一格一格敲「平均月薪」「中位数」。四十页、十几张表、七八张柱状图,敲完一下午没了,还容易把「年包 18.6 万」抄成「月薪 18.6 千」。反直觉的地方在于:这份 PPT 里的每一个数字,本来就躺在机器可读的位置——pptx 只是一包 zip,表格在ppt/slides/slideN.xml<a:tbl>里,图表数值在ppt/charts/chartN.xml<c:numCache>里,图表背后的原始工作表还作为 embedded xlsx 打包在ppt/embeddings/目录下。

这篇要解决的是把它变成可复用的数据资产:用 python-pptx 遍历页与形状,抽出全部薪酬表和图表序列,落成一张长表,再处理分位数口径、城市行业归一化、同比增速这些真正决定分析对不对的问题。适合要做行业薪酬分析、搭看板、或者把白皮书数据并进自己数仓的数据与后端同学;只想确认「读 PPT 该用哪个库」的,看完第 2、3 章就能直接抄。

2. PPTX 的 OOXML 结构与 python-pptx 对象模型怎么对上

2.1 先别急着写代码:把白皮书当压缩包拆开看目录

.pptx后缀只是包装,内核是 OPC(Open Packaging Conventions)包:一个 zip,里面是若干 XML 部件加资源文件,部件之间靠_rels/*.rels互相引用。所以第一步不是装库,是unzip看一眼真实目录,这比读任何文档都快。

mkdir -p /tmp/wp && cd /tmp/wp unzip -o "2024年毕业生薪酬白皮书.pptx" -d unpacked # 看一级目录结构,确认有没有图表与嵌入工作簿 find unpacked/ppt -maxdepth 2 -type d | sort # 图表数值、嵌入表格、备注页分别在哪儿 ls unpacked/ppt/charts unpacked/ppt/embeddings unpacked/ppt/notesSlides 2>/dev/null || true

unzip -o表示覆盖同名文件,-d指定解包目录。注意ppt/chartsppt/embeddings并非必然存在,纯文本排版的 PPT 就没有,所以用|| true兜住报错,别让脚本在这里断掉。

路径里面是什么抽取时的用途
ppt/slides/slideN.xml该页的形状树标题文本框、表格<a:tbl>
ppt/slides/_rels/slideN.xml.rels该页引用的外部部件顺藤摸到 chart、图片、嵌入对象
ppt/charts/chartN.xml图表定义与数值缓存系列名、分类轴、每个数据点
ppt/embeddings/*.xlsx图表背后的工作表还原完整数据、补齐缓存缺失值
ppt/notesSlides/notesSlideN.xml演讲者备注样本量、口径说明、数据来源注释
ppt/media/图片资源被贴成图片的图表只能走 OCR

最值得记住的一点:图表数值有两份。chartN.xml里的numCache是渲染快照,embeddings下的 xlsx 是原始表。作者在 PPT 里正常编辑时两者一致;但只要做过「粘贴为图片」或者后期手改过 XML,缓存就可能过期。chart 与 embeddings 对不上时,以 embeddings 为准。

2.2 python-pptx 的 Presentation / Slide / Shape 三层映射

python-pptx 的对象模型几乎是 XML 的一对一映射:Presentation对应ppt/presentation.xmlprs.slides对应ppt/slides/下的页,slide.shapes对应页内的形状树。先跑一遍巡检脚本,把「这一页有什么」摸清楚,比直接写抽取逻辑高效得多。

# inspect_shapes.py:先摸清每页有哪些形状类型 from pptx import Presentation prs = Presentation("2024年毕业生薪酬白皮书.pptx") print("总页数:", len(prs.slides)) for page, slide in enumerate(prs.slides, start=1): for sh in slide.shapes: print( page, sh.shape_id, # 形状 ID,同一版本内唯一,跨版本会变 sh.name, # 作者命名,常是「表 3-1」「图 4-2」 sh.shape_type, # 文本框 / 表格 / 图表 / 组合 / 图片 sh.has_text_frame, # 是否有文字 sh.has_table, # 是否是可编程表格 sh.has_chart, # 是否是原生图表 )

输出里最该关注三列:shape_typeGROUP的形状需要递归展开,否则里面的表格会整块漏掉;has_chart为 True 的形状,数值要从 chart 部件读;而has_table为 False 但shape_type看起来像表格的,多半是图片或嵌入对象,只能另想办法。

关于定位主键:shape_id在同一份文件里唯一,但白皮书每年重做一次,ID 会整体重排,不能当跨年比对的主键。更稳的做法是用(页码, 形状名, 表头指纹)三元组,其中表头指纹就是表头文字拼接后的哈希——只要作者没改表头,跨版本就能对上。

2.3 合并单元格、文本框标题和备注页:最容易被丢掉的三个信息源

薪酬白皮书为了排版好看,几乎必然出现跨行跨列的合并单元格,比如「一线城市」横跨三行,只在第一行有文字,后两行是空串。python-pptx 的cell.text只会返回空,于是抽取结果里那三行城市全变成 NaN。较新版本的 python-pptx 提供了cell.is_merge_origincell.is_spanned两个判断,旧版本可以退回读 XML 属性:

# 读 a:tc 上的合并属性,gridSpan 横向跨度,rowSpan 纵向跨度 def cell_span(cell): tc = cell._tc return { "gridSpan": tc.get("gridSpan"), # '2' 表示横向占两格 "rowSpan": tc.get("rowSpan"), # '3' 表示纵向占三格 "hMerge": tc.get("hMerge"), # '1' 表示被左侧格横向吞并 "vMerge": tc.get("vMerge"), # '1' 表示被上方格纵向吞并 }

标题同理。白皮书的页标题大多不是占位符,而是一个普通文本框,位置在页面顶部。可靠的做法遍历所有带文字的形状,取sh.top最小且文字长度大于 4 的那段作为页标题——sh.top的单位是 EMU,越小越靠上。

第三个信息源是备注页。口径说明往往写在备注里,比如「平均月薪为算术平均,含年终奖摊薄」「样本量为 12,438 份有效问卷」。抽出来当作该页所有数值行的note字段,后面做口径校验时会省很多事。抽取方式是slide.has_notes_slide为真时读slide.notes_slide.notes_text_frame.text

提示:单位经常不在单元格里,而在表头上方一行小字,内容是「单位:元/月」或「单位:万元/年」。这一行必须单独抓,否则后面做数值换算时会整列错一个数量级。

2.4 三条抽取路线的取舍

纯 Python 生态里能走的路不止一条,选错方向会在后期付出成倍的调试成本。

路线依赖适合场景代价
python-pptx纯 Python,pip 即装结构化表格、原生图表、批量抽取合并单元格、跨部件关系要自己补
lxml 直读 OOXMLlxmlpython-pptx 没暴露的属性与 rels 关系命名空间琐碎,代码可读性差
LibreOffice 无头转换需安装 soffice整页版式还原、扫描页兜底环境重、转换有损、CI 里要额外装包

我一般会混用前两条:主干用 python-pptx,碰到vMerge恢复、chart 到 embeddings 的 rels 跳转这类细节,再下沉一层用 lxml 补。LibreOffice 只在最后做一次人工校对用的整页 PDF 导出,不进主流程。

3. 用 python-pptx 把薪酬表和图抽成 DataFrame 的最小可运行脚本

3.1 依赖准备与项目目录

python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install python-pptx pandas openpyxl lxml

python-pptx负责解析 PPT 结构与图表对象,openpyxl是 pandas 读嵌入 xlsx 的引擎,lxml用于兜底读 XML 属性。项目目录建议拆成extract/(抽取)、clean/(清洗)、report/(出图与写回)三层,因为清洗逻辑会随着白皮书改版不停调整,混在一个文件里两周后就没人敢改了。

3.2 递归遍历形状并把表格转成记录

先写两个基础函数:一个递归展开组合形状,一个按位置挑页标题。

# extract_tables.py from pptx import Presentation from pptx.enum.shapes import MSO_SHAPE_TYPE def iter_shapes(shapes): """递归展开组合形状,避免组合内的表格被整块漏掉""" for sh in shapes: if sh.shape_type == MSO_SHAPE_TYPE.GROUP: yield from iter_shapes(sh.shapes) else: yield sh def slide_title(slide): """标题多为普通文本框而非占位符:取最靠上、文字较长的那段首行""" best, best_top = "", None for sh in iter_shapes(slide.shapes): if not sh.has_text_frame: continue text = sh.text_frame.text.strip() if len(text) < 4: continue if best_top is None or sh.top < best_top: best, best_top = text.splitlines()[0], sh.top return best

iter_shapes用生成器递归,好处是内存占用与页数无关。slide_title里的len(text) < 4是过滤页码、单位小字这类噪声;sh.top是 EMU 单位的纵坐标,白皮书页面上方通常还有一条细装饰线,也会被当成形状,但它没有文字,会被过滤掉。

然后是表格主体与表头识别:

HEADER_HINTS = ("城市", "学历", "行业", "岗位", "分位", "月薪", "年薪", "平均", "样本") def split_header(grid): """判断首行是否为表头:命中关键词数达到阈值就当表头""" if not grid: return [], [] first = grid[0] hit = sum(1 for c in first if any(h in c for h in HEADER_HINTS)) if hit >= max(2, len(first) // 3): return first, grid[1:] return [f"col{i}" for i in range(len(first))], grid def extract_tables(pptx_path): prs = Presentation(pptx_path) for page, slide in enumerate(prs.slides, start=1): title = slide_title(slide) for sh in iter_shapes(slide.shapes): if not sh.has_table: continue tbl = sh.table # 单元格内多段落会被拼成 \n,先压平,避免污染数值解析 grid = [[c.text.strip().replace("\n", "") for c in row.cells] for row in tbl.rows] yield page, title, sh.name, grid

split_header的阈值写成max(2, len(first) // 3)而不是固定值,是因为有些表第一列是「城市」,有些表第一列是序号列。表头识别失败时退回col0/col1/...,宁可后面人工改名,也不要在这一步猜错列语义。

3.3 图表数值:numCache 与 embedded xlsx 的双通道读取

原生图表走 python-pptx 的 chart 对象,这是最省事的一条路:

# extract_charts.py from pptx import Presentation def chart_records(pptx_path): prs = Presentation(pptx_path) for page, slide in enumerate(prs.slides, start=1): for sh in slide.shapes: if not sh.has_chart: continue chart = sh.chart for plot in chart.plots: # 散点图的分类在 ser.x_values 上,categories 会是 None cats = list(plot.categories or []) for ser in plot.series: values = list(ser.values or []) for cat, val in zip(cats, values): yield { "page": page, "shape": sh.name, "chart_type": str(chart.chart_type), "series": str(ser.name), "category": str(cat), "value": val, }

chart.plots可能返回多个绘图区,比如柱状图加折线图的组合图,所以必须循环而不是取plots[0]ser.values为 None 只有一种情况:该系列的数值没有缓存,只有公式引用,这时只能走第二通道。

第二通道是把嵌入工作簿整份读出来:

import io, zipfile import pandas as pd def embedded_sheets(pptx_path): """一个 chart 通常对应一个嵌入工作簿,里面可能有多个 sheet""" with zipfile.ZipFile(pptx_path) as z: for name in z.namelist(): if name.startswith("ppt/embeddings/") and name.lower().endswith(".xlsx"): blob = io.BytesIO(z.read(name)) # header=None 保留首行,防止表头被当成列名丢掉 yield name, pd.read_excel(blob, sheet_name=None, header=None) for name, sheets in embedded_sheets("2024年毕业生薪酬白皮书.pptx"): for sheet, df in sheets.items(): print(name, sheet, df.shape)

映射关系要精确到「哪个 chart 对应哪个工作簿」,靠的是ppt/charts/_rels/chartN.xml.rels:里面 Type 以/package结尾的那条 Relationship,Target 指向../embeddings/xxx.xlsx。一个白皮书里图表很多时,这个映射必须建,否则你无法判断哪份工作表属于哪一页。

注意:嵌入工作簿里常混着辅助列和中间透视结果,不是所有 sheet 都是原始数据。sheet 名带「数据源」「Data」的优先,带「图」「chart」的通常是加工后的。

3.4 统一成长表:字段设计与参数说明

把表格记录和图表记录合并成一张长表,是最适合后续透视的形态。

import pandas as pd rows = [] for page, title, shape_name, grid in extract_tables("2024年毕业生薪酬白皮书.pptx"): header, body = split_header(grid) for r in body: rec = {"page": page, "title": title, "shape": shape_name} rec.update({h: v for h, v in zip(header, r)}) rows.append(rec) long_df = pd.json_normalize(rows) print(long_df.shape) print(long_df.head(3).to_string())

json_normalize会按并集生成列,不同页表头不一致时不会丢字段,缺的自动补 NaN。落表时至少要保留这几个元字段:

字段类型作用
pageint反查定位到原页
titlestr页标题,用于识别维度分组
shapestr形状名,作者命名往往是「表 3-1」
unit_notestr页级单位标记,换算的唯一依据
raw_*str每个数值的原文,清洗后仍保留,便于追溯

4. 薪酬数据清洗:分位数口径对齐与维度归一化

4.1 分位数和平均月薪不能混算

白皮书里最常见的四列是 P25、P50、P75 和「平均月薪」。前三个是同一分组的三个位置,均值是第四个统计量,把 P50 和均值放进同一个mean()里求平均,结果没有任何统计意义。更要命的是「年包」和「月固定薪资」的差别:年包含年终奖、签字费、股票折算,除以 12 得到的数会明显高于月固定薪资,两者做同比就是在比两件事。

清洗的第一步是把所有数值统一到「税前月现金收入」一个口径上,换算函数要显式处理单位:

import re def to_monthly(text, page_unit=None): """把 '18.6万/年' '9.5k' '9500元/月' '15-20万' 统一成税前月薪""" if not isinstance(text, str): return None t = text.strip().replace(",", "").replace(",", "") nums = re.findall(r"\d+(?:\.\d+)?", t) if not nums: return None # 区间取中点,单值直接用 val = sum(float(n) for n in nums) / len(nums) if len(nums) > 1 else float(nums[0]) marker = (t + (page_unit or "")).lower() if "万" in marker: return round(val * 10000 / 12, 0) # 年薪折月 if "k" in marker or "千" in marker: return round(val * 1000, 0) return round(val, 0)

这里的关键设计是page_unit参数:单位优先看单元格内的标记,没有就继承页级标记。区间15-20万取中点只是为了让数据可用,正确做法是同时保留raw原文列,报告里出现异常值时能一眼看出是区间中点造成的。

4.2 城市、行业、学历三层归一化字典

同一份白皮书里「北京」和「北京市」同时出现的概率很高,行业名更是五花八门:「互联网」「互联网/电商」「信息传输、软件和信息技术服务业」讲的可能是同一件事。归一化必须落成显式字典,而不是用模糊匹配——模糊匹配在某一年会悄悄把两个不同行业合并,而且没人会发现。

CITY_ALIAS = {"北京市": "北京", "北京": "北京", "上海市": "上海", "深圳市": "深圳", "广州市": "广州", "杭州市": "杭州"} TIER = {"北京": "一线", "上海": "一线", "深圳": "一线", "广州": "一线", "杭州": "新一线", "成都": "新一线", "武汉": "新一线", "西安": "新一线"} DEGREE = {"本科": "本科", "学士": "本科", "硕士": "硕士", "研究生": "硕士", "博士": "博士", "大专": "大专"} def normalize(row): city = CITY_ALIAS.get(str(row.get("城市", "")).strip(), "其他") return { "city": city, "tier": TIER.get(city, "其他"), "degree": DEGREE.get(str(row.get("学历", "")).strip(), "其他"), } norm = pd.DataFrame([normalize(r) for r in long_df.to_dict("records")]) long_df = pd.concat([long_df, norm], axis=1)

落到「其他」而不是丢弃,是有意为之:数量少的类别进入「其他」后仍然计入总量,一旦发现「其他」占比超过 5%,说明字典该补了。行业维度建议单独维护一份industry_map.csv,让业务方直接改表,而不是改代码。

4.3 透视与同比增速的三种写法

清洗完的维度列应该立刻能支撑交叉透视,这是检验清洗质量最快的方式。

p50 = long_df["P50"].map(lambda s: to_monthly(s, long_df["unit_note"].iloc[0])) pivot = (long_df.assign(p50=p50) .pivot_table(index="tier", columns="degree", values="p50", aggfunc="median", observed=True)) print(pivot.round(0)) # 同比:同口径、同分组,先各自取中位数再算增速 cur = long_df[long_df["year"] == 2024].groupby("industry")["p50"].median() prev = long_df[long_df["year"] == 2023].groupby("industry")["p50"].median() yoy = ((cur - prev) / prev * 100).round(1).sort_values(ascending=False)

三个细节值得单独说:aggfuncmedian而不是mean,是因为同一格内可能混了不同岗位的样本,中位数抗极值;observed=True在分类列较多时能显著减少稀疏组合;增速计算必须groupby之后再算,先算行级增速再平均会得到加权错误的数。基数很小的行业增速容易失真,输出时把样本量count一起带上,低于 30 个样本的分组标记为不可靠。

4.4 三条能塞进 CI 的勾稽自检

抽取脚本最大的风险不是报错,而是悄悄少抽了几行。把下面四条规则写成断言,每次白皮书更新跑一遍,比人工核对省事得多。

校验规则失败意味着什么
分位单调P25 ≤ P50 ≤ P75列错位,或合并单元格串行
单位一致同页同列的单位标记相同有人把「万/年」混进了「元/月」列
覆盖度每个 city×degree 组合都有值漏抽页面,或该组合白皮书确实未披露
样本勾稽各分组样本数之和 = 页脚样本量合并单元格被重复计数
# 分位单调性断言,P25/P50/P75 必须按列取到 sub = long_df.dropna(subset=["P25", "P50", "P75"]).copy() bad = sub[(sub["P25"] > sub["P50"]) | (sub["P50"] > sub["P75"])] assert bad.empty, f"分位不单调 {len(bad)} 行,检查是否列错位:\n{bad[['page','title']].head()}"

5. 反查与增量:让白皮书数据能反复用

5.1 从 DataFrame 反查页面并把结果写回一页 PPT

清洗后的数据要能双向追溯:pageshape两个字段必须能定位回原页。做年度复核时,用 python-pptx 新建一页把归一化后的中位数画回去,比在 Excel 里手工拼图快得多。

from pptx import Presentation from pptx.chart.data import CategoryChartData from pptx.enum.chart import XL_CHART_TYPE from pptx.util import Inches prs = Presentation("2024年毕业生薪酬白皮书.pptx") slide = prs.slides.add_slide(prs.slide_layouts[5]) # 版式索引因模板而异,稳妥做法是按名字查 data = CategoryChartData() data.categories = list(pivot.index) # tier 作为分类轴 data.add_series("本科 P50", tuple(pivot["本科"].fillna(0)), number_format='#,##0') data.add_series("硕士 P50", tuple(pivot["硕士"].fillna(0)), number_format='#,##0') gf = slide.shapes.add_chart(XL_CHART_TYPE.COLUMN_CLUSTERED, Inches(0.6), Inches(1.4), Inches(9), Inches(4.5), data) gf.chart.has_legend = True prs.save("薪酬复核_2024.pptx")

add_series的数值序列不能含 None,先用fillna(0)补,否则会抛类型错误;slide_layouts[5]在不同母版里指向的版式未必一样,正式脚本里应按layout.name遍历查找,取名字含「标题和内容」的那个。写回文件用另存,不要覆盖原白皮书,避免破坏原始证据。

5.2 用 slide XML 哈希做增量重算

白皮书一年出一版,八成页码的内容是不变的。全量重跑既慢又会让历史修正过的清洗结果被覆盖。可行的做法是对每页的slideN.xml求哈希,只重跑变化页,再按(page, shape, row)主键与旧长表 merge。

import hashlib, zipfile def slide_hashes(pptx_path): out = {} with zipfile.ZipFile(pptx_path) as z: for n in z.namelist(): if n.startswith("ppt/slides/slide") and n.endswith(".xml"): out[n] = hashlib.sha1(z.read(n)).hexdigest() return out new, old = slide_hashes("2024版.pptx"), slide_hashes("2023版.pptx") changed = [k for k, v in new.items() if old.get(k) != v] print("变化页:", len(changed), sorted(changed)[:10])

哈希只覆盖页面 XML,所以有一个盲区:只改口径说明、不改数字的页,哈希不会变。补这个洞的办法是给每页加一层二级指纹——把该页所有单元格的「单位标记 + 备注页文本」拼起来再哈希,两级指纹都一致才判定为未变。另一个细节是ppt/embeddings/里的工作簿不在页面 XML 里,图表页的哈希必须把ppt/charts/chartN.xml一并算进去,否则作者只改了图表数据源而没动版式时,你会漏掉这次更新。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 16:31:16

Python中级编程实战:字符串处理与数据分析技巧

1. 题目背景与价值解析董付国老师的Python小屋系列编程题在编程学习者中享有盛誉&#xff0c;其中111-120这组题目特别适合已经掌握Python基础语法、正需要提升实际问题解决能力的中级学习者。这组题目设计精妙之处在于&#xff1a;它们既不像入门题那样简单直白&#xff0c;也…

作者头像 李华
网站建设 2026/9/17 16:31:07

智能车走马观碑组视觉识别实战指南

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

作者头像 李华
网站建设 2026/9/17 16:30:03

文华财经波浪尺指标公式源码详解与WH6实战应用

简介&#xff1a;文华财经波浪尺指标公式源码.doc 是一份面向股票技术分析者的公式源码文档&#xff0c;重点解决如何在文华财经平台中识别波段高低点、绘制波浪尺通道并辅助买卖点判断。资源为1个doc文件&#xff0c;压缩包约50KB&#xff0c;内容以指标公式的逐段注释与函数说…

作者头像 李华
网站建设 2026/9/17 16:29:12

Python自动生成数字信号处理实验报告:FFT、滤波器与docx排版实践

简介&#xff1a;这是一份北京理工大学数字信号处理课程的实验报告文档&#xff0c;面向信息工程、电子通信等专业本科生及需要完成DFT相关实验的学习者&#xff0c;用于理解离散傅里叶变换原理并完成信号频谱分析的实验任务。压缩包内仅含1个docx文件&#xff0c;大小约795KB&…

作者头像 李华
网站建设 2026/9/17 16:27:19

企业服务总线ESB平台方案:协议转换、消息路由与OA系统对接实战

简介&#xff1a;一份聚焦企业服务总线&#xff08;ESB&#xff09;平台建设的解决方案文档&#xff0c;面向企业架构师、集成开发人员及信息化项目负责人&#xff0c;适用于异构系统互联、业务流程自动化与数据交换等场景。文档围绕 ESB 产品定位、产品概述、客户价值、关键特…

作者头像 李华
网站建设 2026/9/17 16:26:43

Java Swing + MySQL 学生选课系统:从界面到数据库的完整实现

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

作者头像 李华