简介:《早期经济思想概述PPT学习教案.pptx》是一套面向经济学专业学生与自学者梳理学科源流的专题课件,适合课前预习、课堂讲授与考前复习使用。资源包体轻量,仅含1个pptx文件,约172KB,下载后可直接演示或打印成讲义。全篇共43页,先交代课程简介、教学目的、15讲教学计划、考核方式与推荐教材胡寄窗《西方经济学说史》,再依时间线索依次展开:从古希腊色诺芬《经济论》、柏拉图与亚里士多德的分工与交换思想,到古罗马法理对私有财产和契约自由的承认,再到中世纪经院学派关于公平价格与放贷取息的争论;随后进入15至18世纪地理大发现、西欧商品经济变化、重商主义兴起与古典政治经济学先驱,并延伸至斯密革命、古典体系分化、边际革命、马歇尔综合、凯恩斯革命以及新制度经济学的崛起。目前已有74人学习,可帮助读者快速搭起经济学说史的时间框架,并据此对照当代经济问题展开思考。
1. 43 页 PPTX 里藏着一条完整的经济思想时间线
一份名为《早期经济思想概述PPT学习教案.pptx》的专业资料,共 43 页。前 5 页是课程简介、15 讲教学计划、考核方式与教材信息,第 6 页起进入第一讲:15~18 世纪西方商品经济的发展与早期经济思想概述。多数人翻一遍就归档,但从文本工程的角度看,这份教案真正值钱的是它的排版规律:讲次、一级要点(溯源、地理大发现、重商主义、古典政治经济学先驱)、二级人物与著作,层级编号稳定,页脚统一带"第 X 页 / 共 43 页"。它天然适合被切成"讲次—主题—实体"三层结构,既能验证一套抽取流水线,也能搭出一个可追溯到具体页码的小型知识库。下面按解析、建模、生成、排错的顺序,把这份 pptx 拆成可复用的结构化数据。
2. python-pptx 解析教案文本层:占位符、页码与全角空格
2.1 pptx 本质是一个 zip,先搞清楚 slide 与文本框的从属关系
把 .pptx 后缀改成 .zip 解压,会看到ppt/slides/slide1.xml到slide43.xml、ppt/slideLayouts/、ppt/notesSlides/以及ppt/presentation.xml。真正决定幻灯片顺序的是presentation.xml里的p:sldIdLst,而不是文件名里的数字——这一点在别人手工增删过页面之后尤其重要,slide12.xml 未必是第 12 页。每页里每个可编辑对象都是一个p:sp形状,形状下挂p:txBody,文本按a:p段落切分,段落里再套a:r文本运行块。
python-pptx 把这套命名空间和 rId 解引用全部包好了,43 页的规模完全没必要手写 lxml。关键映射关系如下。
| OOXML 位置 | python-pptx 访问方式 | 教案中对应内容 |
|---|---|---|
ppt/presentation.xml的p:sldIdLst | prs.slides的迭代顺序 | 43 页物理顺序,与页脚页码一致 |
ppt/slides/slide6.xml | prs.slides[5] | 第一讲标题页,含"一、溯源"等四个要点 |
p:sp/p:nvSpPr/p:nvPr/p:ph[@type] | shape.placeholder_format.type | 区分标题占位符与正文占位符 |
p:sp/p:txBody/a:p | shape.text_frame.paragraphs | 每个要点占一个段落 |
ppt/notesSlides/slideN.xml | slide.notes_slide | 这份教案的备注页基本为空 |
注意:
placeholder_format.type在非占位符的普通文本框上返回None,而教案里"第 1 页 / 共 43 页"和部分说明文字都是普通文本框,不能一刀切成正文。
2.2 按占位符类型抽标题,别按字号猜
教案第 6 页的"一、溯源""二、地理大发现与西欧社会经济的大变化"和下面的正文段落字号接近,有的还只是加粗,用"字号最大的是标题"这种启发式规则会误判。稳妥的做法是认占位符类型,再用视觉坐标兜底。
from pptx import Presentation def iter_slide_blocks(pptx_path): """按页产出 (页码, 标题, 正文段落列表)""" prs = Presentation(pptx_path) # 一次加载整包,43 页内存占用可忽略 for idx, slide in enumerate(prs.slides, start=1): shapes = [s for s in slide.shapes if s.has_text_frame] # 按视觉位置排序,避免 shape 索引与阅读顺序不一致 shapes.sort(key=lambda s: (s.top or 0, s.left or 0)) title, body = None, [] for shape in shapes: text = "\n".join(p.text for p in shape.text_frame.paragraphs).strip() if not text: continue ph = shape.placeholder_format is_title = ph.type is not None and "TITLE" in str(ph.type) if is_title and title is None: title = text else: body.append(text) yield idx, title, bodyshapes.sort里的s.top和s.left单位是 EMU,直接比较即可,不需要换算成厘米。ph.type is not None这一层判断很关键:普通文本框的type是None,如果不排除,str(None)会返回字符串"None",虽然不会误命中 TITLE,但会让人误以为判断生效了。真正容易踩的坑是另一类:有些页面标题压根没放进标题占位符,这时候title会是None,需要退回到"取该页 top 最小、且文本长度小于 40 字的形状"作为标题。
2.3 页脚噪声清洗与中文编号归一
抽完正文后立刻遇到三个噪声源:页脚"第 X 页 / 共 43 页"、全角空格 U+3000、以及中文编号前缀"一、""1 、"混用(教案里第 15 页的"1 、'公平价格'"中间就多了一个空格)。
import re FOOTER = re.compile(r"^第\s*\d+\s*页\s*/\s*共\s*\d+\s*页$") CN_MARK = re.compile(r"^\s*(?P<no>[一二三四五六七八九十]+)\s*、\s*(?P<name>.+)$") AR_MARK = re.compile(r"^\s*(?P<no>\d+)\s*[、..]\s*(?P<name>.+)$") def clean(lines): out = [] for ln in lines: ln = ln.replace("\u3000", " ").strip() # 全角空格先归一 if not ln or FOOTER.match(ln): continue m = CN_MARK.match(ln) or AR_MARK.match(ln) out.append({"mark": m.group("no") if m else None, "text": m.group("name").strip() if m else ln}) return out三个正则各管一段:FOOTER允许页码两侧有空格,因为不同页手写习惯不一致;CN_MARK覆盖"一、溯源"这种一级要点;AR_MARK覆盖"1 、公平价格"和"2 、放贷取息"这种经院学派争论点。把mark单独存一列,后面做讲次切分和题型生成时直接复用,比在纯文本里反复正则便宜得多。这一步做完,第 6 到第 15 页会得到 4 个一级要点标记和十余条人物、著作条目。
3. 从 15 讲清单到 SQLite:讲次切块与实体字段设计
3.1 用教学计划反推讲次边界
教案第 2 到第 4 页列了完整的 15 讲目录,这是全文唯一的"讲次词典"。先把讲次号和标题落成字典,再拿标题里的关键词回正文里定位起始页。
LECTURES = { 1: "15~18世纪西方商品经济的发展与早期经济思想概述", 2: "18世纪英国工场手工业的发展与斯密革命", 3: "李嘉图与古典经济学体系的完成", 4: "萨伊、马尔萨斯、穆勒:古典经济学体系的分化与综合", 5: "李斯特与德国历史学派", # 6~15 讲同理,只录入目录页能确认的条目 } def locate_lecture_start(slides, keyword): """返回首次命中关键词的页码,未命中返回 None""" for idx, title, body in slides: blob = (title or "") + "".join(body) if keyword[:8] in blob: # 取前 8 字做粗匹配,避开标点差异 return idx return None这里有个取舍:目录页只给了讲次名,没给页码,所以 2~15 讲在当前 43 页版本里可能找不到正文。我的处理是把找不到的slide_from留None,绝不臆造页码。理由很直接——教案是逐周更新的,第 6 页标着"第 1 页 / 共 43 页",说明作者按讲次分文件导出过,页码随时会变。留空比填错强,后面做检索时会明确排除slide_from IS NULL的记录。
3.2 实体抽取:书名号正则加人名种子词表双通道
经济思想史类文本的实体分布很有规律:著作几乎都带书名号,人物基本不带。单靠一种规则覆盖面不够,两条通道并行最省事。
import re BOOK = re.compile(r"《([^》]{1,30})》") PERSON_SEED = ["色诺芬", "柏拉图", "亚里士多德", "迪亚士", "达·伽马", "哥伦布", "麦哲伦"] CONCEPT_SEED = ["公平价格", "放贷取息", "万民法", "自然法则", "重商主义", "社会分工"] def extract_entities(text): books = BOOK.findall(text) persons = [p for p in PERSON_SEED if p in text] concepts = [c for c in CONCEPT_SEED if c in text] return {"book": books, "person": persons, "concept": concepts}在教案第 8 到第 15 页上跑一遍,能得到这样的分布。
| 实体类型 | 抽取方式 | 教案样本 | 备注 |
|---|---|---|---|
| 著作 | 《》正则 | 《经济论》《理想国》《法律论》《政治学》《伦理学》 | 命中率高,几乎无误差 |
| 人物 | 种子词表全文匹配 | 色诺芬、柏拉图、亚里士多德 | 人名不带标记,正则兜不住 |
| 概念 | 手工词表 | 公平价格、放贷取息、万民法、自然法则 | 决定后续检索质量 |
| 地理与事件 | 关键词 | 好望角、印度、美洲、大西洋 | 与"地理大发现"一节绑定 |
概念词表必须手工维护,这是这套流水线里唯一没法自动化的部分。但换个角度看,正因为要手工,词表本身就成了这份教案的知识骨架——你能一眼看出第一讲的重心在公平价格、放贷取息和重商主义,而不是散在 43 页里的碎句。
3.3 建表与切块参数
切块粒度上我不按字数硬切,而是按"一页 + 一个一级要点"。第一讲的四个要点(溯源、地理大发现、重商主义、古典政治经济学先驱)天然对应四个语义块,比 500 字滑窗切出来的结果干净得多。
CREATE TABLE lecture ( lecture_no INTEGER PRIMARY KEY, -- 1~15,来自目录页 title TEXT NOT NULL, slide_from INTEGER, -- 允许为 NULL,表示正文未收录 slide_to INTEGER ); CREATE TABLE chunk ( chunk_id INTEGER PRIMARY KEY, lecture_no INTEGER REFERENCES lecture(lecture_no), topic TEXT, -- 一级要点:溯源/地理大发现/… page INTEGER NOT NULL, -- 原始页码,供引用回填 entity TEXT, -- JSON 数组,存 book/person/concept content TEXT NOT NULL ); CREATE INDEX idx_chunk_lecture_page ON chunk(lecture_no, page); CREATE INDEX idx_chunk_topic ON chunk(topic);page字段是整套设计里我最在意的一列。检索命中的时候,用户最想看到的不是一段孤立的话,而是"这句话在第 14 页"。idx_chunk_topic走的是前缀匹配,用来支撑"重商主义相关的内容都在哪几页"这类按主题聚合的查询。切块时如果某个要点跨页(比如第 17 到 19 页连着讲地理大发现),就按页拆成多条,topic保持相同,靠page排序还原阅读顺序。
4. 用编号层级生成自测题:模板匹配与 Anki 卡片导出
4.1 从标记反推题型模板
教案的编号体系本身就是出题线索。"一、溯源"是主题,"1 、公平价格"是子问题,"色诺芬""柏拉图"后面紧跟《》就是作者与著作的对应关系。按触发条件套模板,比让模型自由发挥稳定得多。
| 模板 | 触发条件 | 生成示例 |
|---|---|---|
| 作者—著作 | 同一段落内出现人名与《》 | 色诺芬的代表著作是什么?→《经济论》 |
| 归属—流派 | 段首标记为"N、xx"且含学派名 | 经院学派争论最多的两个问题是什么?→公平价格、放贷取息 |
| 事件—后果 | 段落含"大西洋贸易""商业重心西移" | 地理大发现后欧洲商业重心如何转移?→由意大利、南德意志转向西、葡,再转向荷、英、法 |
| 数字—事实 | 段落含世纪、比例等数字 | 课程考核中平时作业占总成绩多少?→50% |
def build_author_cards(chunks): cards = [] for c in chunks: books = c["entity"].get("book", []) persons = c["entity"].get("person", []) if len(books) == 1 and len(persons) == 1: q = f"{persons[0]}的代表著作是哪一部?" cards.append((q, books[0], c["page"])) return cards这里的约束len(books) == 1 and len(persons) == 1是刻意的。第 10 页同时出现柏拉图和《理想国》《法律论》两个书名,硬套模板会生成一个答案不唯一的题。宁可漏掉几道,也不要往题库里塞有歧义的卡片——卡片一旦进口碑就崩了。
4.2 导出 Anki 可导入的 TSV
Anki 的文本导入对格式很挑,字段分隔符必须是制表符,字段内出现引号时还容易串列。
import csv, hashlib def emit_tsv(cards, path): seen = set() with open(path, "w", encoding="utf-8", newline="") as f: w = csv.writer(f, delimiter="\t", quoting=csv.QUOTE_NONE, escapechar="\\") for front, back, page in cards: key = hashlib.md5(front.encode("utf-8")).hexdigest() if key in seen: # 43 页里同一问题可能在多页重复 continue seen.add(key) w.writerow([front, back, f"经济思想史::第{page}页"])csv.QUOTE_NONE配escapechar="\\"是必须的:默认的QUOTE_MINIMAL会在字段里出现逗号或引号时自动加双引号,而 Anki 不认这层引号,会把它们当正文显示出来。标签用双冒号::分层,导入后 Anki 会自动生成"经济思想史"父牌组和"第 N 页"子标签,按讲次复习时直接按标签筛就行。
4.3 答案唯一性校验与页码回填
导完先别急着背,跑一遍冲突检测。同一问题对应多个答案,说明切块重叠把同一段算了两遍,或者实体同名不同人。
from collections import defaultdict def audit(cards): m = defaultdict(set) for front, back, page in cards: m[front].add(back) return {k: sorted(v) for k, v in m.items() if len(v) > 1}在教案第一讲的卡片集上跑,典型的冲突来源有两个:一是"公平价格"在第 14 页和第 15 页各出现一次,表述略有差异;二是切块时topic相邻的两页被合并。处理方式很土但有效——把冲突项的page一起打出来,人工比对原文,保留表述更完整的那条。这一步花十分钟,能省掉后面复习时反复怀疑自己的时间。
5. 解析故障排查与 slide_id 版本比对
跑通一遍不代表下次还能跑通。教案这类文件通常一月一改,页码一变,之前抽好的page字段全废。常见故障和处理方式整理如下。
| 现象 | 根因 | 处理 |
|---|---|---|
标题恒为None | 该页用的是普通文本框,没走标题占位符 | 退回"top 最小且文本短于 40 字"的形状 |
| 段落顺序错乱 | 按 shape 索引遍历,与视觉顺序不一致 | 按(shape.top, shape.left)排序 |
| 页码与 slide 序号错位 | 页脚是手写文本,中间增删过页面 | 以页脚为准重排,或直接丢弃页脚 |
| 抽出内容只剩前几页 | 把slideLayouts当成slides读了 | 只用prs.slides,layout 单独走prs.slide_layouts |
| 文本里混进乱码空格 | 原文使用全角空格 U+3000 | replace("\u3000", " ").strip() |
| 中文编号漏识别 | "1 、"中间夹了空格或全角顿号 | 正则里给分隔符加\s*并放行多个符号 |
真正值得单独说一句的是版本比对。教学计划里 15 讲的顺序和编号是稳定的,但 PPT 页码会随着插入案例、补充图表而整体后移。如果每次都重新抽一遍再人工核对,成本极高。稳妥做法是用slide_id做锚点。
from pptx import Presentation def slide_ids(pptx_path): """返回 [(顺序号, slide_id)],slide_id 在同一文件内稳定且唯一""" prs = Presentation(pptx_path) return [(i, s.slide_id) for i, s in enumerate(prs.slides, start=1)] def diff_versions(old_ids, new_ids): old_map = {sid: pos for pos, sid in old_ids} new_map = {sid: pos for pos, sid in new_ids} added = [sid for sid in new_map if sid not in old_map] removed = [sid for sid in old_map if sid not in new_map] moved = [(sid, old_map[sid], new_map[sid]) for sid in new_map if sid in old_map and old_map[sid] != new_map[sid]] return {"added": added, "removed": removed, "moved": moved}slide_id由 PowerPoint 在创建页面时分配,同一文件内唯一且不随位置变化。改版后先跑一次diff_versions,如果added为空、moved也只涉及末尾几页,说明只是原有页面顺序微调,SQLite 里的page字段按moved的结果批量平移即可,不必重抽全文;只有当added非空、且有新增页面插进第一讲区间时,才需要对受影响的chunk重新生成卡片。这套判断逻辑跑一次不到一秒,比每次全量重建划算得多,也让"教案第几版"这件事有了可追溯的依据。
本文还有配套的精品资源,点击获取