ARTICLE DETAIL

资讯详情

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

Python 解析 docx 文档:表格、图片与知识点结构化提取

Python 解析 docx 文档:表格、图片与知识点结构化提取 简介这是一份面向沪教版小学数学一年级学生及家长的期末复习知识点归纳以docx文档形式整理成册帮助孩子系统梳理一学期所学内容。资源包内仅1个docx文件约40KB体积轻巧打印或平板查看都很方便。知识点覆盖加减法五条规律、比一比的方法、加减法互逆关系以及数位、计数单位、位值表、相邻数与整十数、百以内数的大小比较等数的认识内容同时纳入人民币单位元角分换算、时间初步认识包括钟面12大格60小格、时针分针读法、12时与24时计时法互化及时间计算并延伸到两位数加减整十数、加减一位数及进位退位的计算方法。文档条理清晰以条目式罗列便于对照记忆与查漏补缺适合课前预习、单元复习和考前突击使用。目前已有552人学习下载可作为一年级数学基础巩固的参考笔记。1. 一份《沪教版小学数学一年级知识点归纳.docx》为什么值得当成数据源教研组发来的资料十有八九是 docx单元用「一、二、三」手打编号知识点下面挂着「数的分与合」这类表格配图是钟面、小棒、七巧板。人翻着没问题程序一读就散架——python-docx 递给你一串 Paragraph编号层级、表格归属、图片位置全丢了。真正要做的不是把它读成纯文本而是还原成「单元—小节—知识点—题型」的树落成 JSON才能自动组卷、生成口算卡、算知识点覆盖率。下面这套流程走四步先看清 docx 的内部结构再把段落切成结构化条目然后处理图文混排的对齐问题最后用自检脚本验证结果。2. 拆开沪教版小学数学一年级知识点归纳.docxOOXML 结构与解析选型要把一份小学归纳文档变成数据第一件事不是写正则而是搞清楚 Word 到底把内容存在哪里。很多人一上手就Document(path).paragraphs跑通了输出一看表格全没了于是回头骂库不好用。其实问题出在对 docx 结构的假设上它不是一个线性文本文件而是一个带关系表的 XML 包段落、表格、图片各自有不同的存放位置和引用方式。先把这层看清楚后面切分知识点时才知道哪些信息能拿到、哪些必须绕路。2.1 docx 本质是 zip 加 XML段落、表格、图片各藏在哪把沪教版小学数学一年级知识点归纳.docx复制一份改成.zip解开会看到[Content_Types].xml、word/document.xml、word/styles.xml、word/media/、word/_rels/document.xml.rels这几样。正文内容全在document.xml里段落是w:p表格是w:tbl而图片并不直接出现在正文中正文里只留一个a:blip r:embedrId7真正的二进制要去document.xml.rels里按rId7查到目标路径再落到word/media/下对应的文件。样式名Heading 1、标题 2 之类单独存在styles.xml正文段落只带一个pStyle引用。理解这层关系后一个很实际的结论浮现出来段落顺序、表格位置、图片锚点这三样都只存在于document.xml的 XML 树里任何只遍历段落的做法都会把表格整块跳过。而一年级归纳文档里数的分与合、20 以内加减法表、认识人民币这些恰恰是表格形式。所以解析的第一步永远是按 XML 的真实顺序把块级元素一个一个取出来。2.2 python-docx、docx2python、pandoc 三条解析路线怎么选三条路都能走通区别在于你能拿到什么、要付出什么代价。下面这张表是我处理教研文档时的实际取舍依据方案能拿到什么会丢什么适合场景python-docx段落文本、样式名、表格单元格、内嵌图片字节、自动编号标记复杂浮动排版、公式对象OMML 需另读需要保留表格与层级自己写切分逻辑docx2python严格阅读顺序含表格、图片占位符、脚注、页眉页脚样式名很弱无法判断 Heading只关心读到的文本顺序对不对pandoc 转 Markdown一步得到 md表格转成管道表图片导出到目录编号层级变成 md 列表难以还原原始 ilvl后续要人工校对或输出给静态站点lxml 直读 document.xml全字段、全属性想读什么读什么开发成本最高得自己组装对象需要读 numPr 层级、图片锚点等细节我的常用组合是 python-docx 做主解析、lxml 补充读自动编号信息、pandoc 的输出当人工校对参照。不要指望一个库包打天下尤其是一年级文档里自动编号用得极多的时候。2.3 用 python-docx 按 body 顺序打印文档骨架的最小脚本下面这段脚本是整个流程的地基它不做任何切分只按document.xml的真实顺序把段落和表格打印出来让你先看清文档长什么样# skeleton.py 按 body 真实顺序遍历段落与表格都不漏 from docx import Document from docx.oxml.ns import qn from docx.table import Table from docx.text.paragraph import Paragraph doc Document(沪教版小学数学一年级知识点归纳.docx) def iter_block_items(document): 按 XML 顺序产出 Paragraph / Table表格不会被跳过 body document.element.body for child in body.iterchildren(): if child.tag qn(w:p): yield Paragraph(child, document) elif child.tag qn(w:tbl): yield Table(child, document) for i, block in enumerate(iter_block_items(doc)): if isinstance(block, Paragraph): text block.text.strip() if not text: continue # 空段太多直接跳过 style block.style.name if block.style else print(f[{i:04d}] P style{style:12} len{len(text):4} :: {text[:40]}) else: rows, cols len(block.rows), len(block.columns) head block.cell(0, 0).text.strip().replace(\n, ) print(f[{i:04d}] T {rows}x{cols} :: {head[:30]})逻辑说明iter_block_items用的是body.iterchildren()而不是document.paragraphs这样表格会以w:tbl的身份被识别出来段落与表格的相对顺序也和原文一致。参数上text[:40]只截前 40 个字避免一份长文档刷屏style字段留着后续判断标题级别空段直接continue因为归纳文档里手打回车产生的空段占比能到三成全打出来反而看不清结构。跑完这份骨架你会对哪些知识点在段落里、哪些在表格里心里有数。2.4 只读 document.paragraphs 会丢掉哪些知识点具体丢什么取决于文档的排版习惯但一年级归纳里高频出现的三类损失几乎跑不掉。第一类是表格型知识点10 以内数的分与合通常做成两列的分解表document.paragraphs里完全看不到第二类是单元格内的换行段落认识钟面那一栏可能一格塞了三行说明只读外层段落只会得到一个空字符串第三类是文本框与图形Word 里为了排版美观常常把口算题放进文本框这些内容既不在 body 的段落里也不在表格里。排查手法很简单拿 2.3 的脚本统计一遍再用Document(...).paragraphs统计一遍两个数字差得越多说明藏起来的内容越多。差得离谱的时候别急着写正则先回去用unzip -l看一眼word/media/和document.xml的体积确认是不是整个知识点区都是图片或文本框。3. 知识点切分把沪教版一年级归纳.docx 的段落变成结构化条目拿到按顺序排列的块之后下一步是切。切分的目标是让每一行数据带上「学期—单元—小节—知识点」四级归属。这一步最容易被低估一年级的归纳文档没有统一模板编号一会儿是「一、」一会儿是「1.」一会儿又是 Word 的自动编号文本里根本看不见数字。切分逻辑必须做成样式名优先、正则兜底、自动编号补齐的三通道缺一条就会出现层级错乱。3.1 先摸清一年级知识点归纳的编号习惯与自动编号陷阱沪教版一年级的归纳文档常见的层级写法有四种第一单元、一、数与运算、1. 20 以内数的认识、① 数数。这四种还经常混用同一份文档里既有手打编号又有自动编号。自动编号是最大的坑段落的w:pPr里带着w:numPr里面有w:ilvl层级和w:numId编号列表 ID但段落文本里一个字都没有。这时候你用正则去匹配数字永远匹配不上所有小节的标题都会被当成正文最后整棵树塌成一层。识别办法是直接查 XMLp._p.pPr.numPr存在就说明这段用了自动编号ilvl的值就是层级深度。同一份文档里numId相同的段落属于同一个编号列表层级靠ilvl区分这个信息比正则可靠得多。3.2 样式名优先、正则兜底的标题识别代码下面这段判断函数把三条通道串起来返回该段落是否是标题以及级别import re STYLE_TITLE re.compile(r^(Heading|标题)\s*(\d), re.I) NUM_TITLE re.compile( r^\s*(第[一二三四五六七八九十][单元讲] r|[一二三四五六七八九十][、.] r|\d(\.\d)*[、.\s] r|[①②③④⑤⑥⑦⑧⑨⑩]) ) MAX_TITLE_LEN 30 # 超过这个长度的编号段落按正文处理 def title_level(p): 返回 None 表示正文返回整数表示标题层级1 起 text p.text.strip() style_name (p.style.name or ) if p.style else m STYLE_TITLE.match(style_name) if m: return int(m.group(2)) # 通道一样式名 pPr p._p.pPr if pPr is not None and pPr.numPr is not None: ilvl pPr.numPr.ilvl return (int(ilvl.val) if ilvl is not None else 0) 1 # 通道三自动编号 if NUM_TITLE.match(text) and len(text) MAX_TITLE_LEN: return 2 if text.startswith((第, 一, 二, 三)) else 3 # 通道二正则 return None逻辑说明三条通道有明确的优先级。样式名最可信因为 Word 的大纲级别直接映射到导航窗格自动编号次之因为它带真实的ilvl正则兜底只用来处理前两条都失效的手打编号。参数MAX_TITLE_LEN 30很关键一年级的题目里经常出现「1. 数一数下面有几只小动物」这种以数字开头的正文不设长度上限就会把题目误判成标题。如果文档里题目更长把这个值调到 24 左右再跑一遍对比。3.3 数的分与合、加减法表表格知识点转二维数组表格在一年级归纳里承担了主要的结构化表达转成二维数组时要特别注意合并单元格。python-docx 的row.cells会把横向合并的格重复返回同一个对象直接遍历会出现重复列def table_to_rows(tbl): 表格转二维数组按 _tc 去重处理横向合并 out [] for row in tbl.rows: cells, seen [], set() for c in row.cells: key id(c._tc) if key in seen: # 同一行里合并的格只取一次 continue seen.add(key) cells.append(c.text.strip().replace(\n, )) out.append(cells) return out # 用法识别表头把后续行按行组装成知识点 tbl None # 从 iter_block_items 里拿到的 Table 对象 rows table_to_rows(tbl) if rows and any(分 in h or 合成 in h for h in rows[0]): for r in rows[1:]: print({type: decompose, from: r[0], parts: r[1:]})逻辑说明id(c._tc)取的是底层 XML 元素的内存地址同一个w:tc被重复引用时地址相同借此判断合并。参数上replace(\n, )是为了把单元格内的软换行压平避免后续生成 JSON 时出现裸换行符导致解析失败。表头判定用关键词而不是固定列号因为不同单元的表格列数并不统一。3.4 字段定义与落盘knowledge.json 的 Schema 与参数切完的条目统一落进一个 JSON 数组字段设计得克制一点够用就行字段类型说明idstring形如g1-t1-u2-p03学期-学期段-单元-序号unitstring单元名来自一级标题sectionstring小节名来自二级标题pointstring知识点正文已去除编号typestringtext/table/formula/imagepayloadobject表格转二维数组、图片转{path, index}assetsarray该知识点关联的图片文件名src_indexint在document.xml中的块序号用于回溯定位src_index这个字段容易被忽略但排查问题时极其有用自检发现某条知识点内容为空直接按src_index回到原文对应的块三秒钟就能判断是切分错了还是原文本来就空。3.5 切分阈值调不准时的三个排查方向切出来层级全平了先查自动编号看numPr是否存在、ilvl是否有值。知识点被切碎成几十条多半是正则把题目里的数字开头也当成了标题把MAX_TITLE_LEN调小再跑。还有一种是知识点粘连一节下面所有条目挤在一条记录里这通常说明文档的小节标题用的是纯文字加粗而没有编号需要补一条加粗且独占一段的判定规则用p.runs[0].bold来判断。4. 图文混排处理钟面、小棒、图形与知识点的对齐一年级数学的特殊之处在于很多知识点离开图就没法表达。认识钟面靠一张表盘图图形的拼组靠七巧板比多少靠一行小棒。纯文本方案在这一步会彻底失效必须把图片从 docx 里抠出来并且知道每张图属于哪个知识点。这一步做不好后面生成的复习卡片就是一堆没有上下文的图片文件。4.1 一年级知识点为什么图比字多纯文本方案为什么失效统计过一份归纳文档正文有效文字不到 6000 字但内嵌图片有 40 多张配图密度远高于高年级资料。原因不复杂一年级学生的识字量有限教材本身就靠图示建立概念归纳文档只是把教材的图搬过来。这意味着如果只导出文字认识钟面会变成一句认识钟面会看整时和半时孩子拿着这句话根本复习不了。更麻烦的是图片的位置信息在纯文本视图里完全丢失。同样一句看图填空配的是加法图还是减法图只有图片本身能说明。所以图片不仅要导出来还要带着它出现在哪个知识点附近这个锚点信息一起存。4.2 用 python-docx 关系表导出 word/media 并保序内嵌图片通过inline_shapes按顺序访问再顺着r:embed找到二进制import os from docx.oxml.ns import qn def dump_images(doc, outdirmedia): os.makedirs(outdir, exist_okTrue) items [] for i, shape in enumerate(doc.inline_shapes): blip shape._inline.graphic.graphicData.pic.blipFill.blip rid blip.get(qn(r:embed)) # 关系 ID part doc.part.related_parts[rid] # 从关系表拿到 Part ext part.partname.ext or png path os.path.join(outdir, fimg_{i:03d}.{ext}) with open(path, wb) as f: f.write(part.blob) items.append({index: i, rid: rid, path: path, bytes: len(part.blob), w: shape.width, h: shape.height}) return items逻辑说明inline_shapes只覆盖嵌入型图片浮动型anchor不在其中。一年级归纳里为了排版美观偶尔会把图片设成浮于文字上方这类图要用 lxml 遍历wp:anchor节点补捞否则会漏。参数上shape.width和shape.height是 EMU 单位1 厘米约 360000 EMU导出后换成像素方便前端展示。文件名用img_000这种补零格式保证按名称排序就是文档顺序。4.3 图片与知识点的就近锚定策略导出图片后要把index映射回知识点。做法是在 2.3 的遍历里同时记录图片出现的位置遍历到w:p时检查该段落是否包含w:drawing把块序号记下来。然后对每条知识点取它的src_index向前后各找一个最近的图片块距离在 3 个块以内的就绑定def bind_assets(blocks, points, max_gap3): blocks: [(idx, P|T|IMG)]points: 知识点列表 img_idx [b[0] for b in blocks if b[1] IMG] for p in points: p[assets] [] for gi in img_idx: if abs(gi - p[src_index]) max_gap: p[assets].append(fimg_{gi:03d}.png) return points逻辑说明max_gap3是个经验值一份典型归纳文档里图片和它的说明文字之间通常隔着标题段、空段、说明段三块左右。如果发现知识点绑到了不相干的图把这个值降到 2如果大量知识点一张图都没绑上说明文档里图片是集中放在单元末尾的那就得改成按单元就近分配的策略而不是按块距离。4.4 钟面、数轴、人民币的结构化替代表示不是所有图都需要原图。有些图形可以参数化参数化之后能自动出题比存图片价值大得多图形类型是否可参数化结构化表示钟面认识整时半时可以{type:clock,h:3,m:0}数轴 / 数线可以{type:numberline,start:0,end:20,marks:[5,10,15]}小棒、圆点计数可以{type:count,shape:stick,n:12}人民币面额可以{type:money,items:[10,5,1,1]}七巧板拼组不建议保留原图追加{type:puzzle,img:img_017.png}立体图形实物图不建议保留原图判断标准是只要图中的信息可以完全用数字描述就参数化只要涉及空间关系、颜色搭配、实物外观就留原图配标签。钟面和数轴参数化之后3 时整和9 时半可以一键换数字重新出题这是纯图片方案做不到的。4.5 校验导出图片数量与文档顺序是否一致导出完一定要对一遍数。先看包里到底有多少张图再看导出了多少# 查看 docx 里的媒体文件清单与数量 unzip -l 沪教版小学数学一年级知识点归纳.docx | grep word/media/ unzip -l 沪教版小学数学一年级知识点归纳.docx | grep -c word/media/两个地方的差异来源通常是图片被多次引用同一个rId出现在多个位置或者图片在页眉页脚里存在word/media/但不在正文的inline_shapes里。如果unzip数出来 45 张而脚本只导了 40 张缺的 5 张大概率是浮动图或页眉装饰图前者要补捞后者可以直接忽略。5. 进阶技巧知识点覆盖度自检、版本 diff 与自动出题5.1 用 numPr 的 ilvl 还原多级编号层级切分阶段用的ilvl只能给出相对深度真正的层级归属要靠numId分组def outline_of(p): pPr p._p.pPr if pPr is None or pPr.numPr is None: return None num_id pPr.numPr.numId.val if pPr.numPr.numId is not None else None ilvl pPr.numPr.ilvl.val if pPr.numPr.ilvl is not None else 0 return (num_id, ilvl) # 同 numId 同 ilvl 属于同一层拿到(numId, ilvl)之后按文档顺序维护一个栈遇到ilvl0就清空栈压入ilvl1就替换栈顶第二层依此类推。这样即使文档中间手动在段落前敲了空格层级也不会错乱因为ilvl是 Word 存的跟空格无关。5.2 覆盖度自检与两份归纳版本的差异比对自检脚本不用复杂四个指标就够每个单元的point数量、payload为空的条数、assets为空的图片型知识点数、src_index是否单调递增。最后一项能抓出一个隐蔽问题——如果src_index不单调说明切分时把某段内容重复处理了。版本比对则更实用教研组每年会更新归纳文档把两年的knowledge.json按id做集合差新增的知识点、删除的知识点、point文本变化的知识点各出一份列表改动一目了然。字段里id用学期-单元-序号而不是文本哈希就是为了让这种 diff 在文本微调时仍然稳定。5.3 从 knowledge.json 直接生成口算卡与复习清单数据齐了之后出题就是纯模板活。参数化的钟面和数轴可以直接渲染成 SVG10 以内数的分与合的二维数组可以循环出填空20 以内加减法表可以随机抽行生成口算卡。一个细节值得注意抽题时按unit分组加权把assets非空的知识点固定踢掉一部分避免每次生成的卡片里图片占比过高导致孩子只翻图不看题。生成端只要保证同一份knowledge.json输出幂等教研组换一版文档整批复习材料跟着更新不用再手工翻 Word。本文还有配套的精品资源点击获取
返回列表