课程表的归一化处理

Typora 中的 Markdown 文本如何保留格式插入到当前编辑器?

在 Typora 中复制 Markdown 文本后,直接粘贴到 CSDN 富文本编辑器时,格式往往无法保留。推荐使用以下方法:

  • 方法一(推荐) :在 Typora 中直接复制(Ctrl+C),然后到 CSDN 编辑器粘贴(Ctrl+V)。Typora 复制时会自动携带 HTML 格式,富文本编辑器通常能识别标题、加粗、列表、代码块等基础格式。别用鼠标右键复制粘贴(因为掉格式)。

  • 方法二:如果直接粘贴丢失格式,先在 Typora 中切换到「源代码模式」,复制原始 Markdown 文本,再到 CSDN 编辑器切换到「Markdown 编辑器」粘贴,最后再切回富文本模式。

  • 方法三:若以上都不行,可将 Markdown 文本粘贴到任意支持「粘贴为纯文本后再转格式」的中间工具(如本地 Word 或在线转换器),再复制粘贴到当前编辑器。

粘贴后请检查标题层级、代码块和图片是否完整,必要时手动微调。

学校老师比较多,一张课程大表,在查询个人课表时非常不方便。做个人课表查询,常会因为提供的数据源不够规范,导致课程和老师的任教科目不一致而出现课表内空缺。比如教师任课科目为"信技",而大课表里的名称是"技术",这时候会出现如图这种现象。在按教师姓名查找时出现没有

要显示的教师姓名而查不到课表。

这时候就需要对数据进行加工,把用到的课程别名统一转为一个规范的名称。比如把""信息技术":

"信技": "通用技术":都归为 "技术"。

下面分步拆解具体做法:

一.先导入两个模块:

python 复制代码
import re
from typing import Any

re 是 Python 标准正则表达式库(regular expressions)。

import re:把整个 re 模块导入,之后代码就可以调用正则相关工具:

复制代码
re.sub(r"[\s\-_---/()()]+", "", s)

re.sub():属于 re 模块的函数,功能:正则替换,把匹配到的字符全部删掉 / 替换.

❗如果删掉 import re:运行到 re.sub(...) 代码时,抛出报错:NameError: name 're' is not defined,程序直接崩溃。

正则含义:把下面这一类字符全部删除,得到 "无干扰紧凑字符串"

\s:所有空白(半角空格、制表)

\-:短横杠 -

_:下划线

---:中文长破折号

/:斜杠

():中文括号中的任一个
():英文括号中的任一个

  • 量词:修饰前面整个字符集 ...

含义:匹配前面的字符,出现 1 次 或者 连续多次。

示例:

1.对比有无 + 的区别:

① 带 +:\\s\\-_---/()()+

匹配连续一串干扰符号,把连续的多个符号当成一整个单元,一次性全部删掉。

例子:

输入:"信 技"(中间多个空格)

  • 会把连续多个空格,作为整体一次匹配,全部清空。

"信 技" → "信技"

输入:"信息技术((上))" 连续多个括号

连续括号会被一次性全部删掉。

② 如果去掉 +:\\s\\-_---/()()

只能匹配单个 符号。re.sub也能全部删掉,但是正则引擎要多次循环逐个匹配。

功能结果看上去差不多,但遇到大量连续符号,+效率更高

边界重点:+ 要求至少出现 1 个才匹配

如果没有这些干扰字符,就不触发匹配,不做替换,原文本原样保留。

⚠不会匹配 0 个字符,不会凭空插入空字符串。

实际业务例子

案例 1:"信   技"(多个连续空格)

无+匹配单个空格;+ 把这一整串连续空格一次性捕获,替换为空,直接得到"信技"。

案例 2:"通用--___技术",多个横杠、下划线连在一起

+把--___这一整串全部捕获,一次性删除,得到"通用技术"。

如果没有+,正则会逐个匹配每一个-、_,逐个删除,表面看最终结果一样,但是引擎匹配次数更多,效率低。

正则量词小复习

表格

符号 含义

+ 1 次 或 多次(≥1)
* 0 次 或 多次(≥0,可以匹配空)
? 0 次 或 1 次(最多 1 次)

❗这里不能换成 *:

*允许匹配 0 个字符,在部分场景会产生空匹配,一般清洗文本用+最合适。

📌踩坑总结(你写科目归一化会遇到)

正则字符串前面加 r 原始字符串,r"pattern",避免\转义异常。

re.sub不会修改原字符串,必须赋值接收返回值。

\[\]里面的-要转义\-,否则会被当成范围符号。

+代表 1 次及以上;不要随便用*,会产生空匹配。

匹配中文直接写中文,不需要特殊编码。

+写在 括号之外,表示括号内的任一个或多个。

2. 真正体现差异的场景

场景 1:re.sub 第 4 个参数 count(限制替换次数)

re.sub(pat, repl, s, count=N):最多替换 N 次

复制代码
import re
text = "信   技"
带 +,count=1,只允许替换1次
res1 = re.sub(r"[\s]+", "", text, count=1)
print(repr(res1))   # '信技',一整块空格一次干掉
不带 +,count=1,只允许替换1次
res2 = re.sub(r"[\s]", "", text, count=1)
print(repr(res2))   # '信  技',只删掉第一个空格,剩下两个还在

repr() 作用:把对象的原始内部表示 打印出来,专门用来调试看不见的特殊字符(\ufeff\u3000),看字符串的真实编码,是文本清洗的调试神器。

在cursor中调试时,出现自动补全提示时,tab键一键自动补全。Ctrl+Space手动唤起补全。

场景 2:re.search 判断 "是否存在符号

复制代码
s = "信技" # 没有任何干扰符号
+:要求至少1个符号才匹配,找不到返回None
print(re.search(r"[\s-]+", s)) # None
不带+:匹配单个符号,找不到同样返回None
print(re.search(r"[\s-]", s)) # None

场景 3:re.findall 提取,差异巨大

复制代码
text = "a---b___c"
print(re.findall(r"[\-_]+", text))   # ['---', '___'] 拿到连续一整块
print(re.findall(r"[\-_]", text))   # ['-','-','-','_','_','_'] 拆成单个字符列表

Python 正则表达式完整使用指南

Python 使用正则需要导入内置模块:import re,不需要额外安装。

复制代码
import re

正则字符串建议用原始字符串 r"" ,前面加 r,避免 \ 转义带来的坑,比如 r"\s",不要写 "\\s"

re 模块 4 个最常用核心函数(你项目主要用前 2 个)

表格

函数 作用 返回值 你的业务场景
re.sub(pattern, repl, string) 查找并替换,最常用 处理后的新字符串 科目清洗,删除空格、括号、符号
re.search(pattern, string) 查找,只要找到一处匹配 匹配对象 / None 判断是否包含某特征文本
re.match(pattern, string) 从字符串开头匹配 匹配对象 / None 判断字符串是否以某内容开头
re.findall(pattern, string) 找出全部匹配内容,返回列表 匹配结果列表 提取括号里的文字、提取数字

1. re.sub () 替换(写科目归一化在用)

语法:

复制代码
re.sub(正则模式, 替换成什么, 原始字符串)

示例:删除一堆干扰符号

复制代码
import re
s = "信息技术(上)-- 1班"
res = re.sub(r"[\s\-_---/()()]+", "", s)
print(repr(res)) # '信息技术1班'

⚠重点:字符串不可变,re.sub不会修改原字符串,必须用变量接收返回结果

2. re.search () 查找是否存在匹配

只要字符串任意位置有匹配就返回,找不到返回None

复制代码
import re
text = "通用-技术"
判断是否包含横杠
m = re.search(r"[----]", text)
if m is not None:
print("存在横杠符号")

3. re.findall () 提取内容,返回列表

提取所有匹配到的片段,非常适合提取数字、提取括号内文字。

复制代码
import re
txt = "语文(1),数学(2),英语(3)"
nums = re.findall(r"\((\d+)\)", txt)
print(nums)  # ['1', '2', '3']

4. 正则元字符基础(常用符号)

表格

符号 含义
\s 任意空白:空格、制表符
\d 数字 0‑9
[] 字符集,匹配括号内任意一个字符
+ 前面字符出现1 次或多次
* 前面字符出现0 次或多次
. 匹配任意单个字符(除换行)
() 分组,用来捕获提取内容
` `
字符集 [] 注意点
  • [] 里面,- 代表范围,所以短横杠要写为 \- 转义,防止被识别成区间。

    r"[\s-_---/()()]"

5. 两种写法:直接调用 re 函数 / 预编译正则

方式 A:直接调用(你的业务代码现在用的,简单,适合少量调用)
复制代码
compact = re.sub(r"[\s\-_---/()()]+", "", s)
方式 B:预编译 re.compile ()(大量循环时性能更好)

如果循环成千上万条科目数据,反复执行同一个正则,先编译一次,不用每次解析正则表达式。

复制代码
import re
# 预先编译正则对象
pattern = re.compile(r"[\s\-_---/()()]+")
s_list = ["信 技","通用-技术","信息技术(上)"]
for s in s_list:
compact = pattern.sub("", s)
print(compact)

小项目、几百行 Excel 数据,直接re.sub()完全够用,不用 compile。

🧪练习示例

复制代码
# %%
import re
1. re.sub 删除干扰符号
s1 = "信 技"
out1 = re.sub(r"[\s-_---/()()]+", "", s1)
print(f"{repr(s1)} → {repr(out1)}")
2. search 判断是否包含数字
s2 = "信息技术1"
if re.search(r"\d", s2):
print(f"{s2} 包含数字")
3. findall 提取数字
s3 = "课程(01)"
result = re.findall(r"((\d+))", s3)
print("提取内容:", result)

📌踩坑总结(写科目归一化会遇到)

  1. 正则字符串前面加 r 原始字符串,r"pattern",避免\转义异常。
  2. re.sub不会修改原字符串,必须赋值接收返回值
  3. []里面的-要转义\-,否则会被当成范围符号。
  4. +代表 1 次及以上;不要随便用*,会产生空匹配。
  5. 匹配中文直接写中文,不需要特殊编码。

结合现有代码回顾

复制代码
compact = re.sub(r"[\s\-_---/()()]+", "", s)
  • r"":原始正则字符串
  • [\s\-_---/()()]:字符集合,匹配任意一个干扰符号
  • +:连续 1 个或多个干扰符号整体捕获
  • "":替换为空,等价于删除匹配到的内容
  • s:原始输入字符串
  • compact:接收处理完成的新字符串

from typing import Any

复制代码
from typing import Any
  • typing 是 Python 标准库,专门提供**类型注解(类型提示)**相关工具,不改变程序运行逻辑,只给编辑器(Cursor)、人看的提示信息。
  • from typing import Any:从 typing 模块,单独把Any这个类型拿出来给当前文件使用。

Any是什么?

Any 代表任意类型

复制代码
def clean_text(value: Any) -> str:
def normalize_subject_name(value: Any) -> str:

(value: Any) 含义:该输入参数value可以接收任意类型:None、字符串、数字,布尔都可以。

重点区分:

  1. 类型注解只做提示,不做运行校验! Any不会限制传入的数据,程序运行时不会报错;
  • 阅读代码的人一眼看懂:这个参数接收五花八门的原始单元格数据。

如果删掉这一行,代码运行到 value: Any,会报错: NameError: name 'Any' is not defined


两者核心区别对比

表格

代码 来源 作用 是否影响运行逻辑 对应代码位置
import re 标准库 re 模块 提供正则函数re.sub() 真正参与程序运算,删掉直接报错 紧凑匹配,删除空格、括号符号
from typing import Any 标准库 typing 模块 类型注解标记任意输入类型 ❌仅提示,运行时不生效 函数参数 value: Any

导入还有另一种写法,效果等价:

复制代码
import re
import typing
def f(value: typing.Any) -> str:
pass

用了from typing import Any,直接写Any更简短。

if m is None:


if not m: 有啥区别?

re.search() 或者 re.match() 返回值:

匹配成功 → 返回匹配对象(Match 对象) ,真值为 True

匹配失败 → 返回 None

python 复制代码
text = "通用-技术"
m = re.search(r"abc", text)
if m is None:
    print("没有匹配到")

1. if m is None:

判断:变量严格等于 None,只有就是 None 的时候才成立

复制代码
if m is None:
    print("没有匹配到")
  • 只有 m = None,条件才为 True

  • 如果 m 是匹配对象(哪怕匹配出来是空字符串),条件为 False

is 判断的是同一个对象身份,专门用来判断是否是 None。

2. if not m:

做布尔真假判断,只要是 "假值" 就成立。

Python 里这些都是假值: None""空字符串、0[]空列表、{}空字典、False

👉对于 re.search 的返回结果,两种写法表现一样! 因为 search 只会返回Match对象或者None,没有其他假值。

真正会出现差异的场景(重点)

如果变量有可能是空字符串 ""0[],两者行为完全不一样。

示例 1:m 是空字符串 ""

复制代码
m = ""
if m is None:
print("is None 成立?") # 不执行!m是空字符串,不是None
if not m:
print("not m 成立?") # ✅执行,空字符串属于假

示例 2:m = 0

复制代码
m = 0
if m is None:
    pass # 不触发
if not m:
    pass # ✅触发

示例 3:m = \[\] 空列表

复制代码
m = []
if m is None:
    pass # 不触发
if not m:
    pass # ✅触发

科目归一化代码里的一行

复制代码
s = clean_text(value)
if not s:
    return ""

s 是字符串:

  • s = ""(空字符串) → not s → True 这里就不能写 if s is None,因为 s 永远是字符串,不会是 None。

clean_text 已经把 None 转成空字符串,所以s只会是 str。


总结对照表

表格

写法 触发条件 适合场景
if m is None: 只有变量是 None 对象才成立 判断函数返回是否是 None,例如 re.search 结果
if not m: 任意假值:None""0[]False 判断字符串是否为空、列表是否为空

实操建议

  • **1.处理 re.search() 返回值:**优先写 if m is None:,语义更精准,明确表达 "没有匹配到"。
python 复制代码
m = re.search(r"...", s)
if m is None:
    ...
  • 2.判断清洗后的字符串是否为空:用 if not s:
python 复制代码
s = clean_text(value)
if not s:
    return ""

记忆口诀

  • is None:就认 None,别的一概不认;
  • not x:所有 "空、零、无" 全都认。

二.前置条件

1.定义文本清洗函数:

python 复制代码
def clean_text(value: Any) -> str:
    if value is None:
        return ""
    text = str(value)
    text = text.replace("\ufeff", "")
    text = text.replace("\u3000", " ")
    return text.strip()

解读:

python 复制代码
def clean_text(value: Any) -> str:
  • def clean_text:定义函数,函数名叫 clean_text,作用是清洗文本
  • value: Any:入参 value,类型注解 Any,代表可以接收任意类型数据:None、数字、字符串、布尔值都可以传进来。
  • -> str:返回值类型注解,说明这个函数最终一定返回字符串类型。
python 复制代码
    if value is None:
        return ""
  • 判断传入的值是不是 None(空值,不是字符串"None")。
  • 如果是None,直接返回空字符串 "",避免后续str(None)得到字符串"None"这种脏数据。
python 复制代码
    text = str(value)

把输入的任意数据强制转为字符串。

示例:传入数字123"123";传入布尔True"True"

python 复制代码
    text = text.replace("\ufeff", "")

\ufeffBOM 字节序标记,常见于 Windows 保存的 UTF‑8 带 BOM 的 csv/txt 文件,是看不见的隐形特殊字符,会造成文本比对、匹配异常。

  • .replace("\ufeff", ""):把这个隐形 BOM 字符全部删除(用空字符串替代)。
python 复制代码
    text = text.replace("\u3000", " ")

\u3000中文全角空格,输入法打出来的全角空白,不是普通半角空格。

  • 将所有全角空格替换成普通半角空格。
python 复制代码
    return text.strip()

strip():删除字符串开头和结尾所有空白(半角空格、制表符、换行符),中间的空格保留。 返回清洗完成后的字符串。

整体功能总结

clean_text 通用文本清洗工具函数:

  1. 遇到None返回空串;
  2. 把任意输入转字符串;
  3. 清除 UTF‑8 BOM 隐形字符;
  4. 中文全角空格转普通空格;
  5. 剔除首尾空白; 常用于读取 csv、excel、网页爬虫拿到的脏文本预处理。

为啥只剔除首尾空白,不连同中间的空白(如果有的话)一并删除?

clean_text() 作为全局通用工具函数,要服务姓名、班级、教室、科目等很多字段。

它只做确定无害的操作:去 BOM、处理全角空格、去掉首尾空白。 凡是拿不准是不是脏数据的修改,一律不做,留给上层业务函数处理。

删除中间空格、横杠、括号这件事,只属于 "科目做别名匹配" 这一个业务场景 ,所以只放在normalize_subject_name内部,并且只生成临时变量用于判断,不修改输出值。

如果把中间空白删除写进 clean_text:

教师 "张 明" → 被强制改成"张明",原始数据被无差别篡改,这就是 bug。

教材、教具、设备字段
复制代码
课本 必修上册
教具 实验箱

空格区分类别与内容,删掉空格会合并文本。

科目本身(校本 / 拓展课程,排除牵强的体育与健康)

学校自定义课程,导出 Excel 自带中间空格:

  • 劳动 实践
  • 科创 制作
  • 心理健康 辅导

这里的空格不是手敲错误,是教务系统导出自带分隔。 当不在别名映射表里时,输出必须保留原始空格; 只有在做别名匹配的时候,临时生成 压缩字符串(用一个变量来接收)用来比对,输出不改

总结一句话

clean_text是给全部单元格通用调用 的底层工具。

部分业务字段的中间空格是语义分隔符 ,删掉会直接改变业务含义(典型就是连堂第1 2节)。

所以:删除中间空格、符号这种破坏性较强的清洗,只放在特定业务函数内部,并且只作为临时比对,不修改输出结果。

简单测试示例

复制代码
print(clean_text(None))                     # ""
print(clean_text("\ufeff 测试文本 "))      # "测试文本"
print(clean_text(123))                      # "123"

小提示:如果需要压缩中间空格,需要额外加 ' '.join(text.split())或用正则里的参数\s

两种去除字符串全部中间空白的方式

方式 1:"".join(text.split()) 字符串内置方法

方式 2:正则 re.sub(r"\s+", "", text)

⚠这两个都会干掉全部空白,不宜直接放到通用 clean_text 里面 。只适合做临时比对字符串 (compact),不要直接作为业务输出。

方式 1:"".join(text.split())

split () 不带参数的特殊行为

text.split():不带参数,会把任意空白(空格、全角空格、tab、多个连续空格) 当成分隔符,自动忽略首尾空白,返回去掉所有空白的片段列表。 "".join(列表):把列表全部片段拼接在一起,中间不加任何字符。

示例:

python 复制代码
text = "信  技"   # 中间两个空格
res = "".join(text.split())
print(repr(res)) # '信技'
text2 = "综 合   实 践"
res2 = "".join(text2.split())
print(repr(res2)) # '综合实践'
text3 = " 第1 2节 "
res3 = "".join(text3.split())
print(repr(res3)) # '第12节'  ❗连堂信息被破坏

方式 2:正则 re.sub(r"\s+", "", text)

\s 匹配任意空白字符:半角空格、tab、换行; + 匹配一个或多个连续空白;替换为空,删掉全部空白。

python 复制代码
import re
text = "信  技"
res = re.sub(r"\s+", "", text)
print(repr(res)) # '信技'
text2 = " 第1 2节 "
res2 = re.sub(r"\s+", "", text2)
print(repr(res2)) # '第12节' ❗同样破坏连堂语义
text3 = "通用-技术"
res3 = re.sub(r"\s+", "", text3)
print(repr(res3)) # '通用-技术',只删空白,符号不动
import re
text = "通用-  技术"
res=text.split("-")  #将字符串分裂为列表,以-为分界线
ress="".join(res)  #将列表转换为字符串,把分裂的元素连接起来
resss=re.sub(r"[\s----/()()]", "", text)
ressss=re.sub(r"[\s----/()()]", "", ress)
print(repr(text))  #原始字符串'通用-  技术'
print(repr(res))  #列表['通用', '  技术']
print(repr(ress))  #字符串'通用  技术'
print(repr(resss))  #字符串'通用技术'
print(repr(ressss))  #字符串'通用技术'

如果既要删空白,又要删横杠、括号,让正则来搞:

复制代码
re.sub(r"[\s\-_---/()()]+", "", s)

对比小结

表格

写法 能力 依赖
"".join(text.split()) 删除全部空白,符号不动 纯 Python,不需要 re
re.sub(r"\s+","",text) 删除全部空白,符号不动 需要import re
re.sub(r"[\s\-_---/()()]+","",s) 空白 + 横杠 + 括号等全部清除 需要import re
re.sub (r"\\s-_---/()()+", "", s),r 后面引号中,带方括号和不带有什么区别?

re.sub(r"[\s\-_---/()()]+", "", s)

重点对比:带方括号 [...](字符集) vs 不带方括号

1. [abc] 方括号:字符集(字符类)

[\s\-_---/()()]

[] 的含义:匹配括号里面任意某一个字符,只要命中其中一个就算匹配成功

这条正则意思:匹配下面任意一个字符:

\s(空白)、-_---/()

后面的**+** :一个或多个连续的上面这些字符。

示例:

复制代码
import re
s = "通用-技术(上)"
res = re.sub(r"[\s\-_---/()()]+", "", s)
print(repr(res)) # '通用技术'

只要出现集合里任意符号,就会被删除。


2. 如果去掉方括号:r"\s\-_---/()()+"

复制代码
# ❌去掉[],这就完全变了!
re.sub(r"\s\-_---/()()+", "", s)

没有[]之后,不再是 "任选其一",而是按顺序依次匹配一长串固定序列 : 需要文本严格按顺序出现: 空白 → 紧接着- → 紧接着_ →紧接着--- →紧接着/ →紧接着 →紧接着 →紧接着()(而且)要出现 1 次或多次)

要求字符串必须出现这一整套连续符号才会匹配,现实课表数据几乎不会出现这种连续组合,几乎匹配不到任何东西。

举个直观例子

输入:"通用-技术"

  • []-属于集合内字符,直接匹配删掉,得到通用技术
  • 不带[]:需要:空白 + - +_+---... 整套依次出现;文本只有一个-,条件不满足,完全不替换,输出还是通用-技术

3. 通俗类比

  • [A B C] 方括号:,A、B、C 随便哪一个都行。
  • ABC 无方括号:且、顺序,必须先 A,再 B,再 C,按顺序全部出现。

表格

模式 含义
[abc] 匹配 a 或者 b 或者 c,任意单个
abc 必须先 a,紧跟着 b,紧跟着 c,完整连续序列

4. 额外注意: []内部元字符规则

在方括号字符集里面,很多符号失去特殊含义:

  1. -:代表范围,所以要写\-转义;
  2. . * +[]里面就是普通字符,不需要转义;
  3. ()[]内部不再是分组,仅仅代表普通括号字符。
    代码里 [...()()],括号写在方括号内部,就是当做普通的左右括号字符去匹配,不是分组

一句话总结

  • []:字符集,匹配里面任意一个字符(或关系),正是我们清洗需要的;
  • 去掉[]:变成顺序匹配一串固定字符,需要所有符号按顺序连在一起才匹配,不适合我们删除各类零散干扰符号的场景。
    我们的业务场景:文本里空格、横杠、括号零散随机出现,不是成套顺序出现,所以必须用[]字符集。

2.SUBJECT_NORMALIZE_EXCLUDE:排除集合,

例如{"班会", "自习", "活动课"},这些科目绝对不做别名映射

复制代码
# 这些科目【不参与】归一化,原样保留,防止误并
SUBJECT_NORMALIZE_EXCLUDE = {
    "班会", "自习", "班主任", "大课间", "跑操", "午休", "晨读",
}

3.SUBJECT_ALIAS_TO_CANONICAL:别名→标准名 字典

python 复制代码
# 学科别名 → 标准名(按需扩充,)
SUBJECT_ALIAS_TO_CANONICAL: dict[str, str] = {
    "信息技术": "技术",
    "信技": "技术",
    "通用技术": "技术",
    "通技": "技术",
    "技术": "技术",
}
1. 整体定义 把易混乱的名称与标准名称放在一个字典里,实现多对一。
  • SUBJECT_ALIAS_TO_CANONICAL:(这就是一个字典的)变量名,这儿大写命名,Python 惯例代表常量,程序运行中不修改。
  • : dict[str, str]类型注解 ,说明这是一个字典:
    • key(键):字符串类型,科目别名
    • value(值):字符串类型,标准规范名称(canonical 标准名)
  • = { ... }:字典字面量赋值。
    业务含义:科目别名映射字典 ,把同一个科目的多种叫法,统一映射成同一个标准名称 "技术"

2. 字典每一行键值对拆解

字典格式:"别名": "标准名称"

表格

键 key(原始别名) 值 value(统一标准名) 业务说明
"信息技术" "技术" 完整正式科目名 → 统一为 "技术"
"信技" "技术" 简写口语叫法 "信技" → 统一为 "技术"
"通用技术" "技术" 另一门完整科目名 → 统一为 "技术"
"通技" "技术" "通用技术" 简写 "通技" → 统一为 "技术"
"技术" "技术" 本身已经是标准名,映射到自己,兼容直接输入标准名的情况

核心逻辑

不管输入是:信息技术 / 信技 / 通用技术 / 通技 / 技术,查询这个字典,得到的输出全部都是 "技术"

示例使用:

复制代码
# 根据别名拿到标准科目名
alias = "信技"
std_name = SUBJECT_ALIAS_TO_CANONICAL[alias]
print(std_name) # 输出:技术

3. 名词解释

  1. Alias:别名:现实业务里同一个东西有多种叫法、缩写、口语简称。
  2. Canonical:标准名 / 规范名:kəˈnɒnɪkl/ 系统内部存储、统计、筛选用的唯一名字。

场景来源:学校科目数据,表格、Excel、用户输入里写法很乱:有的写全称,有的写缩写,需要归一化。

4. 常见配套使用模式(和前面 clean_text 文本清洗函数搭配)

现实项目一般流程:

  1. 先用 clean_text() 清洗原始文本,清除 BOM、全角空格、首尾空白;
  2. 再拿清洗后的文本去这个字典查找,得到标准科目;

示例:

复制代码
raw_text = "信技 "
cleaned = clean_text(raw_text)
subject_std = SUBJECT_ALIAS_TO_CANONICAL[cleaned]
print(subject_std) # "技术"

5. 注意点 & 坑

  1. 字典是精确匹配 如果原始文本是 信息技术课,字典里没有这个 key,直接 [] 取值会抛 KeyError。 安全写法用 .get():在字典名后面加.get方法(所读取的科目名称)
python 复制代码
subject_std = SUBJECT_ALIAS_TO_CANONICAL.get(cleaned, cleaned)
#能找到括号里前面那个参数对应的Key就返回该key对应的值。
#找不到别名时,返回None,这时如果有两个参数则(后面的那个参数的值),不报错
  1. 大小写:字典区分大小写,"信息技术""信息技术 "(带空格)是不同 key,所以前面必须文本清洗。
  2. 最后一行 "技术": "技术" 作用:兜底,输入本身就是标准名的时候不会报错。

6. 数据流演示

表格

原始输入 clean_text 清洗后 字典查询结果
"信技" "信技" "技术"
"通用技术" "通用技术" "技术"
"技术" "技术" "技术"
"\ufeff通技" "通技" "技术"

总结

这个字典就是科目名称归一化映射表,解决数据源中同一科目多种叫法不一致的问题,把五花八门的别名收敛为系统内部唯一标准名称,方便后续统计、分组、筛选。

4.定义normalize_subject_name()函数 科目名归一化

python 复制代码
def normalize_subject_name(value: Any) -> str:
    """
    科目归一化:
      信息技术/信技/通用技术/通技 -> 技术
      空白/None -> ""
      排除名单 -> 原样
      其余     -> 仅清洗空白,原样
    """
    s = clean_text(value)
    if not s:
        return ""
# ① 排除名单直接返回(关键:班会/自习不能被并掉)
if s in SUBJECT_NORMALIZE_EXCLUDE:
    return s

# ② 精确命中别名表
if s in SUBJECT_ALIAS_TO_CANONICAL:
    return SUBJECT_ALIAS_TO_CANONICAL[s]

# ③ 紧凑兜底:去掉 空格/横杠/下划线/破折号/斜杠/中英文括号 再比
#    应对 "信 技" "通用-技术" "信息技术(上)" 这类 Excel 手填脏写法
compact = re.sub(r"[\s\-_---/()()]+", "", s)
for alias, canonical in SUBJECT_ALIAS_TO_CANONICAL.items():  #遍历字典,从字典里取出来KEY(即这里的临时变量alias),再和读到的名称相比较。
    if compact == re.sub(r"[\s\-_---/()()]+", "", alias):   #和粗洗后的alias相比较
        return canonical  #如果Key能对上,则回该key对应的值canonical

return s   # ④ 不在表里的(语文/数学/物理...)原样返回,只去了空白
#这里就用不到.get()方法了,因为不是拿可能没有的名称找在字典里找,而是反过来,先从字典里取出来再比

normalize_subject_name()的使命是只要有粘边连上关系的,都给你弄出来。

在课程表这里,只要在``科目``上有勾连,教师和课表就对应上跑不掉。

加了两个保护容错:

  • SUBJECT_NORMALIZE_EXCLUDE:避免哪天别名表里手滑写 "自习":"技术",把自习课也当成技术了。
  • 函数先查排除再查映射,顺序不能反。

5.定义标准化excel表头函数normalize_header( )

python 复制代码
def normalize_header(value: Any) -> str:
    text = clean_text(value)
    text = re.sub(r"\s+", "", text)  #r"\s+":匹配一个或任意多个空白字符(半角空格、全角空格、tab),全部替换为空。
    return text

把字符串首尾和中间的空白++全部删除++ ,只能用于表头,不要用于处理业务单元格

前面做的这些大多是清洗工作:clean_text粗洗,re.sub精洗全删空白,normalize_header也是

6.定义解析excel行字典用的函数`` get_first()

python 复制代码
def get_first(
    row: dict[str, Any],
    field_names: list[str],    #表头名称列表候选待用的
    default: str = "",
) -> str:
#定义一个新字典normalized来装清洗了的 row字典内容。
    normalized = {
        normalize_header(key): clean_text(value)
        for key, value in row.items()
        if key is not None
    }
for field_name in field_names:    #涵数头里表头名称列表
    key = normalize_header(field_name)

    if key in normalized:
        value = normalized[key]

        if value:
            return value

return default
(1)函数定义头部
python 复制代码
def get_first(
    row: dict[str, Any],
    field_names: list[str],
    default: str = "",
) -> str:
  • row: dict[str, Any]:Excel 解析出来的一行数据,字典;key 是原始表头,value 是单元格原始值。
  • field_names: list[str]:候选表头名称列表,按优先级排序,例如 ["科目","学科","课程名称"]越靠前优先级越高
  • default: str = "":全部字段都取不到有效值时返回的默认值,默认为空字符串。
  • -> str:函数返回值一定是字符串。

使用示例: subject = get_first(row, ["科目","学科","课程名称"]) 优先拿 "科目";如果这一列不存在 / 为空,尝试拿 "学科";再不行尝试 "课程名称";全都没有返回""

(2). 字典推导式构建 normalized 字典
python 复制代码
#表头清洗空白全删,单元值只删外部空白    
normalized = {
        normalize_header(key): clean_text(value)
        for key, value in row.items()
        if key is not None
    }
'''
上面推导式等价于普通for循环:
normalized={}   #默认是字典,集合需写set,如:s=set()
for key,value in row.items():
if key is not None:
new_key=normalize_header(key)
new_val=clean_text(value)
normalized[new_key]=new_val  #把新得到的一对赋给字典,随着循环,相当于追加
'''

normalized标准化后的行字典 。把原始 row 字典的表头单元格值全部清洗,转换成一个新字典。方便后续匹配,避免表头空格、大小写、全半角干扰。

python 复制代码
#如果想判断类型,工程代码里,一般不推荐直接用 `type()` 做判断,优先用 `isinstance()`。
normalized = {}
方式1 type()
print(type(normalized) == dict)   # True
方式2 isinstance()
print(isinstance(normalized, dict)) # True

拆解推导式:

  1. for key, value in row.items():遍历原始行字典,key对应原始表头,value对应 单元格内容。
  2. if key is not None:过滤,表头为None的直接跳过,不加入字典,防止异常。
  3. normalize_header(key)处理表头 key ,清洗表头:去除空格、符号,统一格式。解决 Excel 表头脏问题,例如原始表头" 科目 "→标准化后"科目"
  4. clean_text(value)处理单元格 value,调用之前的 clean_text:处理 None、BOM、首尾空格,输出字符串。

字典推导式(dict comprehension)拆解:

模拟 Excel 一行:

python 复制代码
row = {
    " 科目 ": "信 技",
    None: "垃圾数据",
    "教师": "张 老师",
    " 班级 ": None
}

推导式语法结构:

python 复制代码
{ 键表达式 : 值表达式  for循环部分  过滤条件 }

对应:

  1. 键表达式normalize_header(key) 对原始表头key做表头标准化:去掉表头多余空格、符号。" 科目 "normalize_header(" 科目 ")"科目"
    这个位置上的必是一个整体,如果是分散的多个参数则需用圆括号包起来。
  2. 值表达式clean_text(value) 单元格内容调用你之前的clean_text:处理 None、BOM、首尾空白,输出字符串。None"""信 技""信 技"(只删首尾,中间空格保留)
  3. 循环部分for key, value in row.items() 遍历原始行字典,key是原始表头,value是单元格内容。
  4. 过滤条件if key is not None ❗只保留表头 key 不为 None 的项; 如果 Excel 某列表头是None,直接丢弃这一列,不放进新字典,防止字典出现None作为 key 带来后续查找异常。

上面示例 row 执行后的结果:

python 复制代码
normalized = {
    "科目": "信 技",
    "教师": "张 老师",
    "班级": ""
}

👉总结这一块: 把原始row,生成一份表头、单元格全部清洗完毕的新字典 normalized。 后续所有查找,都用这份清洗后的字典,不再碰原始 row,消除表头脏字符带来匹配不到的 bug。

Python 推导式一共 4 种

分别:列表推导式、字典推导式、集合推导式、生成器表达式(生成器推导)

⚠没有元组推导式!圆括号的那个叫生成器表达式,不是元组推导。

1. 列表推导式 [ ... ]

方括号 [],产出 list

python 复制代码
# 普通循环
res = []
for i in range(5):
    res.append(i*2)
列表推导式
res = [i*2 for i in range(5)]
[0, 2, 4, 6, 8]
带过滤条件
res = [i*2 for i in range(5) if i > 1]

语法模板:

[表达式 for 变量 in 可迭代对象 if 条件]

2. 字典推导式 {k:v ... }

大括号,++键:值++ ,产出 dict,就是你代码里 normalized 用的那一种

python 复制代码
data = {"a":1, "b":2}
new_d = {k.upper(): v*10 for k,v in data.items() if v>0}
# {'A': 10, 'B': 20}

模板: {key_expr : value_expr for 变量 in 可迭代对象 if 条件}

3. 集合推导式 { ... }

大括号,只有表达式,没有冒号,产出 set,自动去重

复制代码
nums = [1,1,2,2,3]
s = {x*2 for x in nums}
print(s) # {2,4,6}

注意:{} 空大括号是字典;空集合只能用 set()

4. 生成器表达式 ( ... )

圆括号 ()不是元组推导,产出生成器对象(惰性求值,不一次性全部存内存)

复制代码
g = (i**2 for i in range(10))
print(g)
# <generator object <genexpr> at 0x...>

特点:

  • 不会一次性把全部结果放进内存;迭代的时候才逐个计算。
  • 如果要转元组:tuple( (i**2 for i in range(10)) )

四种对比速查表

表格

类型 符号 语法特征 返回对象
列表推导式 [] [expr for ...] list
字典推导式 {} {k:v for ...} 有冒号 dict
集合推导式 {} {expr for ...} 无冒号 set
生成器表达式 () (expr for ...) generator 生成器

带 if 过滤条件通用格式

所有推导式都支持末尾加 if 条件 做过滤:

python 复制代码
#列表
[x for x in range(10) if x%2==0]
#集合
{x for x in range(10) if x%2==0}
#字典
{k:v for k,v in d.items() if v>0}
#生成器
(x for x in range(10) if x%2==0)

不支持多分支 if‑else 写在末尾;如果要三元,写在最前面表达式位置:

python 复制代码
[x if x>2 else 0 for x in range(5)]

小坑提醒

  1. 字典和集合都用大括号,靠有没有冒号区分;
  2. 生成器表达式节省内存,大数据量优先;推导式直接生成完整容器占内存;
  3. 推导式不要写的太复杂,可读性变差就换回普通 for 循环。

快速记忆口诀

  • 方括号→列表;
  • 大括号有冒号→字典;
  • 大括号无冒号→集合;
  • 圆括号→生成器,不是元组

推导式 vs 生成器表达式核心区别

列表 / 字典 / 集合推导式:立刻全部计算,把所有结果存到内存,生成完整容器对象 生成器表达式:惰性计算(延迟求值),不一次性存全部数据,迭代的时候才算下一个值

1、语法形式对比(外包装不一样,但,都要有包装,裸是不行的)

复制代码
# 列表推导式 ------ [] ,得到list
lst = [x*2 for x in range(5)]
print(lst)   # [0, 2, 4, 6, 8]
生成器表达式 ------ (),得到generator对象
gen = (x*2 for x in range(5))
print(gen)   # <generator object <genexpr> at 0x...>
  • 列表推导执行完,内存里面已经有完整列表 5 个元素。
  • 生成器表达式执行完,什么数值都还没有计算,只是拿到一个 "计算公式"。

2、运行行为(最关键:惰性)

列表推导式

复制代码
lst = [x**2 for x in range(1000000)]

程序运行这一行,直接循环全部,把 100 万个数字全部生成出来放在内存。 👉一次性全部算出,占用内存大;数据量大时内存压力高。

生成器表达式

复制代码
gen = (x**2 for x in range(1000000))

这一行几乎不耗内存。 只有你去迭代它(for循环、next ())的时候,才现场算出下一个值,用完就扔掉,不会保存全部结果

复制代码
next(gen) # 0
next(gen) # 1
next(gen) # 4

生成器只能遍历一次,遍历完毕就耗尽,不能重复使用。

复制代码
gen = (x for x in range(3))
print(list(gen)) # [0,1,2]
print(list(gen)) # [] 已经空了,第二次拿不到任何数据

✅列表推导得到的 list 可以反复多次遍历。

3、能不能转成别的容器

生成器可以喂给 list () /tuple () /set (),一次性全部消费掉:

复制代码
gen = (x*2 for x in range(5))
lst = list(gen) # 把生成器全部算出,转为列表

一旦 list (gen),生成器就耗尽。

4、四种推导式与生成器对照表

表格

写法 产物 是否一次性加载全部数据 可重复遍历 内存消耗
[expr for ...] list 列表 ✅全部加载 ✅可以多次遍历
{k:v for ...} dict 字典 ✅全部加载 ✅可以多次遍历
{expr for ...} set 集合 ✅全部加载 ✅可以多次遍历
(expr for ...) generator 生成器 ❌惰性,需要才计算 ❌只能遍历 1 次 极小

注意:生成器表达式不是元组推导式 ,Python 没有元组推导。想要元组:tuple(x*2 for x in range(5))

5、if 条件、多重循环语法是一样的

语法层面两者写法几乎一样,只是外层括号不同:

复制代码
#列表推导
[x for x in range(10) if x%2==0]
#生成器表达式
(x for x in range(10) if x%2==0)

6、业务场景怎么选

✅用推导式(list/dict/set)

  • 数据量不大;
  • 需要多次读取、索引取值、后续还要修改;
  • 就像你课表代码中的字典推导式:normalized = { ... },需要完整字典随时查询,必须字典推导,不能用生成器。

✅用生成器表达式

  • 数据量很大,不想一次性把全部内容吃进内存;
  • 只需要遍历一次,遍历完就不再需要;
  • 常用于文件读取、大 Excel 逐行解析。

示例:大文件逐行处理,不要读全部进 list:

复制代码
# 生成器,一行一行处理,内存友好
lines = (line.strip() for line in open("big.txt","r"))
for l in lines:
    pass

7、易错坑

  1. 生成器 打印看不到内容,只会看到<generator object ...>,不是 bug,它不是容器
  2. 生成器只能迭代一次,第二次迭代为空。
  3. 生成器不能下标取值gen[0]直接报错;列表推导的 list 可以lst[0]

一句话总结

列表 / 字典 / 集合推导式:直接生成完整容器,所有结果都在内存,可反复使用。 生成器表达式:保存计算逻辑,惰性按需产出数据,省内存,只能遍历一遍。

python 复制代码
normalized = {normalize_header(key):clean_text(value) for key,value in row.items() if key is not None}

这里必须字典推导式 ,因为后续要 if key in normalizednormalized[key] 做字典查询;生成器做不到字典查询。

生成器只是顺序吐出数据,它没有建立 key‑value 映射表,不支持按键查找。


那生成器里面的元组怎么拿到值?

只能 for 循环逐个遍历,不能直接按键查询:

复制代码
for k, v in gen:
    print(k, v)

想要查 "科目",只能自己循环一遍逐个比对,没有 in[] 这种字典的快捷查询能力。

如果你非要把生成器变成字典,必须整体消费一遍,传给dict()

复制代码
gen = ((k.upper(),v) for k,v in row.items())
d = dict(gen)   # 全部迭代生成真正字典对象
print(d["科目"]) # ✅这时才可以查询

一旦执行dict(gen),就把生成器全部执行完毕,全部数据存入内存,此时就和字典推导效果差不多,失去惰性优势。

结合业务代码解释

get_first函数后续逻辑:

复制代码
if key in normalized:
    value = normalized[key]

这里需要按键快速查找

  • 如果normalized是字典:O (1) 直接查找,非常快。
  • 如果是生成器:没有 key 映射,不能in、不能[key]取值。

所以这里不能用生成器表达式,必须用字典推导式,直接构造出完整 dict。

关键小结

  1. dict:建立 key‑value 映射,支持按键查询 d[key]key in d,全部数据存内存。
  2. generator:只顺序产出元素,没有映射表,不能按键查询;只能 for 循环挨个拿。
  3. 生成器可以喂给dict()一次性转换成字典,但这一步就把全部数据加载进内存,惰性特性消失。

通俗比喻

  • 字典:一本字典书,知道每个词(key)在哪一页,直接翻页找到内容。
  • 生成器:一卷磁带,只能从头顺序播放,想要找某个词,必须从头到尾听完,不能直接跳转查找。

int(k,v) for k,v in d.items()] # ✅好:普通for循环 for k, v in d.items(): print(k, v)

什么时候适合用推导式? 当你需要收集结果存到容器的时候:

复制代码
# 收集所有value,过滤不为空
valid_values = [v for k,v in d.items() if v]

生成器就象包子机,包子机里没包子所以不能查询,推导式得出的象包子笼,包子笼里有包子,所以能查询。

这个比喻非常形象,再打磨一下:

  • 生成器 = 包子机 包子机本身不存放包子 ,只保存一套 "怎么做包子" 的流程。 你调用一次(next()/for 迭代),它现场做 1 个包子吐给你,吃完就没; 机器里面没有一堆现成包子,你不能伸手进去直接找第 3 个包子、找某个特定包子,不能随机查询。 而且包子机只能从头到尾做一轮,全部吐完之后机器就空了,再来一遍不出货。

    gen = (f"包子{i}" for i in range(5))

    机器在这,但是还没有任何包子

    gen["包子2"] ❌ 不能查询,机器里没有成品

  • 列表 / 字典推导式 = 蒸好的一笼包子 执行推导式的时候,一次性全部加工完成,所有包子全部实实在在摆在笼子(内存)里面。 包子都已经做好存好了,你可以随便挑、查找、反复拿。 字典就好比:每个包子贴了标签(key),直接按标签找对应的包子。

    baozi_dict = {f"key{i}":f"包子{i}" for i in range(5)}

    一笼全部做好,放在内存

    print(baozi_dict["key2"]) ✅直接按标签取出包子

补充两个小细节贴合你的比喻

  1. 你可以把包子机(生成器)的产出,全部倒进蒸笼:

    gen = (f"包子{i}" for i in range(5))
    long = list(gen) # 把机器全部生产一遍,全部装入笼子

👉一旦全部倒出来,包子机就空了;现在有笼子可以查询,但是惰性的优势就没了。

  1. 包子机适合量特别大:一万个包子,不需要一次性全部蒸出来占地方,吃一个做一个。 而笼屉(推导式)适合需要反复查找、随机访问,代价是要把所有包子一次性全部做好占内存。

对应代码里的场景

normalized 需要随时按键查找列的值,必须要 "蒸笼"(字典推导) 。 如果换成包子机(生成器),机器里没有现成的 key‑value,没法做 if key in normalized 查询。

👉总结这一块: 把原始row,生成一份表头、单元格全部清洗完毕的新字典 normalized。 后续所有查找,都用这份清洗后的字典,不再碰原始 row,消除表头脏字符带来匹配不到的 bug。

3. 循环遍历候选字段名---继续解读get_first()

python 复制代码
for field_name in field_names:
        key = normalize_header(field_name)

        if key in normalized:
            value = normalized[key]

            if value:
                return value

    return default
复制代码
    for field_name in field_names:

循环传入的候选表头列表,顺序很重要,从第一个(最高优先级)开始挨个试 。 例子:["科目","学科","课程名称"],先试科目,再试学科,最后试课程名称

4. 对要查找的字段名,同样标准化

复制代码
        key = normalize_header(field_name)

关键点:查找的目标字段名,也要用同样的normalize_header()做标准化 。 例:代码写的查找字段是"科目",原始 Excel 表头是" 科目 "; Excel 表头经过 normalize_header 变成"科目";查找的field_name="科目"normalize_header 之后也是"科目",两边标准统一,才能匹配上。
如果两边不做统一标准化:原始表头带空格,代码里写不带空格,就会出现明明有这一列,却匹配不到。

5. 判断标准化后的 key 是否存在字典中

复制代码
        if key in normalized:

判断清洗后的表头,是否在normalized字典里。

  • True:该行存在这一列;
  • False:Excel 没有这个表头,跳到下一个候选字段。

6. 取出值,判断非空

复制代码
            value = normalized[key]
            if value:
                return value
  • value = normalized[key]:取出已经经过clean_text()清洗后的单元格字符串。
  • if value::字符串真值判断。""空字符串为 False;有内容字符串为 True。

clean_text 已经保证 value 一定是字符串,if value等价于if value != ""

  • ✅一旦找到存在且非空 的值,立刻return value,函数直接结束,后面候选字段不再执行

这就是get_first名字的由来:返回第一个有效的值。

7. 全部候选字段都没有有效数据

复制代码
    return default

for 循环走完,所有候选字段:要么列不存在,要么单元格是空。返回传入的 default(默认是空字符串"")。

打印python的保留字:

复制代码
import keyword
print(keyword.kwlist) # 打印全部Python保留字列表

输出列表里面看不到 dictliststrAnyrow

记忆

  • 保留字:语法硬规定,不能拿来做变量名,赋值直接报错。
  • dict/list/str:内置类,可以赋值覆盖,不报错,但不要这么做。
  • Any:typing 库导入的普通对象,不是关键字。

需求:打印全部 Python 关键字,每行输出 10 个

keyword.kwlist 是一个字符串列表。利用切片循环,每批取 10 个打印。

复制代码
import keyword

kw_list = keyword.kwlist
# 步长10,分片输出,每行10个
for i in range(0, len(kw_list), 10):
    chunk = kw_list[i:i+10]
    print(chunk)

输出示例:

复制代码
['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class']
['continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global']
['if', 'import', 'in', 'is', 'lambda', 'match', 'nonlocal', 'not', 'or', 'pass']
['raise', 'return', 'try', 'while', 'with', 'yield']

美化版:空格隔开,不要方括号,更整洁

复制代码
import keyword

kw_list = keyword.kwlist
for i in range(0, len(kw_list), 10):
    chunk = kw_list[i:i+10]
    print("  ".join(chunk))

输出:

复制代码
False  None  True  and  as  assert  async  await  break  class
continue  def  del  elif  else  except  finally  for  from  global
if  import  in  is  lambda  match  nonlocal  not  or  pass
raise  return  try  while  with  yield

原理说明

  • range(0, len(kw_list), 10):从 0 开始,每次 + 10
  • kw_list[i:i+10]:切片,取出下标 `

[i, i+10)` 的一批元素。最后不足 10 个也会正常输出剩余全部。

Python 版本不同,保留字列表会略有差别(3.10 增加 match)

完整业务示例演示

复制代码
# 模拟excel一行
row = {
    " 科目 ": None,
    "学科": "通用技术",
    "课程名称": "信息技术"
}

# 优先取科目,科目为空,继续尝试学科
res = get_first(row, ["科目","学科","课程名称"])
print(res) # 输出 "通用技术"

执行流程:

  1. 生成 normalized:表头全部清洗,值全部 clean_text;" 科目 "→key:"科目",value 变为""
  2. 第一轮循环field_name="科目"
    • normalize_header 得到"科目";key 存在;value 是""if value不成立,不 return,进入下一轮循环。
  3. 第二轮循环field_name="学科"
    • normalize_header 得到"学科";key 存在;value="通用技术"非空;直接 return "通用技术",函数结束。

"课程名称"根本不会被访问。

核心设计亮点(工程上的巧思)

  1. 表头两边统一标准化 :原始 row 的表头做normalize_header;待查找的字段名也做normalize_header。解决 Excel 表头带空格、多余符号导致匹配失败。
  2. 值提前统一 clean_text 清洗:字典生成阶段全部清洗,循环里面只做判断,避免重复调用清洗函数。
  3. 短路返回第一个有效值,找到立刻 return,不再向后查找。
  4. 兼容多表头别名,一份解析代码可以适配多种格式的 Excel 课表文件。

潜在坑点(需要注意)

  1. 只判断非空字符串 。如果单元格内容是空格,经过clean_text后变成空字符串,会被当做无效,继续找下一个字段,符合课表解析需求。
  2. 函数只会返回第一个非空;不会把多个字段拼接。
  3. normalize_header这个函数你要保证存在,否则直接报错。

极简流程口诀

原始行全部清洗表头和单元格 → 按优先级遍历候选列名 → 列名也做同样清洗 → 找到第一个存在且不为空的就返回 → 全部无效返回默认值。

get_first 参数复盘

函数定义:

复制代码
def get_first(
    row: dict[str, Any],
    field_names: list[str],
    default: str = "",
) -> str:

一共3 个形参

  1. row
  2. field_names
  3. default 👉带默认值 ="",是可选参数

调用代码:

复制代码
res = get_first(row, ["科目","学科","课程名称"])

这里只传了2 个实参 。 👉default没有传,就自动使用定义时写的默认值:default = ""

参数对应关系(位置传参)

复制代码
get_first( row,    ["科目","学科","课程名称"] )
           ↑            ↑
        row参数     field_names参数
default 不传入,使用默认值 ""

三种调用写法全部合法

复制代码
#写法1:不传default,用默认""(你示例的写法)
res = get_first(row, ["科目","学科","课程名称"])

#写法2:显式传入default
res = get_first(row, ["科目","学科","课程名称"], default="无数据")

#写法3:关键字传参,不怕顺序混淆,可读性高
res = get_first(
    row=row,
    field_names=["科目","学科","课程名称"],
    default="无"
)

规则:带默认值的参数可以省略不传

Python 函数规则:

  • 参数写了=默认值,调用的时候可以不给这个参数;内部自动取这个默认。
  • 没有写默认值的参数(rowfield_names)调用必须传,不传直接报错

函数 3 个参数,但调用时必填 2 个,第 3 个可选

怎么快速看清对应关系?2 个实操技巧

技巧 1:位置参数,按顺序一一匹配

实参从左往右,依次匹配定义的形参;没给的,就拿默认值。 定义顺序:row → field_names → default

调用:get_first(A,B)

  • A 赋值给 row
  • B 赋值给 field_names
  • default 没给,取""

调用:get_first(A,B,"--") ‑A→row ‑B→field_names ‑"--"→default

技巧 2:关键字传参(强烈建议写业务代码用这个)

参数名=值不再受顺序约束,一眼看清楚给谁赋值

复制代码
res = get_first(
    row = row,
    field_names = ["科目","学科","课程名称"],
    default = ""
)

跑一遍完整示例

复制代码
#模拟
row = {
    " 科目 ": None,
    "学科": "通用技术",
    "课程名称": "信息技术"
}
#调用,default省略,内部default=""
res = get_first(row, ["科目","学科","课程名称"])
print(res) #通用技术

执行流程回顾:

  1. row传入字典;field_names = ["科目","学科","课程名称"]default=""
  2. 生成 normalized 字典," 科目 "清洗后 key 为科目,value 经过 clean_text 变成空字符串""
  3. 循环候选字段:
  • "科目":key 存在,但 value 是空,跳过
  • "学科":key 存在,value="通用技术" 非空,return 返回。

易错坑

定义的时候,带默认的参数必须放在无默认参数的后面,下面是错误写法:

复制代码
def get_first(row:dict, default:str="", field_names:list[str]): # ❌语法报错
    pass

一句话总结

get_first 一共 3 个形参,其中default有默认值,调用时可以省略,只传前面两个必填参数。 参数匹配:不传名字就按顺序匹配;写xxx=yyy关键字传参就按名字匹配,和顺序无关。

相关推荐
sxjk19872 小时前
GPON带宽共享与忙时拥塞机制深度解析
程序人生
2601_962099082 小时前
replit如何运行python
python·代码编辑器·编程环境·在线ide·replit
头发够用的程序员2 小时前
别被 TOPS 参数迷惑:手把手推导边缘 GPU 整型算力
人工智能·python·ubuntu·计算机视觉·硬件架构·边缘计算
TechWayfarer2 小时前
做IP归属地查询时总是查不准?IP纯净度检测实战:从“查归属地“到识别“脏IP“的四个维度
服务器·网络·python·tcp/ip·安全·web安全
小溪学编程2 小时前
Java InputStream 详解:从基础到实战
java·python·php
D_codingXuChu3 小时前
2026物联网应用开发服务商D-coding定制开发
物联网·信息可视化·开发经验·d-coding
笨笨饿3 小时前
#137_死机恢复现场_Armino平台AP系统swd调试
git·python·stm32·单片机·嵌入式硬件·物联网·vim
2601_962382433 小时前
python常用函数大全pdf-python函数大全.pdf
python·内置函数·io操作·集合操作·数学运算
奈斯先生Vector4 小时前
AIGC 视频生成实战:用 Kling Video 拆解文生视频、图生视频与完整工作流
开发语言·人工智能·windows·python·aigc·音视频