ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PPT课件资源治理:批量转PDF、FTS5中文检索与去重

PPT课件资源治理:批量转PDF、FTS5中文检索与去重 简介这是一套面向初三学生的化学教学课件对应新人教版九年级上册全册内容可用于课堂演示、备课参考与同步复习。压缩包内共1个PPT文件整体约152.72MB属于体量较大的图文课件涵盖绪言及第一单元「走进化学世界」等核心章节。课件围绕化学研究的对象与化学发展简史两条主线展开一方面讲解物质及其变化涵盖自然界已有物质与人工合成新物质、性质与结构、实验操作如药品取用并通过「烧不坏的手帕」等视频与例题辨析合成新药、防治污染、开发新能源等化学范畴另一方面梳理从火的使用、陶器与金属工具到道尔顿原子论、阿伏加德罗分子学说、1869年门捷列夫元素周期律再到纳米技术与绿色化学的脉络配有课堂小结与随堂练习。目前已有114人学习适合初三化学入门、教师备课与中考基础复习使用。1. 一份课件的 IT 视角从「新人教版九年级上册初三化学全册课件PPT.ppt」到可检索的校内资源开学前一周教务发来一个压缩包解压后是几十个文件夹里面躺着「新人教版九年级上册初三化学全册课件PPT.ppt」这类文件整册合订本和按单元拆分的版本混在一起后缀 .ppt 与 .pptx 各占一半封面写着人教版翻两页还能看到旧版残留的插图。教研组要的是「内网能搜、点开是 PDF、手机上能看缩略图」而运维手上只有一台 Linux 服务器和一个共享目录。这个标题背后的活儿是把一批教学课件当数据资产来治理读懂它的内部结构批量转 PDF 与缩略图建索引、去重、做版本差分。适合要对接教务系统、搭校园资源库或者只是被同事塞了一堆 PPT 的开发者与运维。2. 拆开 .ppt 与 .pptx用 python-pptx 读出化学课件的每一页2.1 后缀是 .ppt 的课件第一步不是解析而是转换标题里的文件后缀是.ppt而「新人教版九年级上册初三化学全册课件PPT」在流转过程中经常两代格式并存。.pptx是 OOXML本质是一个 ZIP 包解压后能看到ppt/slides/slide1.xml、ppt/media/这样的目录.ppt是 OLE 复合文档正文藏在名为PowerPoint Document的流里用私有记录格式存放没有稳定可用的开源解析库。所以见到.ppt不要硬啃。常见的做法是先用 LibreOffice 洗一遍格式保留原文件把归一化结果写到另一个目录后续所有处理只认.pptx。# 先摸清家底老格式和新格式各有多少 find ./courseware -type f -iname *.ppt | wc -l find ./courseware -type f -iname *.pptx | wc -l # 把 .ppt 统一洗成 .pptx原文件不动输出到 normalized/ soffice --headless --norestore \ -env:UserInstallationfile:///tmp/lo_wash \ --convert-to pptx --outdir ./normalized ./courseware/unit03/*.ppt--headless关闭图形界面服务器上没有 X11 也能跑--norestore跳过上次会话恢复避免批量执行时弹状态-env:UserInstallation给这次转换指定一个独立的配置目录这是并发时最关键的一个参数后面 3.3 节会详细说--convert-to pptx指定目标格式--outdir落到独立目录方便和源文件做对照。如果只需要文本、不关心版式catdoc包里的catppt也能从.ppt里抠出文字但它拿不到图片、表格和备注页的位置关系做课件检索时信息量不够。2.2 一页幻灯片里到底有什么shapes 遍历的正确姿势拿到.pptx之后python-pptx是最省事的入口。它把一页幻灯片抽象成slide.shapes文本框、图片、表格、组合图形都挂在这个集合下。from pptx import Presentation from pptx.enum.shapes import MSO_SHAPE_TYPE def iter_shapes(shapes): 组合图形内部的文本不会被顶层 shapes 直接遍历到需要递归 for sh in shapes: yield sh if sh.shape_type MSO_SHAPE_TYPE.GROUP: yield from iter_shapes(sh.shapes) def dump_slide(path: str): prs Presentation(path) for no, slide in enumerate(prs.slides, start1): title slide.shapes.title.text.strip() if slide.shapes.title else texts, pics, tables [], 0, 0 for shape in iter_shapes(slide.shapes): if shape.has_text_frame and shape.text_frame.text.strip(): texts.append(shape.text_frame.text.strip()) elif shape.shape_type MSO_SHAPE_TYPE.PICTURE: pics 1 elif shape.has_table: tables 1 # 备注页里常常放着讲课稿做全文检索时别漏 note slide.notes_slide.notes_text_frame.text.strip() if slide.has_notes_slide else yield no, title, .join(texts), pics, tables, note if __name__ __main__: for no, title, body, pics, tables, note in dump_slide(化学-rjb-9a-U03-分子和原子-课件.pptx): print(no, title[:20], 图片:, pics, 表格:, tables, 备注字数:, len(note))几个容易踩的点。shape.has_text_frame为真的形状里既有标题占位符也有普通文本框判断slide.shapes.title只能拿到用了标准版式的那一类很多老师手工拖的文本框标题它是认不出来的所以title和body要一起存。shape_type用MSO_SHAPE_TYPE.PICTURE枚举而不是硬写数字库版本升级后不会突然失效。组合图形的递归遍历是必须的化学课件里实验装置图经常是几十个形状打组不递归就会漏掉标注文字。2.3 把课件文本落成结构化字段遍历只是手段落库要的是字段。一个能支撑后续检索和过滤的最小结构如下字段来源用途unit目录名或封面文本正则提取按单元过滤lesson首屏与页眉文本按课题过滤slide_no枚举序号检索结果定位跳转body所有文本框拼接全文索引主体pic_count图片形状计数识别实验图为主的课件note备注页文本讲稿检索教师端最常用fingerprint全册文本规范化后取哈希去重见 4.2 节单元和课题这两个字段靠人工填不现实从封面正则抽更省事import re UNIT re.compile(r第\s*([一二三四五六七八九十\d])\s*单元) LESSON re.compile(r课题\s*([一二三四五六七八九十\d])\s*([^\s。,]{0,12})) def parse_meta(first_page_text: str): u UNIT.search(first_page_text) l LESSON.search(first_page_text) return ( fU{int(u.group(1)):02d} if u and u.group(1).isdigit() else (u.group(1) if u else ), l.group(2).strip() if l else , )unit统一补零成U03这种两位形式是为了让目录排序不出现「U10 排在 U2 前面」的笑话中文数字编号的单元保留原样落到 4.1 节的命名规范里再做一次映射。这两个正则覆盖不了所有封面抽不到就留空让后续检索退化成纯全文匹配不要为了补全字段去写越来越复杂的正则。3. 批量把化学课件转成 PDF 与缩略图soffice 无头模式参数与并发控制3.1 为什么服务器上不选 COM 自动化Windows 环境里常见做法是 PowerShell 调PowerPoint.Application这个 COM 接口批量SaveAs。放到 Linux 服务器或容器里这条路直接断掉而且 COM 方案本身也不适合批量进程不回收、遇到损坏文件弹窗卡死、并发调用互相抢占同一个应用实例。LibreOffice 的--headless是更可控的路径代价是字体渲染与 PowerPoint 有细微差异化学方程式里的上下标、特殊符号偶尔需要人工抽查。3.2 单文件转换命令与参数表soffice --headless --norestore \ -env:UserInstallationfile:///tmp/lo_pdf \ --convert-to pdf:impress_pdf_Export \ --outdir ./pdf ./normalized/化学-rjb-9a-U03-分子和原子-课件.pptx参数作用建议--headless不启动图形界面必带否则容器内报错--norestore跳过会话恢复必带避免残留锁文件阻塞-env:UserInstallation指定独立配置目录并发时必带单进程可省--convert-to pdf目标格式纯 PDF 输出最稳pdf:impress_pdf_Export指定 Impress 导出过滤器需要控制导出细节时使用--outdir输出目录与源目录分开便于增量判断需要把备注页也就是讲稿一起导出成讲义时较新的 LibreOffice 支持在过滤器名后追加 JSON 参数指定导出选项写法是把过滤器名写成pdf:impress_pdf_Export:{ExportNotesPages:{type:boolean,value:true}}。这类扩展参数在不同发行版打包的版本上支持程度不一上线前先拿一个文件试确认输出 PDF 里真的带了讲义页再放进批处理脚本。3.3 并发与超时xargs -P 与独立 profileLibreOffice 同一时刻只允许一个实例使用同一个用户配置目录第二个进程启动时发现目录被占会静默退出表现就是「脚本跑完了输出目录是空的日志里什么都没有」。解决办法是每个并发进程给一个独立的UserInstallation目录同时用timeout兜住个别文件导致的挂死。#!/usr/bin/env bash set -uo pipefail SRC./normalized OUT./pdf JOBS4 mkdir -p $OUT find $SRC -name *.pptx -print0 \ | xargs -0 -P $JOBS -I{} bash -c f$1 base$(basename $f .pptx) prof/tmp/lo_${base}_$$ if timeout 180 soffice --headless --norestore \ -env:UserInstallationfile://${prof} \ --convert-to pdf --outdir $OUT $f /dev/null 21; then echo OK $base else echo FAIL $base | tee -a /tmp/convert_fail.log fi rm -rf $prof _ {}说明几点。xargs -P控制并发度JOBS不要超过 CPU 核数的一半LibreOffice 启动本身开销大并发过高反而更慢timeout 180是给单个文件的硬上限化学课件里插入大量高清实验图时转换会明显变慢这个值按实际文件体积调整配置目录用文件名加进程号命名跑完立即删除避免/tmp里堆积出几万个空目录失败清单单独记一份批量转换一定会有几个文件失败把清单留住比反复全量重跑划算。3.4 缩略图与页数校验pdftoppm 与 pdfinfo转完 PDF 之后教师端真正高频用的是缩略图列表首页缩略图就够用。# 取第 1 页生成 PNG-r 48 对应大约 400px 宽目录页足够清晰 pdftoppm -png -r 48 -f 1 -l 1 ./pdf/化学-rjb-9a-U03-分子和原子-课件.pdf \ ./thumb/化学-rjb-9a-U03-分子和原子-课件 # 页数校验PPT 页数与 PDF 页数不一致通常意味着转换丢页或排版溢出 echo PPT: $(python3 -c from pptx import Presentation;import sys;print(len(Presentation(sys.argv[1]).slides)) \ ./normalized/化学-rjb-9a-U03-分子和原子-课件.pptx) pdfinfo ./pdf/化学-rjb-9a-U03-分子和原子-课件.pdf | awk /^Pages/{print PDF:, $2}-r是渲染分辨率改成 72 就是标准 960px 宽缩略图列表用 48 更省空间-f和-l限定页码范围缩略图只要第一页就别全量渲染一个 60 页的课件全量出图能慢十倍。页数对齐这一条看起来笨但它是批量流程里性价比最高的一道校验文字溢出、字体缺失导致的换行错位都会在页数上露出马脚比人眼翻页靠谱得多。4. 让全册课件可被搜到命名规范、内容指纹去重与 SQLite FTS5 索引4.1 先定目录与命名再谈建库同一份「新人教版九年级上册初三化学全册课件」在不同老师手上文件名可能是「第一单元 走进化学世界.ppt」「9上化学1单元.ppt」「新人教版九年级上册初三化学全册课件PPT.ppt」。文件名不统一任何检索方案都是白搭所以第一步是定一套能排序、能切分、能反查的命名规则。层级取值示例说明学科化学固定中文前缀版本rjb人教版年级册次9a九年级上册单元U03两位补零课题K2单元内序号类型课件/教案/习题同一课题多份资源时区分落地后的目录长这样路径本身就是一层索引courseware/chemistry/rjb/9a/U03/ 化学-rjb-9a-U03-K2-分子和原子-课件.pptx 化学-rjb-9a-U03-K2-分子和原子-教案.docx thumb/化学-rjb-9a-U03-K2-分子和原子-课件.png pdf/化学-rjb-9a-U03-K2-分子和原子-课件.pdf命名规则一旦定下来unit、lesson、type都能从路径切出来不用再依赖 2.3 节的正则兜底。旧文件的批量改名可以写一次性脚本用git mv保留历史改名和建索引分两步做中间留一次人工抽查。4.2 文件哈希只能查重查不出「改过一页」去重这件事直接算sha256只能抓到字节完全相同的副本。课件最常见的场景是教研组统一下发一版某位老师在某一页补了两道练习重新保存。文件哈希变了内容重合度却有 95%。方式计算对象能抓到的重复局限文件 sha256整个文件字节完全相同的副本换行、元数据变化即失效内容指纹全册文本规范化后拼接换皮、换模板的同一份内容纯图片课件指纹为空单页指纹每页文本规范化页级复用与局部改动需逐页比对存储量上升内容指纹的实现比想象中简单关键在于规范化去掉所有空白、全角半角统一让格式差异不影响结果。import hashlib, re, unicodedata from pptx import Presentation def norm(s: str) - str: s unicodedata.normalize(NFKC, s) # 全角转半角 return re.sub(r\s, , s) def fingerprint(path: str) - tuple[str, list[str]]: prs Presentation(path) pages [] for slide in prs.slides: buf [norm(sh.text_frame.text) for sh in slide.shapes if sh.has_text_frame and sh.text_frame.text.strip()] pages.append(.join(buf)) whole hashlib.md5(\n.join(pages).encode(utf-8)).hexdigest() per_page [hashlib.md5(p.encode(utf-8)).hexdigest() for p in pages] return whole, per_page if __name__ __main__: whole, pages fingerprint(化学-rjb-9a-U03-K2-分子和原子-课件.pptx) print(全册指纹:, whole, 页数:, len(pages))norm里的NFKC归一化能把全角括号、全角数字统一到半角教学课件里这两种混用非常普遍pages列表保存单页指纹用来支持「这份 PPT 的第 12 页和那份的第 3 页是同一页」这类跨文件比对。纯图片课件扫描图、截图拼接文本为空指纹会退化成固定值遇到这种就在入库时打上image_only标记跳过指纹去重改走图片感知哈希别让它们互相误判为重复。4.3 SQLite FTS5 中文检索trigram 分词与实际查询课件检索的量级通常不大一个年级的课件拆到页也就几千条记录SQLite 加 FTS5 足够。中文分词是这里的坎默认的unicode61分词器按空白和标点切词中文整段会被当成一个超长 token搜「质量守恒」永远搜不到。-- 页级明细表 全文索引表明细与索引分离便于重建 CREATE TABLE IF NOT EXISTS slide ( id INTEGER PRIMARY KEY, doc_id TEXT NOT NULL, file_path TEXT NOT NULL, unit TEXT, lesson TEXT, slide_no INTEGER, page_fp TEXT, -- 单页内容指纹见 4.2 节 text TEXT ); CREATE VIRTUAL TABLE IF NOT EXISTS slide_fts USING fts5( text, file_path UNINDEXED, unit UNINDEXED, lesson UNINDEXED, slide_no UNINDEXED, tokenize trigram ); -- 入库后重建索引 INSERT INTO slide_fts (text, file_path, unit, lesson, slide_no) SELECT text, file_path, unit, lesson, slide_no FROM slide; -- 检索按相关度排序snippet 高亮命中片段 SELECT file_path, slide_no, snippet(slide_fts, 0, [, ], …, 12) AS hit FROM slide_fts WHERE slide_fts MATCH 质量守恒 ORDER BY rank LIMIT 20;tokenize trigram把文本按三字滑窗切分中文子串匹配立刻可用代价是索引体积大约是原文的两到三倍几千页课件完全能接受如果部署环境里的 SQLite 版本不支持 trigram退路是把每页文本自己按二元组切好再用unicode61效果差一些但能用。snippet的五个参数分别是列号、高亮前后缀、省略号和片段长度ORDER BY rank用的是 FTS5 内置的 BM25 相关度排序不要换成按slide_no排那样命中片段会沉到列表底部。4.4 用 Flask 包一个内网搜索接口索引建好后给教研组一个能直接用的入口二十行代码就够绑定内网地址而不是0.0.0.0对外。from flask import Flask, request, jsonify import sqlite3 app Flask(__name__) def conn(): con sqlite3.connect(courseware.db) con.row_factory sqlite3.Row return con app.get(/search) def search(): q request.args.get(q, ).strip() # 关键词过短会让 trigram 索引退化成全表扫描直接拒绝 if len(q) 2: return jsonify(code400, msg关键词至少 2 个字) rows conn().execute( SELECT file_path, unit, lesson, slide_no, snippet(slide_fts, 0, b, /b, …, 12) AS hit FROM slide_fts WHERE slide_fts MATCH ? ORDER BY rank LIMIT 20, (q,), ).fetchall() return jsonify(code0, data[dict(r) for r in rows]) if __name__ __main__: app.run(host10.0.0.20, port8000)MATCH后面的参数不要用 f-string 拼FTS5 的查询语法里、*、NEAR都有特殊含义用户输入的引号会让语句直接报错交给占位符处理更安全。返回结果里的file_path是相对路径前端拼上 PDF 静态目录就能直接跳转缩略图路径按 3.4 节的规则推导不需要在数据库里再存一份。搜索结果里出现「同一页在多个文件里命中」是正常的展示时按page_fp折叠把重复文件收敛成一行「另有 3 份相同页」。5. 课件改版之后两版 PPT 的文本差分与上线自检资源库上线半年后最真实的诉求不是「搜得到」而是「这次和上次比改了什么」。教研组发来一版新的九年级上册课件你不可能靠翻页比对。用 2.2 节抽取的页级文本配difflib就能生成一份人话版变更说明。import difflib from pptx import Presentation def slides_text(path: str) - dict[int, str]: out {} for i, s in enumerate(Presentation(path).slides, 1): buf [sh.text_frame.text.strip() for sh in s.shapes if sh.has_text_frame and sh.text_frame.text.strip()] out[i] \n.join(buf) return out old slides_text(化学-rjb-9a-U03-分子和原子-课件-v1.pptx) new slides_text(化学-rjb-9a-U03-分子和原子-课件-v2.pptx) for no, txt in new.items(): base old.get(no, ) diff list(difflib.unified_diff(base.splitlines(), txt.splitlines(), lineterm, n0)) if diff: print(f--- 第 {no} 页 ---) print(\n.join(diff[2:])) # 丢掉 ---/ 头两行只留内容差异按页号对齐是最省事的做法前提是改版没有插入删除整页。真遇到页数变化先比len(old)和len(new)页数一致才逐页 diff不一致就退化成整册文本 diff 并明确提示「页数由 42 变为 45请人工确认插入位置」。差异结果可以直接写进变更单教研组一眼能看出哪一页的板书换了说法。上线前的自检项不要贪多抓住几个能自动化、且出问题代价最大的检查项判据失败后的典型原因页数对齐PPT 页数等于 PDF 页数字体缺失导致排版溢出、转换丢页图片非空pic_count与源文件一致图片未嵌入仅链接到本地路径缩略图存在每个 PDF 对应一张 PNGpdftoppm未执行或被超时中断宏残留归一化后无.pptm与vbaProject.bin源文件带宏需转成pptx剥掉索引条数slide表行数等于各文件页数之和入库中断索引与明细不一致最后一条靠一句 SQL 就能验SELECT (SELECT COUNT(*) FROM slide) - (SELECT SUM(pages) FROM doc)结果不为 0 就说明有文件入库失败。把这几项挂到提交后的自动检查里每次课件更新跑一遍比人工翻页快得多也更容易在群里把「到底改了什么、有没有漏页」说清楚。本文还有配套的精品资源点击获取
返回列表