尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从PDF到知识索引:PyMuPDF提取化学选修四课件全链路

从PDF到知识索引:PyMuPDF提取化学选修四课件全链路 简介面向高中化学选修四人教版教材的308页全套课件系统覆盖反应热、焓变、化学键与能量变化、热化学方程式书写及计算等核心章节适合高二高三学生系统复习与化学教师备课参考。整份资源打包为1个PDF文件大小约6.71MB便于按章节阅读、打印或导入平板做笔记。目前已有125人学习使用。课件从物质、微粒、化学键和能量四个角度梳理化学反应本质详细讲解放热反应与吸热反应的判断依据并通过氢氧反应、H2I2等实例分析ΔH正负号的意义同时结合热化学方程式书写规范、化学计量数与反应热的关系、可逆反应正逆反应热数值等典型题目帮助学生理解易混淆概念、规避计算误区提升解题准确率。这份材料对备考期末和高考板块都很实用。1. 308页全套课件PDF为什么先定位坐标再谈高中化学选修四的知识提取拿到一份 308 页的人教版高中化学选修四全套课件 PDF别急着转 Word。选修四化学反应原理的页面密集分布反应速率对比表、平衡常数数据、电化学装置图和化学方程式直接抽字符串会丢掉页面坐标、字体层级和表格边界后续所有这页讲什么的加工都失去锚点。常见处理链路是先用 PyMuPDF 读出文本块包围盒再用书签重建章节区间随后抽取表格与公式最后做倒排索引与依赖图把整套课件变成可检索的知识源。这篇面向做备课工具、K12 知识库或课程资源打包的工程师按链路逐步拆解命令、参数和坑位。2. 先拆版式再谈提取化学选修四PDF文本块与表格的分流策略2.1 用 PyMuPDF 轮询页面先拿到文本块的坐标系PyMuPDF 的get_text(dict)返回页面内文本的树状结构页面包含块block块包含行行内是带字体信息的 span。每个块和 span 都有 bbox包围盒这是后续所有过滤的坐标基础。选修四课件里正文、公式和插图说明混排严重只有先把坐标、字号和文本三要素抓齐才能判断一块文字是标题、正文还是页脚。打开文档后先打印任一页的rect尺寸确认实际版式是 4:3 还是 A4这会影响后面的阈值设置也决定要不要专门处理上下留白。import fitz doc fitz.open(chem_select4_308p.pdf) # 课件PDF page doc[6] # 取第7页通常是某章首页 blocks page.get_text(dict)[blocks] for block in blocks: if block[type] ! 0: # 0文本块1图片块跳过图片 continue x0, y0, x1, y1 block[bbox] text .join(span[text] for line in block[lines] for span in line[spans]) size block[lines][0][spans][0][size] if block[lines] else 0 print(fbbox({x0:.0f},{y0:.0f},{x1:.0f},{y1:.0f}) size{size:.1f} text{text[:36]!r})这段代码把每一块的左上、右下坐标、首行字号和前 36 个字符打印出来等于给页面的版面拍一张带坐标的 X 光片。block[type]用于区分文本块和图片块课件里的大幅反应装置图会以 type1 出现跳过它们可以避免把图片元信息误当正文。字号没有直接挂在 block 上需要从第一个 span 里取取之前先判断block[lines]非空否则空块会抛索引错误。通常我会把输出重定向到文件里快速翻几页观察特征。以这份 308 页课件为例正文一般在 14 到 18 磅之间章节标题超过 22 磅页脚注释在 10 磅上下这个分层可以直接作为后续过滤的初始阈值。环境上先装好pymupdf和pdfplumber其余依赖按需引入。字号近似值版面角色处置建议22pt 及以上章标题、节标题进目录节点候选14–18pt正文、例题说明保留为正文10–12pt注释、页脚、版权行过滤或降权不同软件导出的课件字体渲染尺寸存在 1 到 2 磅浮动固定阈值并不保险。更稳的做法是把 308 页全部块的字号收集起来画直方图取波谷作为分类边界上表只是启动判断的参考值不是最终标准。中文课件里 span 的字体名在不同操作系统里可能被映射成不同名称判断标题时不要死匹配字体名字号和是否居中这两个特征比字体名稳定得多。2.2 表格与公式的抽取交给 pdfplumber 单独过一遍文本块坐标解决的是哪块文字在哪表格是另一种形态单元格里的文字在文本层里是分散的独立块直接拼接会丢掉行列关系。选修四课件里出现频率最高的是速率影响因素对照表、平衡常数参考表和原电池正负极比较表这些表都带线条边框适合让 pdfplumber 根据线条重建表格结构。全量跑 308 页很慢我一般会先用 2.1 节的坐标分析圈定含表格线的页面再对这些页单独调用提取。下面这段限定在第 9 页第二章影响反应速率因素的小节执行import pdfplumber TABLE_SETTINGS { vertical_strategy: lines, horizontal_strategy: lines, snap_tolerance: 3, join_tolerance: 3, } with pdfplumber.open(chem_select4_308p.pdf) as pdf: page pdf.pages[8] for t_idx, table in enumerate(page.extract_tables(TABLE_SETTINGS)): for row in table: clean [c.replace(\n, ).strip() if c else for c in row] print(t_idx, clean)vertical_strategy和horizontal_strategy同时设为lines表示只依据页面上检测到的直线段来确定行列边界这是带边框课件表最可靠的提取方式。snap_tolerance是对齐容差允许相隔 3 像素以内的线头对齐成同一条边线join_tolerance决定两条靠近的线段是否合并课件导出时线条轻微断裂很常见调大这两个值能减少单元格分裂。返回结果里空单元格是None统一转成空字符串再处理单元格内的换行符否则后续写数据库时会出现大量\n残留。如果遇到没有边框的表格两个策略改为textpdfplumber 会尝试用文字坐标猜测列边界效果视排版密度而定对三列表格可用遇到跨行大单元格容易错。课件里还有一种用大括号和散落文本拼出的拟表格线条策略和文本策略都拿不到干净结果我的兜底方案是不做表格化把它留在正文里用段落文本保留内容完整性。2.3 页眉页脚与重复装饰块的坐标过滤课件在翻印成 PDF 时页眉经常带上学校名称、章节名或二维码页脚多为固定版权文字这些块在 308 页里反复出现。如果不先过滤后面的关键词倒排索引会把这些装饰文本算成命中页导致化学平衡一词出现在每一页里索引彻底失去区分度。过滤的思路不是按关键词黑名单而是按跨页重复率识别因为页眉页脚的文字是复读机式的正文不可能整段重复。from collections import Counter repeated_header Counter() for page in doc: seen_this_page set() for block in page.get_text(dict)[blocks]: if block[type] ! 0: continue x0, y0, x1, y1 block[bbox] if y1 110 or y0 page.rect.height - 60: # 顶部区、底部区 text .join(span[text] for line in block[lines] for span in line[spans]).strip() if len(text) 2: seen_this_page.add(text) repeated_header.update(seen_this_page) noise_texts {t for t, n in repeated_header.items() if n len(doc) * 0.8} print(过滤候选:, noise_texts)这里用Counter统计文本出现的页面数而不是累计次数因为同一页可能同时出现两行页眉。y1 110与y0 page.rect.height - 60分别框定顶部和底部区域数值按 A4 版式估算如果前面打印 rect 时发现页面高度差异大这两个阈值要跟着等比调整。当某个文本在超过 80% 的页面都出现时它基本可以认定是页眉、页脚或水印直接放入排除名单。对于倾斜水印dict结构仍然会按行输出坐标过滤能覆盖大部分场景但若课件是 WPS 另存为 PDF 时把页眉做成嵌入图片文本层里就没有任何内容只能借助图像 OCR 识别这一小片区域。提示对页脚区域做过滤前先用 2.1 节的打印脚本抽查页码所在块的位置。页码多是 10pt 左右的独立行如果它与正文块共用同一个 bbox说明导出工具把整页渲染成一张图片文本层结构已经失效这时要回到 OCR 流程。3. 用书签重建章节树把308页从线性翻阅变回结构化查询3.1 读 Outline 条目并校正页码偏移播放器里读 PDF 的书签对应到 PyMuPDF 就是get_toc()。选修四课件大多是从 PPT 打包转换的导出时自动带有三级书签一章对应 level1一节对应 level2知识点对应 level3。这个结构能直接用等于白送一套目录树比任何智能抽取都可靠。问题是返回的 page 是 PDF 逻辑页码从 1 开始计数而前面page.get_text()的索引从 0 开始两者之间隔了封面和目录页偏移不校正后面做页面切片会整体错位一到两页。toc doc.get_toc(simpleFalse) # (level, title, page, dest) for level, title, page, dest in toc: print(flevel{level} page{page} title{title})simpleFalse时每个条目多返回一个dest字典里面包含目标命中的具名位置信息。有的课件会在开始加一页目录书签同时在正文再次设置书签造成标题重复dest能帮你区分点进去停在哪一页。校正偏移量时取第一个 level1 的书签标题比如第一章 化学反应与能量然后在每一页的前 20 个文本块里搜这个标题命中的页就是正文真正的首屏如果没有命中再扩大搜索到页面文本的前 100 字符。偏移量就是该书签页码与命中页物理序号的差值后续所有从书签页码换算成物理页码的地方都要带上这个常量。3.2 无书签时用字号聚类还原标题层级不是所有课件都带书签。有的打包工具会去掉 outline有的原始文件干脆是扫描图文混排而成。先检查文本层是否为空对每一页执行page.get_text()统计字符数大多数页面字符数不足 20说明是纯扫描件文本层正常但无书签就用字号聚类重建标题。聚类前要过滤页眉页脚否则页脚的 10pt 文字会和正文混在一起干扰阈值计算。from collections import Counter size_hist Counter() for page in doc: for block in page.get_text(dict)[blocks]: if block[type] ! 0: continue for line in block[lines]: for span in line[spans]: t span[text].strip() if len(t) 4: # 只统计有效词块避免单个字干扰 size_hist[round(span[size], 1)] 1 for size, cnt in size_hist.most_common(15): print(fsize{size:5.1f} cnt{cnt})most_common(15)列出出现频次最高的 15 个字号正文通常对应其中的最高峰。把显著高于该峰字号、且出现在页面上半段的块作为一级标题候选比一级标题略小的归为二级标题。拿到候选后按页码排序把同一页上多个候选标题块按书写顺序合并成一行就能生成一份粗糙的目录。这种靠字号的还原无法保证精确课件里如果用了艺术字或图片标题字号聚类会漏掉但保留两级结构足够支撑把 308 页切成大段落。扫描件的情况要换用 OCR 流程中文场景我一般用 PaddleOCR 的 PP-OCR 系列模型批处理时把页面转成 150dpi 的 PNG 再识别。提示len(t) 4会过滤掉单个的第字或页码数字但也会漏掉真正只有一个字的标题。实际使用时改为统计全部 span 的总字符数和该字号出现页面数两个维度比单看频次更稳能避免某个字号只在某一页集中出现却干扰聚类。3.3 章节-知识点-页面关联表目录树最后要落成一张可查询的表。常见做法是把书签按本条起始页 下一条起始页的前一页组成区间章节区间就对应着 PDF 页面的一个连续切片。用 pandas 组织这个结构方便后续过滤、排序和合并。下面的代码把get_toc()结果转成 DataFrame假设已经通过 3.1 得到偏移量PAGE_OFFSET把逻辑页码换算成物理页码import pandas as pd PAGE_OFFSET 0 # 实际值 正文首屏物理页号 - 首个书签逻辑页号 rows [] for level, title, page, _ in doc.get_toc(simpleFalse): rows.append({level: level, title: title, start_page: page PAGE_OFFSET}) df pd.DataFrame(rows) df[end_page] df[start_page].shift(-1).fillna(len(doc)).astype(int) - 1 chapter_view df[df[level] 2][[title, start_page, end_page]] print(chapter_view)shift(-1)把后一行的起始页挪到当前行作为当前章节区间的预测终点fillna(len(doc))保证最后一条记录能延伸到文档末尾。level2 筛选可以直接看节一级的区间分布。检查结果时特别留意end_page start_page的条目那说明两条书签连在一起中间没有正文页常见于封面后的目录书签或空白分隔页。这些区间除了存数据库还可以反过来做页面标签给每一页附加所属章节后续所有关键词命中的页面就能按章节聚合。按书签合并后的示意输出如下实际数据以你自己的文件为准章节start_pageend_page页数第一章 化学反应与能量57672第二章 化学反应速率和化学平衡77178102第三章 水溶液中的离子平衡17926284第四章 电化学基础263308464. 倒排索引与概念依赖图把全套课件变成可导航的知识索引4.1 概念到页面的倒排索引有了章节区间下一步是建立知识点到页面的倒排表。倒排索引的价值在于把找某页在哪变成某个概念出现在哪些页这是课件检索和复习切片的基础查询接口。抽取概念的方式有两种从书签标题里收割命名实体或者从章节标题里拆词。以选修四为例概念集合大概是化学反应速率、化学平衡、勒夏特列原理、电离平衡、盐类水解、原电池、电解池、金属腐蚀这一组。from collections import defaultdict concepts [化学反应速率, 化学平衡, 勒夏特列原理, 电离平衡, 盐类水解, 原电池, 电解池, 金属腐蚀, 化学电源] inverted_index defaultdict(set) # 概念 - 出现页码集合 for page_idx, page in enumerate(doc): text page.get_text() for concept in concepts: if concept in text: inverted_index[concept].add(page_idx 1) # 转成1-based页码 for concept in concepts: pages sorted(inverted_index.get(concept, set())) print(f{concept} - {,.join(map(str, pages))})这里用defaultdict(set)天然去重同一页重复出现同一个概念不会造成两次命中页码按升序输出后直接能形成概念:页码列表的倒排条目。常见误用是直接把整页文本连标点一起存起来查询时每次都全字段扫描308 页规模不大但一旦扩展到多学科、上千个 PDF 性能就会明显恶化。我会把倒排表单独落成一张 SQLite 表字段就是 concept 与 page_no 的二元关系。查询概念时带上 3.3 的章节区间做二次过滤就能从出现在的页收敛到出现在的章节。4.2 概念依赖关系与有向图构建倒排表解决的是在哪里依赖图解决的是先学什么再学什么。选修四的概念联系是天然有向的平衡常数从属于化学平衡勒夏特列原理解释平衡移动方向化学平衡又服务于后面水溶液中的电离平衡。依赖关系来自两个信号一是概念 A 所在页面的序号区间整体早于概念 B二是同一章节区间内 A 的首次出现页靠前。两个信号一致时可以相当自信地建立一条 A→B 的边。import networkx as nx G nx.DiGraph() G.add_nodes_from(concepts) for a in concepts: pages_a sorted(inverted_index.get(a, set())) if not pages_a: continue first_a pages_a[0] for b in concepts: if a b: continue pages_b sorted(inverted_index.get(b, set())) if pages_b and max(pages_a) min(pages_b): G.add_edge(a, b, weight1) print(边数量:, G.number_of_edges())这个实现用max(pages_a) min(pages_b)作为出现区间不相交的判据逻辑简单但容易把同页出现两个概念的合法依赖丢掉。更稳的方法是把首现页差值和区间重叠率组合成一个得分first_b - first_a为正且重叠率低于 0.3则记一条边否则丢弃。实际课件里化学平衡和平衡常数在同一页反复共现仅靠区间不相交会漏边所以我会用重叠率作为辅助条件保留它们。拿到边之后用 networkx 的write_gexf导出再丢给可视化工具查看能直接看到概念之间的主链。4.3 按依赖序生成复习切片依赖图的实用出口是生成复习路径。把图看成一个 DAG做一次拓扑排序排在后面的概念默认依赖前面的概念。遇到原本成环的边说明课件中这两个概念互相引用常见情况是某张总览图把前后概念画在了一起这种情况我会去掉切分权重最小的一条边强行破环保证输出是线性队列。try: order list(nx.topological_sort(G)) print(复习顺序建议:, - .join(order[:5])) except nx.NetworkXUnfeasible: print(检测到环需先破环)拓扑排序的结果就是一份按依赖关系排列的概念学习序列。把它和 3.3 的章节区间、4.1 的倒排表合到一起就能给每个概念输出三件套出现的页码列表、所属章节、依赖它的后续概念。做复习切片时按这个序列每 2 到 3 个概念切一组把对应页码的文本块拼接出来形成一份份带页码出处的切片材料。整套流程跑完后那份 308 页的课件就从一个只能逐页翻的 PDF变成能回答某概念在哪、先学什么、后学什么的知识索引后续接知识库查询或生成讲义都只是输出层的事。5. 用一轮页面级巡检验收提取链路文本覆盖率、页面漂移与坏页定位5.1 文本覆盖率与空白页检查处理完一轮提取后必须先做验收否则前面的映射可能建立在坏数据上。最底层的检查是每个页面的文本覆盖率把每一页提取出来的文本字符数和 PDF 对象层可读文本量做对比如果某页提取到的字符数明显少于周围页面要么是图片页要么是文本层确实缺失。巡检脚本会同时检查get_text()和get_images()两者都为空才判定为真正的坏页。empty_pages [] for idx, page in enumerate(doc): text page.get_text().strip() if len(text) 20: has_image bool(page.get_images()) empty_pages.append((idx 1, has_image, len(text))) for page_no, has_image, char_count in empty_pages[:20]: print(fpage{page_no} has_image{has_image} chars{char_count})has_imageTrue说明内容是图片承载需要走 OCR 补全has_imageFalse且字符数极少则要检查是不是页面就是一张全幅插图或空白分隔页。扫描型 PDF 会在这里暴露大量坏页如果空页比例超过 5%前面第 2、3 章的结果都要打折扣先解决文本层的问题再谈后续。5.2 章节区间的连续性校验文本覆盖没问题后再校验环节的映射。拿第 3 章生成的章节区间逐段核对打印每个区间的起始页和结束页用肉眼抽查首尾页的标题文本是否与书签标题一致。这一步主要是抓PAGE_OFFSET差了一页这类系统性偏移。还有一种更隐蔽的漂移课件在制作过程中插入或删除了页面导致中间某条书签页码与实际标题不匹配后续所有依赖它切片的页面都会跟着错位。PAGE_OFFSET 0 # 由上一步标定 toc_pages [t[2] PAGE_OFFSET for t in doc.get_toc(simpleFalse)] for idx in range(len(toc_pages) - 1): start, end toc_pages[idx], toc_pages[idx 1] - 1 if end start: print(f异常区间: 书签#{idx} 起{start}止{end})这段代码把相邻书签的页码对做一次差检查end start时说明两个书签之间没有正文页通常是重复目录书签不一定是错误但如果异常区间集中在某一段就要回去看原始 PDF 的页数是不是和课件页数对不上。把所有异常区间列出来人工对照原 PDF 翻阅一次比全量检查快得多。最后把提取的文本总量、表格数、概念页面命中数和巡检结果一起记入处理日志后续再换新的课件 PDF 时这套巡检脚本可以原样复用。本文还有配套的精品资源点击获取
返回列表