烟台大学一课堂笔记共享协作Skill
概述
本 Skill 的核心是协作方式,不是笔记软件。它解决的是同一门课里"你记了前半段、我漏了后半段"的问题——先帮用户选对笔记分工模式,再评估课程是否适合协作,再设计避免搭便车的机制,最后用工具把协作落地。
工作流程固定为五步:明确场景 → 推荐协作模式 → 评估课程适配性 → 设计反搭便车机制 → 工具落地。
网页交互展示(每次使用必做)
每次使用本 Skill 时,必须同步调用配套交互网页 assets/ytu-notes-collab.html,作为与用户互动的主要入口:
- 用 present_files 打开该网页(HTML 会自动在预览面板渲染,标题为「烟台大学 · 课堂笔记共享协作」)
- 引导用户直接在网页上操作:30 秒协作模式推荐向导、课程适配五问打分器、三模式对比、反搭便车公约复制
- 依据网页上的推荐/评分结果,再结合本文件附录 A–C 的参考文档给出完整方案与公约
- 单人整理场景同样先打开网页,跳过协作模块,直接进入「工具落地」
网页内置全部核心内容,与用户沟通时保持网页模块名称与 SKILL.md 一致。
核心工作流
第一步:明确场景
先问清三个问题,再进入推荐环节:
1. 用户是一个人整理,还是多人协作?(单人直接跳到「工具落地」,多人继续)
2. 这门课什么类型、班级多少人、课堂是否允许用电子设备?
3. 小组目前是零基础还是已有协作习惯?
第二步:推荐协作模式(核心)
三种模式各有适用场景,对照下表速选,详细运作方式、优缺点、失败点对策见「附录 A:三种协作模式详解」:
| 模式 | 一句话说明 | 主要优点 | 主要缺点 | 优先适用 |
|---|---|---|---|---|
| 轮流记 | 排轮值表,每次课 1–2 人完整记录,全员补漏 | 单人负担小、质量稳定、管理成本低 | 依赖当值者、无多视角、怕拖延 | 大班、内容结构化、进度快的课 |
| 各自记重点后合并 | 每人记不同侧重,课后合并成完整版 | 覆盖最全、合并即复习、适合理解型课 | 每人每次都要记、合并成本高 | 3–6 人小团队、强调理解的课 |
| 在线文档协同 | 共享文档实时多人编辑,边上课边补全 | 实时性强、贡献透明、免合并 | 依赖网络设备、易干扰听课、有冲突 | 小班互动课、机房课、信任度高的小组 |
快速选择规则:
- 教室不能用电子设备 → 排除在线协同,从轮流记/各自记重点后合并里选
- 课内容结构化、信息量大 → 轮流记
- 课强调理解消化、需要多视角 → 各自记重点后合并
- 团队 ≤5 人、设备允许、都愿意实时动手 → 在线文档协同
- 拿不准 → 推荐混合模式:平时轮流记 + 章节末各自划重点合并(详见附录 A「混合模式」一节)
给出推荐时必须附带该模式的优缺点与适用条件,让用户能自行权衡,不得只丢一个结论。
第三步:评估课程适配性
不是所有课都值得协作。按三个维度判断:内容结构化程度、信息完整 vs 过程完整、班级规模与考核方式。完整分级表(高/中/低适配)与五问评估清单见「附录 B:课程适配性评估」。
核心结论直接引用:
- 高适配:公共理论课、基础课、语言类编程课、记忆密集型课
- 中适配:数学推导密集课、编程实践课、外语课(用针对性对策)
- 低适配:纯讨论课、实验课、体育艺术课、无结构口述课、老师抽查个人笔记的小班
第四步:设计反搭便车机制
搭便车是协作第一死因,用机制而非道德解决。三大原则:贡献可验证(轮值表公开/署名/版本历史)、收益与贡献挂钩(全量笔记分层开放/考前权益)、降低贡献门槛(模板化、脚本化、1 条补充也算贡献)。可直接复制使用的《协作公约》模板与常见错误清单见「附录 C:反搭便车机制设计」。
第五步:工具落地
选定模式后,用以下工具把协作落地(脚本使用说明见下方「工具支撑」,源码见「附录 G:脚本源码」):
- 初始化课程笔记本:
init_notebook.py创建「学期 > 课程 > 章节」目录结构 - 每次课按标准模板记录:见「附录 F:标准模板」
- 各自记重点后合并:
merge_notes.py自动去重、标注来源、生成贡献报告(兼作搭便车核查依据) - 复习检索:
search_notes.py按关键词/课程/章节全文搜索
目录结构
笔记库采用统一的目录组织方式:
notes/ # 笔记库根目录
├── 2025-春季/ # 按学期分目录
│ ├── 高等数学/ # 按课程分目录
│ │ ├── README.md # 课程信息(课程名、教师、学分等)
│ │ ├── 第01章-函数与极限/
│ │ │ └── notes.md # 章节笔记正文
│ │ ├── 第02章-导数与微分/
│ │ │ └── notes.md
│ │ └── merged/ # 合并笔记输出目录
│ │ └── merged_notes.md
│ └── C语言程序设计/
│ ├── README.md
│ └── ...
└── 2025-秋季/
└── ...
工具支撑
1. 笔记整理与结构化
将零散课堂记录整理为结构化 Markdown 笔记,模板见「附录 F:标准模板」。
# 第X章 章节名称
## 课程信息
- 课程:课程名
- 日期:YYYY-MM-DD
- 记录人:姓名
## 知识点
### 知识点1名称
**定义:** ...
**要点:**
- 要点1
**例题:**
> 题目内容
> 解答过程
## 课堂重点
- ⭐ 重点1
## 疑问与待办
- [ ] 疑问1
整理规则:每个知识点含「定义/概念」和「要点」;有例题加「例题」;末尾汇总「课堂重点」与「疑问与待办」;⭐ 标重点,❓ 标疑问。
2. 多人协作合并
将多位同学的同节课笔记合并为完整版本:
python merge_notes.py \
alice_notes.md bob_notes.md charlie_notes.md \
--output merged/merged_notes.md \
--names "张三,李四,王五"
合并策略:标题匹配归并、重复内容去重、独有内容标注 [贡献者: 姓名]、仅一人记录的知识点标注 [仅 XX 记录]。合并报告(参与人数、合并知识点数、新增内容数)兼作贡献记录,可用于搭便车核查。
3. 笔记搜索与检索
python search_notes.py "极限" --path ./notes # 基本搜索
python search_notes.py "导数" --path ./notes --course "高等数学" # 限课程
python search_notes.py "拉格朗日" --path ./notes --context 3 # 带上下文
4. 初始化课程笔记本
python init_notebook.py \
--course "高等数学" \
--semester "2025-春季" \
--chapters "函数与极限,导数与微分,中值定理,不定积分,定积分" \
--path ./notes
文档结构说明
本 Skill 包精简为两个文件,全部内容均内联于下:
ytu-notes-share/
├── SKILL.md # 主文档:五步工作流 + 全部附录
└── assets/
└── ytu-notes-collab.html # 配套交互网页(每次使用必开)
- SKILL.md — 主文档:正文为五步工作流,附录 A–G 收纳全部参考文档、标准模板与脚本源码
- assets/ytu-notes-collab.html — 交互网页:内置模式推荐向导、课程适配打分、三模式对比、反搭便车公约,随 Skill 同步打开使用
附录索引:
- 附录 A:三种协作模式详解(运作方式、优缺点对比表、失败点与对策、混合模式、30 秒模式选择决策)
- 附录 B:课程适配性评估(三维度判断、高/中/低适配分级表、五问评估清单)
- 附录 C:反搭便车机制设计(三大原则、可复制的《协作公约》模板、常见错误清单)
- 附录 D:笔记结构规范(笔记结构、格式标准、命名规范、协作标记)
- 附录 E:多人协作合并指南(合并流程、策略详解、最佳实践、常见问题)
- 附录 F:标准模板(单节课笔记模板、课程 README 模板)
- 附录 G:脚本源码(init_notebook.py、merge_notes.py、search_notes.py)
使用建议
- 每次使用先打开
assets/ytu-notes-collab.html交互网页,让用户在上面作答/打分,再给详细方案 - 多人协作场景,先走「第二步:推荐协作模式」再谈工具,不要一上来就跑合并脚本
- 推荐模式时始终附优缺点与适用条件,尊重用户对团队规模的判断
- 学期初就用「附录 C」的公约模板把规则立起来,别等出问题再补
- 合并后的笔记存放在课程的
merged/子目录下,不覆盖原始个人笔记
附录 A:三种协作模式详解
同一门课多人协作记笔记,本质是回答三个问题:谁来记、怎么记、何时合并。三种主流模式各有利弊,没有绝对最优,关键是匹配课程特点和团队情况。
模式一:轮流记(Rotation)
运作方式
- 按座位/学号/抽签排定轮值表,每次课由 1–2 人负责完整记录
- 当值者课后 24 小时内按统一模板整理并发布到共享位置
- 其他人收到笔记后只做「补漏 + 纠错」,不重写
- 轮值表固定公开,缺课/请假需提前换班并告知小组
优点
- 单人负担小:一学期按 30 次课算,10 人小组每人只需完整记 3 次
- 质量容易把控:每次只有一个责任主体,笔记结构统一、无重复劳动
- 管理成本低:不需要实时协调,适合课程排布紧、大家时间不同步的情况
- 出勤保障:当值者必须到场,客观上降低逃课率
缺点
- 笔记质量取决于当值者:某次轮到记性差/懒得写的人,全组这周笔记就垮
- 理解断层:非当值者可能只"存"不"看",到考前才发现某次笔记看不懂
- 时效脆弱:当值者拖延或丢失,整周复习材料就缺一块
- 不产生"多方视角":本质是轮流单人记录,缺了相互补充的化学反应
适用场景
- 内容密度高、课堂节奏快、板书/PPT 结构清晰的课(高数、C语言、马原等)
- 团队规模 4–10 人、成员时间难以同步
- 有稳定的共享渠道(QQ群/网盘/在线文档)
常见失败点与对策
| 失败点 | 对策 |
|---|---|
| 当值者笔记潦草或缺漏 | 轮值表 + 匿名补漏制度:非当值者必须提交至少 1 条补充/修正 |
| 当值者拖延 | 约定发布截止时间(课后当晚),超时由下一顺位补位 |
| 笔记"存而不看" | 每章结束后轮值者出一页「本章速览」,逼大家真正阅读 |
模式二:各自记重点,课后合并(Divide & Merge)
运作方式
- 每人上课时都记(可自由记,不限格式),但刻意分工记不同侧重
- 例 1:按位置分——前 1/3 的人记黑板左侧、中间记右侧、后排记提问与补充
- 例 2:按内容分——一人盯定义公式、一人盯例题步骤、一人盯老师口头强调的考点提示
- 例 3:按能力分——记性好的记框架,写字快的记细节,理解深的标注疑问
- 课后(建议 48 小时内)把各自笔记交给整理人,用合并脚本合成一份完整版
- 合并时保留「独有内容 + 来源署名」,分歧点并列展示
优点
- 覆盖最全:多人视角互相补漏,漏记概率显著低于单人
- 产生二次理解:合并过程本身就是一次复习,整理人获益最大
- 灵活性高:个人笔记仍是自己的,不依赖别人;某次缺席不影响记录覆盖
- 适合"理解型"课程:能把各自不同的解题思路、联想记忆都沉淀下来
缺点
- 人力成本最高:每人每次都要记,课后还要合并,总耗时≈单人记录 × 2–3
- 合并是瓶颈:需要有人承担整理角色,且合并质量决定最终质量
- 风格冲突:不同人笔记结构、详略、符号体系不一致,合并时需统一
- 易演变为"各记各的":如果没有明确分工,最后就是三份独立笔记,没形成合力
适用场景
- 需要理解消化、人人参与度高的课(专业课理论课、经济类、法学类)
- 团队人数 3–6 人为宜,再多合并成本失控
- 大家都能坚持上课、愿意花时间的人
常见失败点与对策
| 失败点 | 对策 |
|---|---|
| 分工形同虚设,各记各的 | 课前 5 分钟明确分工口令("前两排记左侧板书"),课后点名核对分工是否执行 |
| 合并拖到考前一周 | 定死合并节奏:每次课后 48 小时内合并,由固定「整理人」轮值 |
| 合并后没人看 | 整理人出「差异摘要」:本次合并新增了哪些别人没记到的点 |
模式三:在线文档协同(Live Co-editing)
运作方式
- 创建共享在线文档(腾讯文档/飞书/语雀/WPS 云文档),按章节分页
- 上课时多人同时在线编辑:主讲人记主干,其他人实时补充、提问写评论
- 课后收敛:标注待确认点,教师答疑后修正
- 文档历史版本自动留存,可回溯任何人的贡献
优点
- 实时性强:边上课边补全,当场解决漏记
- 贡献透明:编辑记录、评论、版本历史天然可见,搭便车者一目了然
- 无需课后二次合并:省掉合并环节的时间成本
- 沉淀即成果:文档本身就是最终笔记,考前直接复习
缺点
- 强依赖网络与设备:教室网络差/不让带电脑的课直接失效
- 课堂干扰:多人同时敲字可能影响听课专注,也可能被老师视为不专心
- 协调成本前置:需要有人建文档、分章节、维护结构
- 实时冲突:同页多人编辑易互相覆盖,需要约定分区(按标题分块)
- 期末留存问题:在线文档权限过期、账号注销等,资料可能丢失
适用场景
- 小班课、讨论课、实验课等互动型课堂
- 机房/网络条件好的课程
- 团队小(2–5 人)、彼此信任、行动力强
常见失败点与对策
| 失败点 | 对策 |
|---|---|
| 同页编辑冲突 | 按标题分块各管一段,或按"先到先写、评论补充"规则 |
| 全员在线但全员不写 | 指定「主讲记录人」,其余人只做评论补充,职责仍然明确 |
| 课堂变成"打字比赛" | 约定只记关键信息(公式/例题/考点),不逐字记 |
三模式速查对比表
| 维度 | 轮流记 | 各自记重点后合并 | 在线文档协同 |
|---|---|---|---|
| 单人单次负担 | 低(几周一次) | 高(每次都要) | 中(边听边记) |
| 笔记完整性 | 中(依赖当值者) | 高(多视角互补) | 高(实时补全) |
| 理解深度 | 低(非当值者只看) | 高(合并即复习) | 中 |
| 课后耗时 | 低 | 高(需合并) | 最低(实时完成) |
| 管理复杂度 | 低 | 中 | 中 |
| 对设备网络依赖 | 无 | 无 | 强 |
| 贡献可追溯性 | 中(轮值表) | 高(署名+合并报告) | 最高(版本历史) |
| 团队规模上限 | 10 人左右 | 3–6 人 | 2–5 人 |
混合模式(推荐实践)
不要把自己锁死在一种模式里,按阶段切换:
- 学期中(平时):轮流记为主,保证低负担 + 稳定产出;当值者之外的成员课后在共享文档里补 1–2 条自己听到的补充点(成本极低,但保住了"多方视角")
- 章节末(复习期):切换到"各自记重点后合并"——每人对着整章笔记划自己的重点/疑问,交给整理人合成「复习冲刺版」
- 考前:在线文档汇总全组疑问清单,统一找老师答疑
这套组合拳里,平时用轮流的低负担保证"有人记",复习期用合并的深度保证"记得住"。
模式选择决策问题(30 秒快速判断)
按顺序回答,落到哪个分支就选哪个模式:
- 教室网络和电子设备允许吗?→ 不允许,跳到 3
- 团队是否 ≤5 人且大家都愿意实时动手?→ 是:在线文档协同
- 这门课内容是否结构化(有明确板书/PPT 骨架)?→ 是:轮流记
- 这门课是否强调理解消化、需要多视角?→ 是:各自记重点后合并
- 都拿不准?→ 默认轮流记 + 章节末合并的混合模式
附录 B:课程适配性评估
笔记协作不是所有课都值得做。判断标准不是"课重不重要",而是这门课的内容形态是否适合多人分工记录。
三个核心判断维度
1. 内容结构化程度
- 有清晰板书/PPT 骨架、知识点边界明确的课 → 适合分工
- 内容碎片化、跳跃性强、以讨论为主的课 → 分工难以执行
2. 信息完整性 vs 过程完整性的要求
- 信息完整型:记住结论、公式、考点就够了(马原、毛概、经管类)→ 协作收益高,因为多人互补能补全信息
- 过程完整型:必须看到完整推导/实验步骤才能复习(数学推导、编程排错、实验流程)→ 协作风险高,一人记漏一步,整份笔记就废
3. 班级规模与考核方式
- 大班(60+ 人)、老师不互动 → 内容单一线性,轮流记最划算
- 小班(<30 人)、互动多 → 需要捕捉提问与口头补充,多人/实时协作更优
- 考核:闭卷背诵型 → 笔记全面性重要;开卷/理解应用型 → 笔记的"别人视角"更重要
课程适配分级
高适配(强烈推荐协作)
特征:内容结构化、信息量大、进度快、有明确知识点边界
| 课程类型 | 原因 | 推荐模式 |
|---|---|---|
| 公共理论课(马原/毛概/史纲/思修) | 内容量大、考点固定、背诵为主 | 轮流记 |
| 基础课(高数/线代/概率论) | 定义定理例题结构清晰 | 轮流记为主,例题多需合并补全 |
| 语言类(C语言/Python/Java) | 概念+语法+例题分层清晰 | 轮流记 + 章节末合并 |
| 大班专业课(经济学原理/管理学) | 知识点多、老师照PPT讲 | 轮流记 |
| 记忆密集型(法律、医学基础、历史类) | 覆盖面广,多人互补收益大 | 各自记重点后合并 |
中适配(有条件的协作)
特征:内容有一定结构化,但存在过程性/实践性内容
| 课程类型 | 风险 | 对策 |
|---|---|---|
| 数学推导密集课(数分/高代) | 推导过程易记漏 | 轮流记时当值者重点记"步骤 + 卡点";课后合并补例题 |
| 编程实践课(数据结构实验/Web开发) | 上课以写代码为主,笔记少 | 只协作记"作业要求 + 常见坑 + 老师口头提示",代码各自留档 |
| 外语课 | 口语听力为主,文字笔记有限 | 协作记"生词表 + 句型 + 课堂例句",轮流分担 |
| 考研/考证课 | 内容重要但每个人复习目标不同 | 各自记重点后合并,保留个人扩展空间 |
低适配(不建议/慎用协作)
特征:内容无法前置结构化,或协作成本远超收益
| 课程类型 | 原因 |
|---|---|
| 纯讨论课/研讨课 | 观点流动,笔记是个人思考记录,合并价值低 |
| 体育/艺术实践课 | 动作要领靠练不靠记 |
| 实验课 | 过程个性化,数据各自生成,共享易引发抄袭嫌疑 |
| 老师不按套路讲、全程口述跳跃的课 | 结构化成本极高,单人录播/录音更靠谱 |
| 10 人以下小班且老师课堂抽查笔记 | 抽查按人头,共享笔记可能被认定未完成个人任务 |
五问评估清单
对任何一门课,答完这 5 题即可定级:
- 这门课有没有稳定的内容骨架(PPT/板书/教材章节)?——有得 1 分
- 复习时"知道结论"够不够用,还是必须看懂完整推导/步骤?——够用得 1 分
- 班级人数是否 ≥30 人、课堂互动少?——是得 1 分
- 课程考核是否以闭卷知识点记忆为主?——是得 1 分
- 全组是否都能稳定出勤?——是得 1 分
- 4–5 分:高适配,放心协作
- 2–3 分:中适配,用对策表中的针对性方案
- 0–1 分:放弃协作,改用录音+个人整理
一个反直觉的提醒
越"难"的课越不要全员各自记然后合并——推导过程一人漏一步全组崩溃。
越"简单"的课反而越适合协作——因为信息量大、考点机械,多人互补的边际收益最高。
正确的分配是:把协作精力投在"量大但不难"的课上,把个人精力留给"难但量小"的课。
附录 C:反搭便车机制设计
搭便车(free-rider)是笔记协作的第一死因:几个人认真记,其他人白嫖,认真的人渐渐也不干了,协作解体。解决思路不是"道德教育",而是机制设计——让"不贡献"的成本高于"贡献"的成本,让贡献可验证、可追溯、有回报。
搭便车为什么必然发生
- 笔记是公共物品:我不记也能看别人的,收益不排他
- 贡献成本即时(每次上课都要花时间),收益延迟(考前才用得上)——短期理性的人会倾向不干
- 课堂场景没有惩罚机制,不贡献没有直接后果
明白了成因,对策就清楚了:让收益排他一点、让贡献可验证、让不贡献有代价。
机制设计原则
原则一:贡献可验证(让"记没记"看得见)
| 手段 | 做法 |
|---|---|
| 轮值表公开 | 全组可见的排班表,谁记哪次一目了然,缺席/换班必须公示 |
| 署名制 | 每份笔记、每个补充点都标注贡献者姓名;合并脚本自动生成「贡献者+独有内容」报告 |
| 版本历史 | 用在线文档的编辑记录,谁写了什么有据可查 |
| 发布截止 | 约定课后 24 小时发布,超时记录在案 |
原则二:收益与贡献挂钩(让"不记"吃亏)
| 手段 | 做法 |
|---|---|
| 全量笔记 vs 精简版分层 | 完整笔记只对贡献者开放;旁观者只能拿到「要点速览」,想要全量就必须参与 |
| 考前答疑权 | 合并版笔记的疑问清单只整理给贡献者;白嫖者自己去找老师 |
| 换班/补位规则 | 缺席当值必须提前找人换班并说明原因,临时放鸽子者承担下轮双倍记录 |
| 互评积分 | 每章末小组互相打分(笔记质量、补充数量),积分低的在组内公示 |
原则三:降低贡献门槛(让"记"不难)
搭便车很多时候不是坏,是懒/忙。把贡献成本降下来,搭便车自然减少:
- 轮流记模式下,非当值者的贡献只需 1 条补充(一句话都行),别要求每人写完整笔记
- 提供标准模板(见「附录 F:标准模板」),填空式记录,10 分钟能搞定
- 用合并脚本自动去重、自动标注来源,整理人不做苦力
- 允许"贡献非笔记形式":整理合并、做章节速览、找老师答疑也算贡献,扩大参与面
落地规则包(可直接复制给小组用)
课堂笔记协作公约(示例)
1. 成员:___、___、___(共 __ 人),共享位置:___
2. 协作模式:平时轮流记,章节末各自划重点合并
3. 轮值表:见下表,换班需提前 24 小时在群内说明
4. 当值者义务:课后 24 小时内按模板发布笔记,逾期记一次
5. 非当值者义务:每次课后补至少 1 条【补充/纠错/疑问】,记入贡献表
6. 贡献表每月公示:记录数、补充数、合并次数
7. 全量笔记与疑问清单仅对贡献者开放(按贡献表核实)
8. 连续 3 次无贡献且无正当理由者,移出共享群(可观察期后回归)
9. 期末每位贡献者按贡献表排名,前三名获得全组最全的「考前冲刺包」
软性方案:比机制更重要的三件事
- 控制规模:3–6 人最优。人越多越匿名,搭便车越难察觉。人多的班拆成多个小组。
- 目标对齐:开组会时讲清楚"这不是帮忙,是共同生产复习资料",让每个人有主人翁感而非受惠感。
- 及时止损:发现 1–2 人持续白嫖,第一时间私聊沟通,别等情绪积累。沟通无效就按公约处理,别让个别人拖垮全组。
需要避免的常见错误
| 错误做法 | 为什么错 |
|---|---|
| 把公约写得像讨伐檄文 | 激起逆反,好成员也流失 |
| 完全依靠自觉和人情 | 短期有效,学期中后期必然崩塌 |
| 白嫖者也能拿到全量笔记 | 等于为搭便车发奖金,认真的人最先退出 |
| 一人身兼记录+整理+发布 | 单点负担过重,最容易撂挑子,务必轮换 |
| 考前才检查贡献 | 木已成舟,机制应在学期初就建立并公示 |
附录 D:笔记结构规范
本文档定义课堂笔记的标准结构和格式规范,确保所有笔记风格统一、便于协作合并和搜索检索。
标准笔记结构
一篇完整的课堂笔记包含以下部分:
# 第X章 章节名称
├── 课程信息(课程、教师、日期、记录人)
├── 知识点
│ ├── 知识点1
│ │ ├── 定义/概念
│ │ ├── 要点列表
│ │ └── 例题(可选)
│ ├── 知识点2
│ └── ...
├── 课堂重点(⭐ 标记汇总)
└── 疑问与待办(❓ 标记 + checkbox)
元素说明
1. 课程信息
每个笔记文件开头必须包含课程信息块:
## 课程信息
- 课程:高等数学
- 教师:张教授
- 日期:2025-03-15
- 记录人:张三
2. 知识点
知识点是笔记的核心单元,每个知识点必须包含「定义」和「要点」:
### 极限的定义
**定义:** 当自变量 x 无限趋近于 a 时,函数 f(x) 无限趋近于常数 L,
则称 L 为 x→a 时 f(x) 的极限,记作 lim(x→a) f(x) = L。
**要点:**
- 极限描述的是趋近行为,不要求 f(a) 有定义
- 左极限和右极限都存在且相等时,极限才存在
- 极限值唯一
**例题:**
> 求 lim(x→2) (x² + 3x - 1)
> 解:代入 x=2,得 4 + 6 - 1 = 9
> 所以 lim(x→2) (x² + 3x - 1) = 9
3. 课堂重点
课程结束时,汇总本节课的重点内容:
## 课堂重点
- ⭐ 极限的 ε-δ 定义是分析基础
- ⭐ 左右极限相等是极限存在的充要条件
- ⭐ 夹逼定理用于求复杂极限
4. 疑问与待办
记录课堂上的疑问和课后待办事项:
## 疑问与待办
- [ ] ❓ ε-δ 定义中 ε 和 δ 的关系如何直观理解?
- [ ] 复习夹逼定理的证明过程
- [ ] 完成课后习题 1-5
格式规范
标题层级
| 层级 | 用途 | 示例 |
|---|---|---|
# |
章节标题 | # 第01章 函数与极限 |
## |
主要部分 | ## 知识点 / ## 课堂重点 |
### |
知识点标题 | ### 极限的定义 |
#### |
子知识点 | #### 左极限与右极限 |
标记符号
| 符号 | 含义 | 使用场景 |
|---|---|---|
| ⭐ | 重点内容 | 课堂重点汇总、重要公式/定理 |
| ❓ | 疑问 | 不理解的内容、需要查阅的疑问 |
| ✅ | 已解决 | 疑问已解决时勾选 |
| 📌 | 注意事项 | 易错点、特殊情况 |
| 💡 | 技巧/方法 | 解题技巧、记忆方法 |
公式书写
简单公式直接用行内/行间 Markdown:
行内公式:lim(x→a) f(x) = L
行间公式:
> ∫(a→b) f(x)dx = F(b) - F(a)
复杂公式可使用 LaTeX 语法(如果渲染环境支持):
$$\lim_{x \to a} f(x) = L \iff \forall \varepsilon > 0, \exists \delta > 0, \text{s.t.} |f(x) - L| < \varepsilon$$
列表格式
使用 - 作为无序列表标记,有序列表使用 1.:
**要点:**
- 第一点
- 第二点
- 子要点
- 子要点
引用格式
例题和重要内容使用引用块:
**例题:**
> 题目:求 lim(x→0) sin(x)/x
> 解答:根据夹逼定理,...
> 结论:lim(x→0) sin(x)/x = 1
命名规范
文件命名
- 笔记文件统一命名为
notes.md - 合并笔记命名为
merged_notes.md - 章节目录命名:
第01章-函数与极限(两位数编号 + 章节名)
目录命名
- 学期目录:
2025-春季/2025-秋季 - 课程目录:使用课程全称,如
高等数学/C语言程序设计 - 章节目录:
第XX章-章节名称
协作标记
多人协作时,在笔记中添加贡献者标记:
### 知识点名称
**定义:** ... [贡献者: 张三]
**要点:**
- 要点1 [贡献者: 张三]
- 要点2 [贡献者: 李四]
合并笔记中,仅一位同学记录的内容会自动标注 [仅 XX 记录]。
附录 E:多人协作合并指南
本文档详细说明多人课堂笔记合并的工作流程、策略和最佳实践。
合并流程概览
各自记录 → 收集笔记 → 运行合并脚本 → 审阅冲突 → 定稿
详细流程
1. 各自记录
每位同学在各自的 notes.md 中独立记录课堂笔记:
- 使用标准笔记结构(参见「附录 D:笔记结构规范」)
- 保持标题层级一致(便于自动匹配)
- 记录人填写自己的姓名
2. 收集笔记
将所有同学的笔记文件收集到同一目录:
merge_workspace/
├── 张三_notes.md
├── 李四_notes.md
└── 王五_notes.md
文件命名建议:姓名_notes.md,便于脚本自动识别贡献者。
3. 运行合并脚本
python merge_notes.py \
张三_notes.md 李四_notes.md 王五_notes.md \
--output merged/merged_notes.md \
--names "张三,李四,王五"
如果文件名已包含姓名,可省略 --names:
python merge_notes.py \
张三_notes.md 李四_notes.md 王五_notes.md \
--output merged/merged_notes.md
4. 审阅合并结果
合并后需要人工审阅以下内容:
4.1 冲突内容
当不同同学对同一知识点有不同理解时,合并脚本会并列展示。需要:
- 判断哪个版本更准确
- 查阅教材或询问老师确认
- 保留正确版本,删除错误版本
4.2 独有内容
仅一位同学记录的内容会被标注 [仅 XX 记录]。需要:
- 判断该内容是否属于本节课范围
- 确认内容准确性
- 保留有价值的补充内容
4.3 格式统一
合并后可能出现格式不一致的情况:
- 统一公式书写格式
- 统一标记符号使用
- 调整标题层级
5. 定稿
审阅修改后,将最终版本保存到 merged/merged_notes.md。
合并策略详解
标题匹配
脚本通过 (标题层级, 标题文本) 二元组匹配知识点:
### 极限的定义在不同笔记中会匹配到一起### 极限定义(缺少"的")也会通过模糊匹配合并#### 极限的定义(层级不同)不会匹配
建议: 所有同学使用统一的章节标题,避免匹配失败。
内容去重
对于相同知识点下的内容行:
1. 标准化文本(去除空格、统一标点、转小写)
2. 完全相同的行 → 保留首次出现
3. 高度相似的行(一方包含另一方且长度比 > 80%)→ 视为重复
4. 不重复的行 → 添加到合并结果,标注 [贡献者: XX]
独有内容标注
仅出现在一位同学笔记中的知识点:
- 标题后添加 [仅 XX 记录]
- 合并报告中列出所有独有知识点
最佳实践
课前准备
- 约定统一的标题格式和知识点划分
- 分工记录不同部分(如一人记定义,一人记例题)
- 使用相同的笔记模板
课后合并
- 尽快合并,趁记忆新鲜时审阅
- 轮流担任"合并者"角色
- 合并后发到群里让所有同学确认
版本管理
- 合并笔记保留合并时间和贡献者信息
- 原始个人笔记不覆盖
- 考前复习时以合并版本为准
常见问题
Q: 标题不完全一致怎么办?
A: 脚本会进行模糊匹配(忽略大小写和空格差异)。如果标题差异较大(如"极限" vs "数列极限"),会被视为不同知识点。建议课前约定统一标题。
Q: 某位同学笔记格式不规范怎么办?
A: 合并前先手动整理格式,确保标题层级正确。格式严重不规范的笔记会影响合并质量。
Q: 合并后内容太多太长怎么办?
A: 可以按章节拆分合并:
# 只合并某一章的笔记
python merge_notes.py ch01_张三.md ch01_李四.md -o merged/ch01_merged.md
Q: 如何处理录音转写的笔记?
A: 录音转写的文本通常缺乏结构,建议先用本 Skill 的「笔记整理与结构化」功能(见正文「工具支撑」第 1 节)整理为标准格式,再参与合并。
附录 F:标准模板
F.1 单节课笔记模板(notes.md)
# 第XX章 章节名称
## 课程信息
- 课程:课程名称
- 教师:教师姓名
- 日期:YYYY-MM-DD
- 记录人:你的姓名
## 知识点
### 知识点1名称
**定义:**
在此处填写知识点的定义或概念描述。
**要点:**
- 要点1
- 要点2
- 要点3
**例题:**
> 题目:在此填写题目内容
>
> 解答:
> 步骤1...
> 步骤2...
>
> 结论:最终答案
### 知识点2名称
**定义:**
...
**要点:**
- ...
## 课堂重点
- ⭐ 重点1
- ⭐ 重点2
- ⭐ 重点3
## 疑问与待办
- [ ] ❓ 疑问1
- [ ] ❓ 疑问2
- [ ] 复习要点
- [ ] 待做习题
---
> 📝 笔记整理提示:
> 1. 每个知识点包含「定义」和「要点」两个部分
> 2. 有例题的知识点加「例题」部分
> 3. 用 ⭐ 标记重点,❓ 标记疑问
> 4. 课后及时补充「疑问与待办」
> 5. 多人协作时使用 merge_notes.py 合并笔记
F.2 课程 README 模板(README.md)
# 课程名称
## 课程信息
- 课程名称:课程名称
- 任课教师:教师姓名
- 学期:YYYY-春季/秋季
- 学分:X
- 考核方式:考试/考查
- 上课时间:周X 第X-X节
- 上课地点:教学楼-教室
## 章节目录
- 第01章 章节名称1
- 第02章 章节名称2
- 第03章 章节名称3
## 笔记说明
- 每个章节目录下有一个 `notes.md` 文件,记录该章节的课堂笔记
- 多人协作合并后的笔记存放在 `merged/` 目录下
- 使用 `search_notes.py` 可搜索本课程笔记内容
## 参与人员
| 姓名 | 学号 | 负责章节 | 联系方式 |
|------|------|----------|----------|
| | | | |
## 学习资源
- 教材:
- 参考书:
- 课程网站:
- 习题集:
附录 G:脚本源码
三个脚本相互独立,仅依赖 Python 3 标准库。使用方式:从下方代码块复制内容,分别保存为 init_notebook.py、merge_notes.py、search_notes.py,再用 python xxx.py 运行(示例命令见正文「工具支撑」与「附录 E」)。
G.1 init_notebook.py —— 初始化课程笔记本
#!/usr/bin/env python3
"""
课堂笔记笔记本初始化脚本
为指定课程创建标准目录结构、README 和章节笔记模板。
"""
import argparse
import os
import sys
from datetime import datetime
from pathlib import Path
def get_default_semester():
"""根据当前月份推断学期"""
now = datetime.now()
year = now.year
month = now.month
if 2 <= month <= 7:
return f"{year}-春季"
else:
return f"{year}-秋季"
def sanitize_dirname(name: str) -> str:
"""清理目录名,移除文件系统不允许的字符"""
invalid_chars = '<>:"/\\|?*'
for ch in invalid_chars:
name = name.replace(ch, "_")
return name.strip()
def format_chapter_dir(index: int, name: str) -> str:
"""格式化章节目录名:第01章-章节名称"""
return f"第{index:02d}章-{sanitize_dirname(name)}"
def create_chapter_note(chapter_name: str, course_name: str, teacher: str) -> str:
"""生成章节笔记模板内容"""
today = datetime.now().strftime("%Y-%m-%d")
return f"""# {chapter_name}
## 课程信息
- 课程:{course_name}
- 教师:{teacher or '待填写'}
- 日期:{today}
- 记录人:待填写
## 知识点
### 知识点1
**定义:**
**要点:**
-
**例题:**
>
## 课堂重点
- ⭐
## 疑问与待办
- [ ]
---
> 笔记整理提示:
> 1. 每个知识点包含「定义」和「要点」两个部分
> 2. 有例题的知识点加「例题」部分
> 3. 用 ⭐ 标记重点,❓ 标记疑问
> 4. 课后及时补充「疑问与待办」
"""
def create_course_readme(course_name: str, teacher: str, semester: str, chapters: list) -> str:
"""生成课程 README 内容"""
chapter_list = "\n".join(
f"- 第{i:02d}章 {ch}" for i, ch in enumerate(chapters, 1)
) if chapters else "- (待添加章节)"
return f"""# {course_name}
## 课程信息
- 课程名称:{course_name}
- 任课教师:{teacher or '待填写'}
- 学期:{semester}
- 学分:待填写
- 考核方式:待填写
## 章节目录
{chapter_list}
## 笔记说明
- 每个章节目录下有一个 `notes.md` 文件,记录该章节的课堂笔记
- 多人协作合并后的笔记存放在 `merged/` 目录下
- 使用 `search_notes.py` 可搜索本课程笔记内容
## 参与人员
| 姓名 | 学号 | 负责章节 |
|------|------|----------|
| | | |
"""
def main():
parser = argparse.ArgumentParser(
description="初始化课程笔记本目录结构"
)
parser.add_argument(
"--course", "-c",
required=True,
help="课程名称(必填)"
)
parser.add_argument(
"--semester", "-s",
default=None,
help="学期,格式 YYYY-春季/秋季(默认自动推断)"
)
parser.add_argument(
"--chapters", "-ch",
default=None,
help="章节列表,逗号分隔(可选)"
)
parser.add_argument(
"--path", "-p",
default="./notes",
help="笔记库根目录(默认 ./notes)"
)
parser.add_argument(
"--teacher", "-t",
default=None,
help="任课教师(可选)"
)
args = parser.parse_args()
semester = args.semester or get_default_semester()
chapters = (
[ch.strip() for ch in args.chapters.split(",") if ch.strip()]
if args.chapters
else []
)
# 构建目录路径
base_path = Path(args.path) / sanitize_dirname(semester) / sanitize_dirname(args.course)
merged_path = base_path / "merged"
# 检查是否已存在
if base_path.exists():
print(f"⚠️ 课程目录已存在:{base_path}")
response = input("是否覆盖?(y/N): ").strip().lower()
if response != "y":
print("已取消。")
sys.exit(0)
# 创建目录
base_path.mkdir(parents=True, exist_ok=True)
merged_path.mkdir(exist_ok=True)
print(f"✅ 创建课程目录:{base_path}")
# 创建 README
readme_path = base_path / "README.md"
readme_content = create_course_readme(args.course, args.teacher, semester, chapters)
readme_path.write_text(readme_content, encoding="utf-8")
print(f"✅ 创建课程 README:{readme_path}")
# 创建章节目录和笔记模板
if chapters:
for i, chapter in enumerate(chapters, 1):
chapter_dir = base_path / format_chapter_dir(i, chapter)
chapter_dir.mkdir(exist_ok=True)
note_path = chapter_dir / "notes.md"
note_content = create_chapter_note(chapter, args.course, args.teacher)
note_path.write_text(note_content, encoding="utf-8")
print(f"✅ 创建章节笔记:{note_path}")
else:
print("ℹ️ 未指定章节,可后续手动添加章节目录")
print(f"\n📝 笔记本初始化完成!")
print(f" 路径:{base_path}")
print(f" 学期:{semester}")
print(f" 课程:{args.course}")
if chapters:
print(f" 章节:{len(chapters)} 个")
print(f"\n下一步:")
print(f" 1. 编辑 README.md 补充课程信息")
print(f" 2. 在各章节的 notes.md 中记录课堂笔记")
print(f" 3. 使用 merge_notes.py 合并多人笔记")
if __name__ == "__main__":
main()
G.2 merge_notes.py —— 多人笔记合并
#!/usr/bin/env python3
"""
多人课堂笔记合并脚本
将多位同学各自记录的同一节课/章节笔记合并为一份完整版本。
合并策略:标题匹配、内容去重、独有内容保留、冲突标注。
"""
import argparse
import re
import sys
from pathlib import Path
from collections import defaultdict
from dataclasses import dataclass, field
from typing import List, Optional
@dataclass
class NoteSection:
"""笔记章节:一个标题及其下的所有内容"""
level: int # 标题层级 (1-6)
title: str # 标题文本
content_lines: list = field(default_factory=list) # 标题下的内容行
contributors: list = field(default_factory=list) # 贡献者列表
def parse_markdown(text: str) -> List[NoteSection]:
"""解析 Markdown 文本,按标题分割为 Section 列表"""
sections = []
current_section = None
for line in text.split("\n"):
# 匹配标题行:# ~ ######
heading_match = re.match(r'^(#{1,6})\s+(.+)$', line)
if heading_match:
# 保存上一个 section
if current_section:
sections.append(current_section)
level = len(heading_match.group(1))
title = heading_match.group(2).strip()
current_section = NoteSection(level=level, title=title)
else:
if current_section is None:
# 标题之前的内容,创建一个虚拟 section
current_section = NoteSection(level=0, title="(文档头部)")
current_section.content_lines.append(line)
if current_section:
sections.append(current_section)
return sections
def normalize_text(text: str) -> str:
"""标准化文本以便比较:去除空格、标点差异"""
# 去除多余空白
text = re.sub(r'\s+', '', text)
# 去除常见标点差异(直/弯双引号、直/弯单引号)
text = text.replace('"', '').replace('\u201c', '').replace('\u201d', '')
text = text.replace("'", '').replace('\u2018', '').replace('\u2019', '')
return text.lower()
def is_duplicate(line1: str, line2: str) -> bool:
"""判断两行内容是否为重复"""
n1 = normalize_text(line1)
n2 = normalize_text(line2)
if not n1 or not n2:
return False
# 完全相同
if n1 == n2:
return True
# 一方包含另一方且长度差异小
if len(n1) > 10 and len(n2) > 10:
shorter = min(n1, n2, key=len)
longer = max(n1, n2, key=len)
if shorter in longer and len(shorter) / len(longer) > 0.8:
return True
return False
def merge_sections(sections_list: List[List[NoteSection]], names: List[str]) -> List[NoteSection]:
"""
合并多份笔记的 Section 列表。
sections_list: 每位同学的 Section 列表
names: 对应的姓名列表
"""
# 用 (level, normalized_title) 作为 key 匹配
merged_map = {} # key -> NoteSection
merged_order = [] # 保持出现顺序
for sections, name in zip(sections_list, names):
for section in sections:
key = (section.level, normalize_text(section.title))
if key not in merged_map:
# 新 section
new_section = NoteSection(
level=section.level,
title=section.title,
content_lines=list(section.content_lines),
contributors=[name]
)
merged_map[key] = new_section
merged_order.append(key)
else:
# 已有 section,合并内容
existing = merged_map[key]
if name not in existing.contributors:
existing.contributors.append(name)
# 逐行合并:去除重复
for line in section.content_lines:
is_dup = False
for existing_line in existing.content_lines:
if is_duplicate(line, existing_line):
is_dup = True
break
if not is_dup:
# 非空行才添加
if line.strip():
existing.content_lines.append(f"{line} [贡献者: {name}]")
return [merged_map[k] for k in merged_order]
def render_merged(sections: List[NoteSection], contributor_names: List[str]) -> str:
"""将合并后的 Section 列表渲染为 Markdown 文本"""
lines = []
lines.append("# 合并笔记")
lines.append("")
lines.append(f"> 本笔记由 {len(contributor_names)} 位同学合并:{', '.join(contributor_names)}")
lines.append(f"> 合并时间:{__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M')}")
lines.append("")
# 统计信息
total_sections = len([s for s in sections if s.level > 0])
lines.append(f"**合并统计:** {total_sections} 个知识点,{len(contributor_names)} 位贡献者")
lines.append("")
lines.append("---")
lines.append("")
for section in sections:
if section.level == 0:
# 文档头部内容,跳过或直接输出
for line in section.content_lines:
if line.strip():
lines.append(line)
continue
# 输出标题
prefix = "#" * section.level
contributor_tag = ""
if len(section.contributors) == 1:
contributor_tag = f" [仅 {section.contributors[0]} 记录]"
lines.append(f"{prefix} {section.title}{contributor_tag}")
lines.append("")
# 输出内容
for line in section.content_lines:
lines.append(line)
lines.append("")
# 合并报告
lines.append("---")
lines.append("")
lines.append("## 合并报告")
lines.append(f"- 参与人数:{len(contributor_names)}")
lines.append(f"- 贡献者:{', '.join(contributor_names)}")
lines.append(f"- 合并知识点数:{total_sections}")
# 统计每位贡献者的独有内容
solo_sections = [s for s in sections if s.level > 0 and len(s.contributors) == 1]
if solo_sections:
lines.append(f"- 独有知识点数:{len(solo_sections)}")
for s in solo_sections:
lines.append(f" - {s.title}({s.contributors[0]})")
lines.append("")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="合并多人课堂笔记"
)
parser.add_argument(
"files",
nargs="+",
help="多个笔记文件路径"
)
parser.add_argument(
"--output", "-o",
default="./merged_notes.md",
help="合并后输出路径(默认 ./merged_notes.md)"
)
parser.add_argument(
"--names", "-n",
default=None,
help="贡献者姓名列表,逗号分隔,与文件顺序对应(默认使用文件名)"
)
args = parser.parse_args()
# 检查文件
file_paths = []
for f in args.files:
path = Path(f)
if not path.exists():
print(f"❌ 文件不存在:{f}")
sys.exit(1)
file_paths.append(path)
# 解析姓名
if args.names:
names = [n.strip() for n in args.names.split(",")]
if len(names) != len(file_paths):
print(f"❌ 姓名数量({len(names)})与文件数量({len(file_paths)})不匹配")
sys.exit(1)
else:
names = [path.stem for path in file_paths]
# 读取并解析所有笔记
sections_list = []
for path, name in zip(file_paths, names):
text = path.read_text(encoding="utf-8")
sections = parse_markdown(text)
sections_list.append(sections)
print(f"📄 已读取:{path.name}({name})- {len(sections)} 个段落")
# 合并
print(f"\n🔄 正在合并 {len(file_paths)} 份笔记...")
merged_sections = merge_sections(sections_list, names)
# 渲染
output_text = render_merged(merged_sections, names)
# 写入
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(output_text, encoding="utf-8")
# 统计
total = len([s for s in merged_sections if s.level > 0])
solo = len([s for s in merged_sections if s.level > 0 and len(s.contributors) == 1])
print(f"\n✅ 合并完成!")
print(f" 输出文件:{output_path}")
print(f" 合并知识点:{total} 个")
print(f" 独有知识点:{solo} 个(仅一位同学记录的内容)")
print(f" 贡献者:{', '.join(names)}")
print(f"\n💡 建议审阅合并结果,特别是标注了 [仅 XX 记录] 的独有知识点,以及 [贡献者: XX] 的补充内容。")
if __name__ == "__main__":
main()
G.3 search_notes.py —— 笔记搜索检索
#!/usr/bin/env python3
"""
课堂笔记搜索脚本
在笔记库中按关键词、课程、章节搜索笔记内容。
支持模糊匹配、上下文显示、多关键词 OR 搜索。
"""
import argparse
import re
import sys
from pathlib import Path
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class SearchResult:
"""单条搜索结果"""
file_path: Path
course: str # 课程名
chapter: str # 章节名
line_number: int # 匹配行号
matched_line: str # 匹配行内容
context_before: list # 上文行
context_after: list # 下文行
def find_notes_files(root: Path) -> List[Path]:
"""递归查找笔记库中所有 .md 文件(跳过 merged 合并笔记目录)"""
files = []
for path in root.rglob("*.md"):
# 跳过 merged 目录中的合并笔记,避免结果重复
if "merged" in path.parts:
continue
files.append(path)
return sorted(files)
def extract_course_chapter(file_path: Path, root: Path) -> tuple:
"""从文件路径推断课程名和章节名"""
try:
rel_path = file_path.relative_to(root)
parts = rel_path.parts
# 结构:semester/course/chapter/notes.md
# 或:course/chapter/notes.md
course = ""
chapter = ""
if len(parts) >= 3:
course = parts[-3] if len(parts) >= 3 else ""
chapter = parts[-2] if len(parts) >= 2 else ""
elif len(parts) >= 2:
course = parts[-2] if len(parts) >= 2 else ""
chapter = parts[-1] if len(parts) >= 1 else ""
return course, chapter
except ValueError:
return "", ""
def fuzzy_match(text: str, keyword: str, exact: bool = False) -> bool:
"""检查文本是否匹配关键词"""
if exact:
return keyword in text
# 模糊匹配:忽略大小写,允许部分匹配
return keyword.lower() in text.lower()
def search_in_file(
file_path: Path,
keywords: List[str],
root: Path,
context_lines: int,
exact: bool,
course_filter: Optional[str],
chapter_filter: Optional[str]
) -> List[SearchResult]:
"""在单个文件中搜索关键词"""
course, chapter = extract_course_chapter(file_path, root)
# 课程过滤
if course_filter:
if not fuzzy_match(course, course_filter, exact=False):
return []
# 章节过滤
if chapter_filter:
if not fuzzy_match(chapter, chapter_filter, exact=False):
return []
results = []
try:
lines = file_path.read_text(encoding="utf-8").split("\n")
except Exception as e:
print(f"⚠️ 无法读取文件 {file_path}: {e}", file=sys.stderr)
return []
for i, line in enumerate(lines):
for keyword in keywords:
if fuzzy_match(line, keyword, exact):
# 提取上下文
start = max(0, i - context_lines)
end = min(len(lines), i + context_lines + 1)
context_before = lines[start:i]
context_after = lines[i + 1:end]
results.append(SearchResult(
file_path=file_path,
course=course,
chapter=chapter,
line_number=i + 1,
matched_line=line,
context_before=context_before,
context_after=context_after,
))
break # 同一行不重复报告
return results
def highlight_keyword(text: str, keywords: List[str]) -> str:
"""高亮关键词(用 <<>> 包裹)"""
for kw in keywords:
# 大小写不敏感替换
pattern = re.compile(re.escape(kw), re.IGNORECASE)
text = pattern.sub(f"<<{kw}>>", text)
return text
def format_result(result: SearchResult, keywords: List[str], show_context: bool) -> str:
"""格式化单条搜索结果"""
lines = []
rel_path = result.file_path
# 文件位置
location = f"📄 {result.course}/{result.chapter}" if result.course else f"📄 {rel_path.name}"
lines.append(f" {location} (第 {result.line_number} 行)")
# 上下文 - 之前
if show_context and result.context_before:
for line in result.context_before:
if line.strip():
lines.append(f" │ {line.rstrip()}")
# 匹配行
highlighted = highlight_keyword(result.matched_line, keywords)
lines.append(f" ▶ {highlighted.rstrip()}")
# 上下文 - 之后
if show_context and result.context_after:
for line in result.context_after:
if line.strip():
lines.append(f" │ {line.rstrip()}")
lines.append("") # 空行分隔
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="搜索课堂笔记库"
)
parser.add_argument(
"keywords",
nargs="+",
help="搜索关键词(多个关键词为 OR 关系)"
)
parser.add_argument(
"--path", "-p",
default=".",
help="笔记库根目录(默认当前目录)"
)
parser.add_argument(
"--course", "-c",
default=None,
help="限定课程名称(支持模糊匹配)"
)
parser.add_argument(
"--chapter", "-ch",
default=None,
help="限定章节名称(支持模糊匹配)"
)
parser.add_argument(
"--context", "-C",
type=int,
default=2,
help="显示匹配行上下文的行数(默认 2)"
)
parser.add_argument(
"--exact", "-e",
action="store_true",
help="精确匹配模式(关闭模糊匹配)"
)
args = parser.parse_args()
root = Path(args.path)
if not root.exists():
print(f"❌ 路径不存在:{root}")
sys.exit(1)
if not root.is_dir():
print(f"❌ 不是目录:{root}")
sys.exit(1)
# 查找所有笔记文件
md_files = find_notes_files(root)
if not md_files:
print(f"❌ 在 {root} 中未找到 Markdown 文件")
sys.exit(1)
print(f"🔍 搜索关键词:{', '.join(args.keywords)}")
if args.course:
print(f" 课程过滤:{args.course}")
if args.chapter:
print(f" 章节过滤:{args.chapter}")
print(f" 搜索范围:{len(md_files)} 个文件")
print(f" {'=' * 50}")
print()
# 搜索
all_results = []
for md_file in md_files:
results = search_in_file(
md_file,
args.keywords,
root,
args.context,
args.exact,
args.course,
args.chapter
)
all_results.extend(results)
if not all_results:
print("❌ 未找到匹配结果。")
print(f"\n💡 建议:")
print(f" - 尝试使用更短的关键词")
print(f" - 去除 --course / --chapter 过滤条件")
print(f" - 使用模糊匹配(不加 --exact)")
sys.exit(0)
# 输出结果
show_context = args.context > 0
for result in all_results:
print(format_result(result, args.keywords, show_context))
# 统计
print(f"{'=' * 50}")
print(f"✅ 找到 {len(all_results)} 条匹配结果")
# 按课程统计
course_counts = {}
for r in all_results:
course_counts[r.course] = course_counts.get(r.course, 0) + 1
if len(course_counts) > 1:
print(f" 按课程分布:")
for course, count in sorted(course_counts.items(), key=lambda x: -x[1]):
print(f" {course or '(未知)'}: {count} 条")
if __name__ == "__main__":
main()