
简介这份《经济社会学概论》PPT 课件面向社会学、经济学及管理类专业的本科生与考研读者也可供讲授相关课程的教师作为课堂讲义参考用于梳理经济社会学的学科定位、理论脉络与核心议题。内容从学习目的与参考书目切入依次展开经济学的定义、社会学的定义、经济社会学的理论架构、概念工具与前工业社会至工业社会的结构比较并延伸至社会交换、库拉交换、消费行为与消费经济学等专题对斯梅尔瑟、富永健一、韦伯、马克思等学者的理论线索有较集中的整理。资源包共 1 个文件为单份 pptx 演示文稿体积约 257KB轻量便于直接查阅、打印或二次编辑。目前已有 76 人学习下载。读者可借此快速搭建经济社会学的知识框架理解经济行动如何嵌入社会结构也可将其作为课程复习与论文选题的提纲参考。1. 一个「经济社会学概论.pptx」要进检索库先得承认它不是一个文本文件培训群里丢来一个[精选]经济社会学概论.pptx要求下周前能在内部知识库里被搜到还要能回答「哪一页讲过嵌入性」这种定位问题。多数人的第一反应是另存为 PDF 再上 OCR或者直接正则去啃slide1.xml两条路都会在表格、组合形状和备注页上翻车。PPTX 的外壳是一包 OOXML 的 zip正文、项目符号层级、讲课备注分别躺在不同部件里放映顺序和形状顺序也常常不是一回事。这篇文章面向真要把课件入库的人做教育信息化的、做企业培训语料的、做 RAG 知识库的工程师。下面从解析一份经济社会学概论课件开始把抽取、批量转换、排错、入库验证整条链路走一遍代码可以直接抄。2. 用 python-pptx 抽出「经济社会学概论.pptx」的标题、正文与备注2.1 装好 python-pptx 后先跑通最小遍历骨架课件的结构比想象中脏老师手拖的文本框、从网页粘来的无段落格式文本、整页截图当内容、关键结论写在备注页里。先别急着定 Schema用最小代码把一页里所有能吐出文字的 shape 打出来看看真实数据长什么样。# pip install python-pptx from pptx import Presentation prs Presentation(经济社会学概论.pptx) print(页面尺寸(EMU):, prs.slide_width, prs.slide_height) print(总页数:, len(prs.slides)) for page_no, slide in enumerate(prs.slides, start1): for shape in slide.shapes: # 图片、表格、图表对象没有 text_frame先跳过 if not shape.has_text_frame: continue for para in shape.text_frame.paragraphs: text .join(run.text for run in para.runs).strip() if text: # level 是项目符号的缩进层级0 为顶层 print(page_no, |, shape.shape_id, |, para.level, |, text)这段代码里有三个点值得解释。prs.slides按放映顺序迭代序号可以直接当作「第 N 页」用。shape.has_text_frame是过滤器表格是 GraphicFramehas_text_frame为假漏掉这个判断后面必然报 AttributeError。para.level是 0 到 4 的整数对应 PPT 里项目符号的缩进层级它是后面还原 Markdown 层级唯一的可靠依据别指望靠缩进空格数或字号去猜。跑完之后你会看到一份 60 页的经济社会学概论课件往往有 1/3 的文字挂在非占位符的文本框上。2.2 用占位符类型判断标题层级别按字号猜标题识别是整条链路的分水岭。标题抽错了后面分块、检索、引用页码全是错的。PPT 的占位符本身就带了语义类型优先信它不要信字号。PP_PLACEHOLDER 枚举在课件里通常是什么抽取动作TITLE每页大标题如「第一章 经济与社会」作为页标题拼进分块头部CENTER_TITLE封面页居中标题作为文档级标题只取第 1 页SUBTITLE封面副标题、授课人、日期写进元数据不进正文BODY / OBJECT正文要点、项目符号列表按para.level生成缩进列表SLIDE_NUMBER / DATE / FOOTER页码、日期、页脚直接丢弃否则每页都混入噪音判断方法是先看shape.is_placeholder为真再读shape.placeholder_format.type。麻烦在于大量课件标题是手动文本框——复制粘贴过来的页面尤其如此。这时退化成几何加字号的启发式规则def guess_title(slide): 占位符识别失败时的退化策略靠上 够宽 字号大 best, best_score None, -1.0 for shape in slide.shapes: if not shape.has_text_frame or not shape.text_frame.text.strip(): continue top shape.top or 0 width shape.width or 0 size max((r.font.size.pt for p in shape.text_frame.paragraphs for r in p.runs if r.font.size), default18.0) score size * width / (top 1) # 权重按课件风格调 if score best_score: best, best_score shape, score return best.text_frame.text.strip().splitlines()[0] if best else score的分子是字号乘宽度分母是纵坐标意思是「越大越靠上越像标题」。这套权重对中文课件普遍适用但如果你的课件在页面上方有横幅图片或校徽文本框就需要额外排除图片下方 30pt 以内的区域。经验做法是先把占位符抽一遍再对没抽到标题的页跑退化策略最后人工抽查 10 页校准。2.3 表格、备注页和组合形状要分开抽表格和备注页是两个最容易被整体漏掉的信息源。经济社会学课件里对比「市场协调」和「网络协调」的差异几乎一定用表格呈现而讲课备注里的解释往往比正文长三倍。from pptx.enum.shapes import MSO_SHAPE_TYPE def iter_shapes(shapes): 递归展开组合形状否则组合框里的文字会被整体漏掉 for shape in shapes: if shape.shape_type MSO_SHAPE_TYPE.GROUP: yield from iter_shapes(shape.shapes) else: yield shape def dump_slide(slide): for shape in iter_shapes(slide.shapes): if shape.has_table: # 表格必须走 table 接口 for row in shape.table.rows: print(T, [cell.text.strip() for cell in row.cells]) elif shape.shape_type MSO_SHAPE_TYPE.PICTURE: print(IMG, shape.image.ext, len(shape.image.blob)) # 可按需落盘 elif shape.has_text_frame: print(TXT, shape.text_frame.text[:60]) if slide.has_notes_slide: # 备注页 讲稿 print(NOTES, slide.notes_slide.notes_text_frame.text[:120])组合形状的递归是硬性要求教学课件为了做动画效果常常把整块要点打包成一个 group不递归就只剩一个空壳。图片分支里的shape.image.blob是原始字节做多模态检索时按「源文件名_页码_序号.png」落盘就够了。备注页这里有个细节直接访问slide.notes_slide会在没有备注的页上凭空创建一个空的备注部件所以先用slide.has_notes_slide判断能少生成一堆无意义节点。2.4 段落里的 run 和软换行中文课件最容易漏字的地方同一段文字在 XML 里会被切成多个 run一次加粗、一次颜色变化都会断开而段内的软换行在 python-pptx 里用垂直制表符\v表示不是\n。直接用 .join(runs)拼接中文句子中间会莫名其妙多出空格不处理\v一个要点会被压成一整行。def para_text(para): parts [] for run in para.runs: # 软换行统一成空格避免破坏后续分句 parts.append(run.text.replace(\v, )) return .join(parts).strip()关键点在.join而不是 .joinrun 之间没有空格语义相邻 run 拼起来才是原句。反过来如果做术语抽取需要保留加粗信息就逐个 run 记录run.font.bold把加粗片段当作候选术语——经济社会学课件里被加粗的通常是「嵌入性」「社会资本」「弱关系」这类核心概念用来做词表非常省事。3. 把整门「经济社会学概论」课件批量转成 Markdown 与 JSONL3.1 还原版式标题判定、项目符号缩进与段落合并规则单页抽完文本只是半成品要让人读得下去、让程序切得开得先定一张映射规则表把 PPT 的视觉结构翻译成 Markdown 结构。规则定了就别改否则同一批课件的输出格式会前后不一致后面排查成本极高。解析信号判定结果输出形式占位符类型为 TITLE / CENTER_TITLE页标题## 第 N 页 标题para.level 0且非标题一级要点- 文本para.level 1子要点缩进level * 2个空格后加-段落无结尾标点且长度小于 12 字疑似小节名升级为###表格首行全为短文本表头标准 Markdown 表格备注页文本长度大于正文讲稿页追加为 讲稿引用块「无结尾标点且长度小于 12 字」这条规则看起来土但对中文课件命中率很高老师写小节名的时候基本不会带句号而正文要点通常带句号或分号。备注页的处理要按长度判断低于正文长度的备注多半是「本节作业阅读第 3 章」这类噪音不值得入库。3.2 批量转换脚本一条命令把整门课转成 JSONL 和 Markdown把前面的碎片拼成一个可复用的命令行工具输出 JSONL 给下游做向量化输出 Markdown 给人校对两条线并行。# convert_pptx.py import argparse, json from pathlib import Path from pptx import Presentation from pptx.enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER def iter_shapes(shapes): for shape in shapes: if shape.shape_type MSO_SHAPE_TYPE.GROUP: yield from iter_shapes(shape.shapes) else: yield shape def pick_title(slide): for shape in iter_shapes(slide.shapes): if shape.has_text_frame and shape.is_placeholder: if shape.placeholder_format.type in (PP_PLACEHOLDER.TITLE, PP_PLACEHOLDER.CENTER_TITLE): return shape.text_frame.text.strip().splitlines()[0] return def slide_record(slide, page_no, keep_notesTrue, min_chars2): title, body, tables pick_title(slide), [], [] for shape in iter_shapes(slide.shapes): if shape.has_table: tables.append([[c.text.strip() for c in row.cells] for row in shape.table.rows]) elif shape.has_text_frame: for para in shape.text_frame.paragraphs: text para.text.replace(\v, ).strip() if len(text) min_chars and text ! title: body.append({level: para.level, text: text}) notes if keep_notes and slide.has_notes_slide: notes slide.notes_slide.notes_text_frame.text.strip() return {page: page_no, title: title, body: body, tables: tables, notes: notes} def main(): ap argparse.ArgumentParser() ap.add_argument(--src, requiredTrue, helppptx 文件或所在目录) ap.add_argument(--out, defaultout.jsonl, helpJSONL 输出路径) ap.add_argument(--no-notes, actionstore_true, help丢弃备注页) ap.add_argument(--min-chars, typeint, default2, help正文最小字数) args ap.parse_args() src Path(args.src) # 注意目录含方括号时不要用 glob 模式容易踩通配符坑 files [src] if src.is_file() else sorted( p for p in src.iterdir() if p.suffix.lower() .pptx) with open(args.out, w, encodingutf-8) as fo: for f in files: prs Presentation(str(f)) for i, slide in enumerate(prs.slides, 1): rec slide_record(slide, i, keep_notesnot args.no_notes, min_charsargs.min_chars) rec[source] f.name fo.write(json.dumps(rec, ensure_asciiFalse) \n) print(f{f.name}: {len(prs.slides)} 页) if __name__ __main__: main()调用方式python convert_pptx.py --src [精选]经济社会学概论.pptx --out jjshx.jsonl --no-notes --min-chars 3参数逐个说--src传单文件时只处理一个传目录时遍历一级.pptx要递归就换成rglob但建议保持一级避免把~$开头的临时文件和回收站副本也扫进来。--min-chars 3用来过滤页码残留和「谢谢」这类碎片设成 1 会把单字噪音全留下。--no-notes在讲稿涉及未公开题库或版权内容时开启默认开启备注抽取。source字段一定要写进每条记录后面做多门课的混合索引时缺了来源名再回头补就麻烦了。3.3 中文字体、图片与公式的三条取舍线字号不要当结构信号。中文课件的正文字号可以从 18pt 到 44pt 任意跳用字号阈值判断标题换一份模板就全部失效。坚持用占位符类型加几何位置字号只作为退化策略里的一个乘数。图片要落盘但别急着 OCR。先把shape.image.blob写成本地文件并记录页码扫描版或整页截图页再单独走 OCR 队列。早期就把全部图片送去 OCR会得到一堆把图表坐标轴识别成正文的垃圾反而污染语料。公式是唯一的真空白。PPT 里的公式要么是嵌入的 OMML 节点要么是图片。python-pptx 的文本接口读不到 OMML需要绕到底层 XMLfrom lxml import etree NS_M {http://schemas.microsoft.com/office/drawing/2010/main} def dump_math(slide, page_no): root etree.fromstring(slide.part.blob) # 当前页的 XML for node in root.iter(f{NS_M}m): # itertext 会把公式里的字符按顺序拼出来符号会丢 print(page_no, .join(node.itertext()).strip())命名空间前缀在不同 Office 版本里可能是a14或m写代码前先用第 4 章的 zipfile 方法看一眼真实文件。公式里的希腊字母和上下标在这个接口下会丢失工程上常见做法是在 PPT 端先手工把公式转成文本或者接受公式缺失去换整体进度别为几页公式卡住整条流水线。4. PPTX 解析常见报错与排错从 PackageNotFoundError 到错版文本框4.1 先用 zipfile 摸清 pptx 的内部结构出问题时别急着改代码先看文件本身。PPTX 就是一个 zip把关键部件列出来很多疑问当场就有答案。unzip -l [精选]经济社会学概论.pptx | head -20import zipfile with zipfile.ZipFile([精选]经济社会学概论.pptx) as z: names z.namelist() print(部件总数:, len(names)) for n in [x for x in names if x.startswith((ppt/slides/, ppt/notesSlides/))][:10]: print(n, z.getinfo(n).file_size)两点结论必须记住。第一slide1.xml、slide2.xml的字面编号不等于放映顺序判断顺序要读ppt/presentation.xml里的sldIdLst再通过ppt/_rels/presentation.xml.rels找目标部件import zipfile from lxml import etree P {http://schemas.openxmlformats.org/presentationml/2006/main} R {http://schemas.openxmlformats.org/officeDocument/2006/relationships} with zipfile.ZipFile([精选]经济社会学概论.pptx) as z: pres etree.fromstring(z.read(ppt/presentation.xml)) rels etree.fromstring(z.read(ppt/_rels/presentation.xml.rels)) rid2target {r.get(Id): r.get(Target) for r in rels} order [rid2target[i.get(f{R}id)] for i in pres.findall(f.//{P}sldIdLst/{P}sldId)] print(order[:5])第二notesSlide1.xml和slide1.xml不同号对应备注页通过 rels 挂在各自页面上靠编号猜关联一定会错位。走 python-pptx 的slide.notes_slide接口反而最稳。4.2 报错与异常输出的对照排查表现象根因处理动作PackageNotFoundError文件实为旧版 .ppt、下载不完整或被加密先用zipfile.ZipFile试开抛 BadZipFile 的记录文件名跳过每页都混入「1」和校名页码、页脚、日期占位符过滤 SLIDE_NUMBER / FOOTER / DATE 三类标题整列为空标题是手拖的文本框启用 2.2 的几何字号退化策略文字顺序乱跳slide.shapes是 z-order 不是阅读顺序按 top 分桶后按 left 排序正文只剩半句老师用了自动缩放肉眼也看不全解析结果仍完整属显示问题标注即可中文输出成问号终端或文件编码不是文件问题统一encodingutf-8Windows 下可设PYTHONUTF81表格只拿到第一列用 text_frame 遍历表格表格走shape.table.rowsglob 找不到文件文件名里的[精选]被当成字符集通配符改用Path.iterdir()加后缀判断或glob.escape排序那行代码写出来是这样# 按 20pt 分桶再按 left 排接近人眼阅读顺序 shapes sorted(iter_shapes(slide.shapes), keylambda s: (round((s.top or 0) / 12700 / 20), s.left or 0))12700是 EMU 到 point 的换算系数除以 20 做桶宽。不这么做同一行左右两个文本框因为 top 差几百 EMU 被拆到两端拼出来的句子会前后颠倒。桶宽设太小不解决问题设太大会把上下两行混在一起20pt 是个稳妥起点。4.3 大课件、加密文件和并行处理什么时候该换方案一张几十页的课件用单线程就够了实测瓶颈不在 CPU 而在首次Presentation()的整体载入。真要并行按文件切进程别按页切因为每开一个进程都要重新载入整个包from concurrent.futures import ProcessPoolExecutor from pathlib import Path from pptx import Presentation def parse_one(p): prs Presentation(str(p)) return p.name, len(prs.slides) if __name__ __main__: files sorted(p for p in Path(./course_ppt).iterdir() if p.suffix.lower() .pptx) with ProcessPoolExecutor(max_workers4) as ex: for name, n in ex.map(parse_one, files): print(name, n)max_workers给 4 是因为瓶颈在磁盘和内存带宽给到 CPU 核数反而会因缓存争抢变慢。什么情况该果断换方案单个文件超过 200MB、内嵌大量高清媒体、或者八成以上页面是整页截图。这时候用 LibreOffice 无头模式转 PDF 再按页抽文本或走版面分析整体成本更低加密文件在 python-pptx 下没有优雅解法常见做法是在 PPT 端先另存为无密码副本再入库把这一步明确写进流程比在代码里死磕省事。5. 从解析结果到课件问答分块、元数据与召回验证5.1 按页分块的参数与元数据设计课件和普通文档不一样天然以「页」为语义单元一页讲一个论点。优先按页分块页内超长再二次切分别一上来就按固定字数硬切。def to_chunks(rec, max_chars480, overlap0): head f《{rec[source]}》第{rec[page]}页 {rec[title]}.strip() lines [head] [ * b[level] b[text] for b in rec[body]] text \n.join(lines) if len(text) max_chars: return [{text: text, meta: {page: rec[page], title: rec[title]}}] chunks, start [], 0 while start len(text): chunks.append({text: text[start:start max_chars], meta: {page: rec[page], title: rec[title]}}) start max_chars - overlap return chunksmax_chars取 480对应中文大约 300 到 500 token 的区间也是多数嵌入模型性价比最高的长度。overlap默认给 0 是刻意的一页讲稿普遍在 600 字以内二次切分基本只发生一次加了重叠反而让页码边界模糊。头部字符串把来源、页码、标题拼进去是整套设计里最划算的一步——检索命中后能直接给出「第 12 页 第一节」的引用比只丢一段裸文本可信得多。5.2 用三类真实问句验证召回定位是解析问题还是检索问题验证别用「你好」这种问题准备三类各五条定义类「嵌入性是什么意思」、定位类「第几页讲过社会资本」、对比类「市场和网络两种协调方式的差异」。每条看 top5 里是否命中正确页码命中率低于六成就先回头查解析别急着调模型。最常见的失败原因是空页没有被发现import json empty [] with open(jjshx.jsonl, encodingutf-8) as f: for line in f: r json.loads(line) if r[source].endswith(经济社会学概论.pptx) and not r[body]: empty.append((r[page], r[title])) print(正文为空的页:, empty)有标题没正文的页八成是内容全塞在组合形状里回去补一遍递归标题和正文都空的页基本是整页截图得挂到 OCR 队列单独处理。还有一类隐蔽故障标题抽到了但级别判错导致分块头部写着上一节的标题定位类问题就会稳定答错页码。分块粒度先按页走一遍召回不达标再考虑按小节二次切分比一上来换嵌入模型划算得多。本文还有配套的精品资源点击获取