ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

旧版PPT课件解析:从OLE2到python-pptx结构化入库

旧版PPT课件解析:从OLE2到python-pptx结构化入库 简介《发展经济学》马工程课件第二章「发展的概念与度量」PPT面向高校经济学专业学生、考研复习者及讲授发展经济学课程的教师可用于课堂展示、知识点梳理与课后复习。课件围绕增长与发展的概念、自由与发展、增长与发展的度量、千年发展目标与可持续发展目标四节展开对经济增长与经济发展的区别、人类发展指数、“五无”增长、马克思人的全面自由发展观以及阿玛蒂亚·森构建性自由与工具性自由等核心内容作了系统整理并指出GDP度量经济绩效的若干缺陷。资源包共1个PPT文件约2.73MB开箱即用可直接用于教学演示或对照教材查漏补缺。目前已有164人学习下载适合需要快速把握第二章理论框架与关键概念的学习者。1. 从一份打不开的《发展经济学》马工程课件说起资料盘里躺着一批命名规整的老课件文件名像「《发展经济学》马工程课件 2.第二章 发展的概念与度量.ppt」这样扩展名是三位字母的旧版格式。双击能打开可一旦丢进现代文档处理管道就全出问题在线预览只给一张缩略图全文检索搜「基尼系数」「人均 GNI」零命中想把幻灯片里那张发展度量指标表变成可统计的数据集只能人工一个字一个字敲。瓶颈不在内容在容器格式——这是上世纪末定型的二进制复合文档跟今天基于 XML 的演示文稿完全不是一回事。这篇要讲的就是把这类课件批量拆开再组装的完整链路先用无头转换把老格式翻成新格式再用 python-pptx 逐页抽取正文、表格与备注接着做指标键值对的抽取和中文可检索索引最后补上转换并发、幂等重跑和文本覆盖率校验。做教学资源整理、课程知识库、文档解析管道的人可以直接抄这条链路只想要关键词的也能砍掉后半段。2. 认清 .ppt 的二进制结构为什么 python-pptx 读不了老课件2.1 PowerPoint Document 流文字到底藏在哪新格式的演示文稿是个 zip 包解开就能看到ppt/slides/slide1.xml文字就是 XML 文本节点随便一个解析器都能取。旧格式不是。它是一个 OLE2 复合文档本质是个小型文件系统里面挂着若干条流PowerPoint Document存幻灯片记录Current User存最后编辑位置Pictures存图片SummaryInformation存标题作者之类的属性。文字被切碎塞进 MSO 记录里记录头固定 8 字节分别是 recVer/recInstance、recType、recLen。文本主要落在 TextCharsAtom 和 TextBytesAtom 两种记录里前者是 UTF-16LE后者是单字节。先用 olefile 看一眼容器结构确认这份课件没被加密、流也没损坏import olefile ole olefile.OleFileIO(2.第二章 发展的概念与度量.ppt) for entry in ole.listdir(): # 每个元素是路径片段组成的列表 print(/.join(entry)) print(ole.get_size(PowerPoint Document)) # 主数据流字节数通常是最大的那条逻辑说明listdir()只列目录项不读内容速度快可以在批量任务里当预检用get_size()拿到的主数据流大小能反映课件复杂度几十 KB 的多半是纯文字稿几 MB 的通常嵌了图表和扫描图。参数上没什么可调的路径用原始文件名即可如果文件名里有空格或中文Python 层不用转义只有走 shell 时才需要引号。真要硬啃记录流靠遍历 recType 能捞出大部分文本但段落边界、缩进层级、表格的行列关系都会丢。Current User里的偏移还可能指向损坏区域跳过之后会出现整页空白。所以自研解析只适合「只要关键词、丢了版式也无所谓」的场景。2.2 三条解析路线怎么选路线能拿到什么结构保真度依赖适用场景自研 MSO 记录解析文本原子里的纯文字低表格与层级丢失olefile只做关键词命中不建库无头转换后再解析pptx再走 python-pptx中高文本框与表格基本保留本机可执行文件批量资料库、知识库入库商业或云端 SDK版式、图表数据、批注高授权与网络调用有预算、需要图表数值判断标准很简单一页里只有标题加正文三条路线差别不大一旦出现跨页表格、SmartArt、艺术字保真度就拉开档次。多数团队的做法是走第二条成本最低、可控性最好把不可控的部分单独标出来人工补。2.3 无头转换的最小命令与并发参数# 单文件转换输出到 out 目录 soffice --headless --convert-to pptx --outdir ./out ./2.第二章 发展的概念与度量.ppt # 批量并发每个进程用独立 profile 目录避免抢同一份用户配置 i0 find ./src -name *.ppt -print0 | while IFS read -r -d f; do i$((i 1)) timeout 120 soffice --headless \ -env:UserInstallationfile:///tmp/lo_$i \ --convert-to pptx --outdir ./out $f [ $((i % 4)) -eq 0 ] wait # 每 4 个一批控住内存与句柄 done wait逻辑说明--headless不弹界面适合放进后台任务--convert-to pptx指定目标格式写全成pptx:Impress MS PowerPoint 2007 XML更稳避免同名过滤器歧义--outdir目录必须提前存在不存在时它不报错也不产出这是最常见的「跑完了但没文件」原因。参数说明-env:UserInstallation是最容易被忽略的坑不隔离时多个进程抢同一份用户配置锁表现为随机某个文件转换失败而退出码仍然是 0脚本看起来全部成功。timeout 120用来兜底遇到含大量位图的课件会卡住。批大小取 4 到 8 之间按机器核数调超过之后收益不明显反而容易触发内存峰值。转换完建议逐个校验文件存在、体积大于 0、页数大于 0三者缺一就重跑。3. 用 python-pptx 抽取幻灯片文本、表格与备注3.1 递归遍历形状组合与占位符都要吃到from pptx import Presentation from pptx.enum.shapes import MSO_SHAPE_TYPE def walk_shapes(shapes, depth0): 递归展开组合形状产出 (形状, 层级) for shp in shapes: yield shp, depth if shp.shape_type MSO_SHAPE_TYPE.GROUP: # 组合内部还有子形状 yield from walk_shapes(shp.shapes, depth 1) def slide_text(slide): parts [] for shp, _ in walk_shapes(slide.shapes): if not shp.has_text_frame: continue for para in shp.text_frame.paragraphs: line .join(run.text for run in para.runs).strip() if line: parts.append(line) return \n.join(parts)逻辑说明不递归就会漏掉分组框里的正文课件里把几行要点打包成一个组合再整体拖位置的做法很常见漏掉之后整页只剩标题。逐 run 取文本而不是直接读text_frame.text是为了保留软回车和分段信息后者会把一个文本框里的所有内容用换行拼在一起看着没问题但后续做指标抽取时容易跨句误匹配。参数说明shp.has_text_frame对表格、图片、图表一律为 False不用额外判断类型。产出的行顺序按形状层级深度优先不等于人眼阅读顺序需要阅读顺序时按shp.top、shp.left排序单位是 EMU换算关系是 1 厘米等于 360000 EMU。层级depth可以拿来做过滤比如只取顶层形状跳过装饰性小字。3.2 表格把「发展的度量」指标表转成记录def table_records(slide, slide_no, src): rows_out [] for shp, _ in walk_shapes(slide.shapes): if not getattr(shp, has_table, False): continue for r_i, row in enumerate(shp.table.rows): cells [] for cell in row.cells: if cell.is_spanned: # 被合并覆盖的格子跳过 continue cells.append(cell.text.replace(\x0b, ).strip()) if any(cells): rows_out.append({src: src, slide: slide_no, table_row: r_i, cells: cells}) return rows_out逻辑说明合并单元格在底层是重复出现的一个横向合并的表头会被同一段文字填满整行不过滤is_spanned为 True 的格子抽出来就是三份重复数据。is_merge_origin标记的是合并起点需要还原跨行跨列结构时以它为准。参数说明表头行号不固定有的课件第一行是标题、第二行才是表头硬编码第 0 行必然出错。稳妥做法是判断第一行是否含「指标」「年份」「数值」这类词命中就当表头否则按普通数据行存把识别逻辑留到下游。\x0b是段落内的软回车不替换会让一行里的两个指标粘成不可分割的长串。3.3 备注页与页码留好溯源坐标prs Presentation(./out/2.第二章 发展的概念与度量.pptx) for idx, slide in enumerate(prs.slides, start1): notes if slide.has_notes_slide: notes slide.notes_slide.notes_text_frame.text.strip() print(idx, slide.slide_id, notes[:40])逻辑说明备注页里常放讲稿、案例答案和补充数据价值比正文还高但绝大多数抽取脚本直接漏掉因为有备注的幻灯片并不多抽样验证时不容易发现。has_notes_slide必须判断直接访问会抛异常。参数说明idx从 1 开始是物理顺序跟课件里人工写的「第二章」不是一回事别混用slide.slide_id是文件内部的稳定标识同一份文件里比序号更可靠删页重排后可能变化所以建库时两个都存用序号给人看、用 id 做关联。3.4 抽取环节的四个高频异常异常现象处理方式软回车同一段多个要点粘成一行抽取时替换为空格或换行符SmartArt 与艺术字has_text_frame为 False转成 PDF 后取文本仍无结果再上 OCR合并单元格同一段文字重复出现过滤is_spanned为 True 的格子空占位符输出大量空行去空白后为空则丢弃该形状中文字体缺失是另一种情况渲染出来丑但文本节点完好不影响抽取不用花时间修。真正麻烦的是艺术字转完之后直接变成一张图片文本彻底消失只能靠 OCR 补而 OCR 对「基尼系数」这类专业词的识别率并不稳定建议单独出一份人工校对清单。4. 把「发展的概念与度量」拆成可检索的结构化数据4.1 指标名与数值抽成键值对import re NUM r\d(?:\.\d)? PAIR re.compile( r(?Pname[\u4e00-\u9fa5A-Za-z][\u4e00-\u9fa5A-Za-z\s]{1,18}?) r\s*[:]?\s*(?Pvalue NUM r)\s*(?Punit%|‰|美元|万亿元|亿人|年)? ) def extract_pairs(text): out [] for m in PAIR.finditer(text): out.append({name: m.group(name).strip(), value: float(m.group(value)), unit: m.group(unit) or }) return out逻辑说明名称部分用{1,18}?非贪婪匹配避免把整句话吞进来[:]?让「基尼系数 0.47」和「基尼系数0.47」两种写法都能命中。这套正则一定会有误报页码、年份、脚注编号都可能被当成指标值所以下游要加三道过滤名称长度小于 2 的丢掉、数值不在合理区间内的丢掉、出现在页面最底部窄条形状里的丢掉。参数说明单位枚举必须按课件实际用词补全「美元」和「亿美元」要分开列否则「亿美元」会被截成「美元」再乘错量级。数值统一转 float 存储百分号只在展示层还原避免后续做排序时字符串比较踩坑。课件里的写法命中结果归一化处理基尼系数 0.47name基尼系数 value0.47无量纲原值入库人均 GNI 12000 美元unit美元与「亿美元」分列防止前缀截断贫困发生率7.2%unit%转成小数 0.072原串另存一列1990—2020 年误报为指标名称含「年」且值在 1900 至 2100 之间则丢弃4.2 落库三张表加一套中文可检索方案CREATE TABLE slide ( id INTEGER PRIMARY KEY, src TEXT NOT NULL, -- 课件文件名 slide_no INTEGER NOT NULL, -- 物理序号从 1 开始 slide_uid TEXT, -- 文件内部 slide_id text TEXT, -- 正文纯文本 notes TEXT, -- 备注页文本 sha256 TEXT NOT NULL -- 源文件指纹用于幂等 ); CREATE TABLE metric ( id INTEGER PRIMARY KEY, slide_id INTEGER REFERENCES slide(id), name TEXT, value REAL, unit TEXT ); CREATE VIRTUAL TABLE slide_fts USING fts5( bigram, content, tokenizeunicode61 );逻辑说明bigram列存的不是原文而是把中文按二元切分后的串比如「基尼系数」切成「基尼 尼系 系数」查询时做同样的切分再走 MATCH两字词就能命中。直接把原文塞进去会踩分词坑默认分词器把连续汉字当成一个 token搜「基尼」返回零结果人很容易误判成数据没入库。参数说明content表示只建索引不存副本原文留在slide表里节省空间tokenizeunicode61对英文和数字是够用的中文靠预切分解决。入库时sha256一并写入后面的幂等重跑直接靠它判断。4.3 查询端一句提问落到具体幻灯片def search(conn, q, limit5): grams .join(q[i:i 2] for i in range(max(len(q) - 1, 1))) sql SELECT s.src, s.slide_no, s.text FROM slide_fts f JOIN slide s ON s.id f.rowid WHERE slide_fts MATCH ? ORDER BY bm25(slide_fts) LIMIT ? return conn.execute(sql, (grams, limit)).fetchall()逻辑说明bm25()是 FTS5 内置的相关度函数返回负值越小越相关直接排序即可不需要自己算权重。返回结果里带上src和slide_no才能回答「这段话出自哪份课件的第几页」——教学资料库和通用全文搜索最大的差别就在这里能不能溯源决定了结果可不可信。参数说明limit取 5 到 10 之间太小会漏掉同一指标在不同页的表述差异太大则噪声盖过有效信息。切分函数对单字查询要特殊处理len(q) - 1可能为 0用max(..., 1)兜住否则单字查询生成空串直接命中全部记录。5. 批量流水线的幂等重跑与转换质量校验5.1 用内容指纹跳过已处理的课件课件在资料盘之间来回拷贝、同步之后修改时间会变用mtime size当判断依据会导致整批重转白烧几小时 CPU。改成内容哈希import hashlib def fingerprint(path, chunk1 20): h hashlib.sha256() with open(path, rb) as fp: while blk : fp.read(chunk): # 分块读几十 MB 也不会吃满内存 h.update(blk) return h.hexdigest()入库前先查SELECT 1 FROM slide WHERE sha256? LIMIT 1命中就跳过转换和解抽两步。分块大小取 1 MB 是经验值再大对小文件不划算再小会增加系统调用次数。转换产出的 pptx 也建议记一份哈希格式转换器升级后输出可能变出问题时要能判断是源文件变了还是转换器变了。5.2 页数比对与文本覆盖率import fitz # PyMuPDF doc fitz.open(out/2.第二章 发展的概念与度量.pdf) pdf_chars sum(len(page.get_text()) for page in doc) print(fPDF 页数 {len(doc)}抽取字符 {db_chars}覆盖率 {db_chars / pdf_chars:.2%})soffice --convert-to pdf走的是渲染路径PDF 页数与 pptx 幻灯片数通常一一对应差一两页多半是隐藏幻灯片或空白页。覆盖率是更有用的指标正常课件在 0.85 以上低于 0.6 基本可以判定有大量文本落在图片或 SmartArt 里。排查顺序是先看那几页的形状类型分布确认是图片还是组合形状再决定补 OCR 还是改抽取逻辑别一上来就调正则——正则救不回根本没有文本节点的内容。本文还有配套的精品资源点击获取
返回列表