Python解析.doc文档:从电工职业标准到SQLite知识库 📅 发布时间:2026/9/20 18:16:15 👁 浏览次数: 简介电工国家职业标准是一份面向电工行业从业者、职业院校学生及技能鉴定考生的规范性文档系统梳理了初级工、中级工、高级工的分级报考条件与考核路径。内容以考核大纲为主线覆盖职业道德、安全用电操作规程、识图与机械焊接基础、电工与电子技术、电力拖动、工厂变配电、微型计算机控制原理等知识模块并针对不同等级列出了具体技能要求与工具使用规范如三相异步电机控制、单相电机启动、测量仪表及安全防护用具操作等。整个资源包仅有1个doc文件共147KB便于打印或按目录快速查阅。已有244人学习浏览对准备职业资格鉴定、制定培训计划或规范岗位技能标准均有直接参考价值。文档还包含故障判断与处理、触电急救和电气灭火等安全实操要点可帮助读者明确从入门到高级的提升路径系统规划学习与备考方向。1. 先把《电工国家职业标准[详].doc》当作数据源而不是文档运维或数据工程师的目录里出现一份《电工国家职业标准[详].doc》时最常见的动作是双击打开翻一翻然后发现里面全是段落、表格、金字塔状的职业等级和考核要求。但从另一个视角看这份 doc 比很多系统导出的报表更值得解析它包含职业等级、技能项、培训要求、权重占比这些信息如果用 Python 抽成结构化的 JSON 或数据库表就能直接支撑岗位体系、培训课程包和技能图谱的搭建。我在这篇文章里不会讨论这份标准具体写了什么只讲如何处理它——从读取 .doc 扩展名、解析标题层级、抽取表格到把结果变成一个能grep或sqlite3查询的知识库。适合正在折腾文档解析、知识库构建的工程师以及需要把“纸质制度”转成“线上数据”的团队成员。2. 读取《电工国家职业标准[详].doc》文件的三种路径与最小环境配置2.1 为什么不能直接用 python-docx 打开 .docpython-docx几乎成了 Word 解析的默认选择但它的适用边界是.docx文件也就是 Office 2007 之后的 XML 格式。文件扩展名写成.doc时这是一个 OLE2 复合文档结构直接把这个路径交给Document()会抛出PackageNotFoundError或BadZipFile因为 python-docx 内部是用 zipfile 读取所有 XML 块的而老的.doc根本不是一个 zip 包。处理这类文件时我一般不会刻意去解析 OLE2 的二进制格式因为太容易踩字符编码的坑。常见的做法是先把.doc转换成.docx再用 python-docx 做后续的层级和表格提取。转换最少有两条路LibreOffice 的命令行转换或者 Windows 上通过 COM 调用 Word。2.2 路径一LibreOffice headless 转换并保留目录结构在 Ubuntu 或 CentOS 上装 LibreOffice就能用一条命令完成转换soffice --headless --convert-to docx --outdir /tmp/elect_raw /data/电工国家职业标准\[详\].doc转换成功后/tmp/elect_raw下会出现同名但扩展名为.docx的文件。注意文件名里的方括号不是正则但在 shell 里是通配符的一部分所以需要转义。这个方案的几个关键参数--headless不启动图形界面适合跑在服务器上。--convert-to docx把 OpenDocument 或老二进制格式转成 docx。--outdir指定输出目录避免覆盖源文件。LibreOffice 转换的问题在于如果原文档里用了很复杂的文本框或域代码转换后这些内容可能变成按照页面的分块而不是正常的段落。遇到这种情况可以先转成 text 提取纯文本再用转换后的 docx 做结构解析。2.3 路径二Windows 上用 win32com 直接读取 .doc如果你手上是 Windows 环境且已经安装了 Microsoft Word用win32com可以不做任何转换就读取.doc甚至能直接命中 Word 的段落和表格对象import win32com.client as win32 word win32.gencache.EnsureDispatch(Word.Application) word.Visible False doc word.Documents.Open(rD:\data\电工国家职业标准[详].doc) para_texts [para.Range.Text for para in doc.Paragraphs] tables [table.Rows.Count for table in doc.Tables] doc.Close() word.Quit()这段代码里EnsureDispatch会确保拿到 Word 的 COM 接口Visible False是为了不弹窗doc.Paragraphs会把段落全部读出来。注意Range.Text末尾会带一个\r字符在后续做清洗时要用strip()去掉。这个方案的局限是它只能在有 Office 的机器上跑不适合容器化或 Linux 服务器。但它最大的好处是能拿到 Word 内置的样式名称这对下一章按标题层级拆目录非常有价值。2.4 路径三antiword 和 textract 做快速纯文本提取有些环境里既没有 LibreOffice也没有 Word只有一台装了包管理器的 Linux 机器。这时可以用antiword快速提取文本antiword /data/电工国家职业标准[详].doc /tmp/standard.txtantiword对.doc文本段落的还原度不错但如果标准里的考核表是用表格画的它提取出来的表格列对齐会乱掉因为 antiword 只把文本按阅读顺序拼出来。textract则是一个 Python 库底层调用 antiword 或 LibreOffice写法变成了pip install textract然后在 Python 里调用textract.process(电工国家职业标准[详].doc)。它的优点是接口统一但依赖链较深不太适合离线环境。我通常只拿它做预览不放进生产流程。2.5 选型与基础环境检查命令三种方案各有边界我列一个比较表方便直接判断方案依赖能否拿样式输出典型场景LibreOffice 转 docx系统安装 soffice能.docxLinux 服务器批量转换win32comWindows Word能COM 对象Win 本机一次性解析antiword/textractantiword 或 textract不能纯文本只要内容不要结构定了方案以后务必先确认运行环境里有没有对应依赖python -c import docx; print(docx ready) soffice --version antiword -v这三行分别检查 python-docx、LibreOffice 和 antiword 是否可用。真正开始解析前第 2.2 节的转换命令我建议先手动跑一遍确认输出目录里出现.docx文件再继续写代码这样后面所有问题都能定位是转换阶段还是解析阶段。3. 把职业标准抽成结构化记录标题层级、表格与技能项3.1 先拆文档骨架从样式名到目录树转换完 docx 后第一件要做的事不是逐字读内容而是把“大纲”抽出来。一份职业标准文档的结构通常由标题 1、标题 2、标题 3 这样的样式标记出来python-docx 里的每个 paragraph 都有style.name属性所以可以用下面代码扫描from docx import Document doc Document(/tmp/elect_raw/电工国家职业标准[详].docx) tree [] for para in doc.paragraphs: style para.style.name if style.startswith(Heading): level int(style.split()[-1]) tree.append({level: level, text: para.text.strip()}) for item in tree: print( * (item[level] - 1) item[text])这段代码输出的是一个缩进树可以直接看到标准里出现了哪几个大章节比如职业概况、基本要求、工作要求和权重表。注意style.name可能不是标准的 “Heading 1”也可能是 “标题 1” 或自定义的 “TOC 1”所以打印出来后要人工看一下样式名再调整 if 条件。如果扫描出来所有段落都是 “Normal”说明这份 doc 是用格式刷手工调出来的没有用样式。这种情况我会放弃基于 style 的解析转用正则去匹配章节号的文本模式。3.2 用正则抓“职业等级”与“技能要求”条目没有样式信息时可以用文本模式来定位。职业类标准文档里等级条目往往长这样五级/初级、四级/中级、三级/高级、二级/技师、一级/高级技师。具体措辞可能有变化但大方向不会差。我会用一组正则去试探import re raw_text [] with open(/tmp/standard.txt, r, encodingutf-8) as f: raw_text f.readlines() level_pattern r^\s*[一二三四五]\s*级\s*[(]?\s*(初级|中级|高级|技师|高级技师) current_level None records [] for line in raw_text: line line.strip() if not line: continue m re.match(level_pattern, line) if m: current_level m.group(0) records.append({level: current_level, text: line}) elif current_level: records.append({level: current_level, text: line})这里的关键是匹配到一条新等级开始时把当前等级变量切换后续行都记到这个等级名下。level_pattern的正则里用[一二三四五]覆盖中文数字也兼容 “1级初级” 这种写法。实际应用时如果发现匹配不到可以把level替换成职业功能模块这样的关键词因为职业标准正文里每个功能模块也是一个重复出现的标题结构。这段代码的缺陷是它会把所有行都塞进记录包括表头、页码和空行。所以下一步要过滤掉“页眉页脚”和“第 X 页”之类的文本。建议加一条if re.search(r第\s*\d\s*页, line): continue3.3 表格数据怎么处理标准中的等级权重表职业标准文档里常常有“技能等级权重表”或“培训学时分配表”这些数据在 docx 里以表格形式存在。python-docx 的doc.tables可以直接访问到它们for i, table in enumerate(doc.tables): print(f--- Table {i} ---) for row in table.rows: cells [cell.text.strip().replace(\n, ) for cell in row.cells] print( | .join(cells))解析表格时要特别注意两个问题。第一列数可能不一致有的行单元格少一格python-docx 取不到的时候就返回空字符串。第二表格里可能有合并单元格导致同一个 cell 对象被多个 row 引用直接遍历会重复打印。针对重复引用可以按单元格在页面的坐标去重seen set() for row in table.rows: row_data [] for cell in row.cells: if cell._tc not in seen: seen.add(cell._tc) row_data.append(cell.text.strip())这段代码通过cell._tc来判断是否同一个 XML 节点。只有第一次出现时才记录后续合并出来的引用直接跳过避免把一行数据重复输出好几遍。3.4 解析结果的 JSON 输出与字段约定为了让后面的查询步骤可用我习惯把解析结果统一成如下 JSON 结构{ version: detail, levels: [ { name: 五级/初级, items: [ {code: A001, text: 能识别常用电工仪表}, {code: A002, text: 能执行安全操作规定} ] } ], tables: [ {title: 培训学时分配表, rows: []} ] }写回文件时用json.dump并指定ensure_asciiFalse否则汉字会变成\uXXXX转义序列with open(/tmp/elect_raw/standard.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2)这个 JSON 是中间产物后续无论是导入 SQLite 还是做技能图谱都可以从这个文件读取。字段名称没有强制标准但建议保持统一等级用levels技能项用items表格用tables。这样下游代码不需要记住每个表叫什么名字。4. 存入 SQLite 并提供查询命令按等级、工种、技能代码过滤4.1 建表结构一张表还是三张表解析出来的数据量不大但为了查询时能区分“这是一个等级”“这是一个技能项”“这是权重表里的一行”我一般会建三张表level、skill_item、weight_table。只建一张大表虽然简单但很容易在查“某个等级有哪些技能项”时混入表头文本。建表 SQL 可以直接写成一个初始化脚本CREATE TABLE IF NOT EXISTS level ( id INTEGER PRIMARY KEY, name TEXT UNIQUE ); CREATE TABLE IF NOT EXISTS skill_item ( id INTEGER PRIMARY KEY, level_id INTEGER, code TEXT, content TEXT, FOREIGN KEY(level_id) REFERENCES level(id) ); CREATE TABLE IF NOT EXISTS weight_table ( id INTEGER PRIMARY KEY, level_id INTEGER, row_data TEXT );这里level.name加上唯一约束可以避免重复导入把同一个等级插两次。4.2 用 Python 脚本灌入解析结果用上一章的 JSON 文件我写了一个很小的导入脚本import sqlite3, json with open(/tmp/elect_raw/standard.json, r, encodingutf-8) as f: data json.load(f) conn sqlite3.connect(/tmp/elect_raw/standard.db) cur conn.cursor() for level in data[levels]: cur.execute(INSERT OR IGNORE INTO level(name) VALUES (?), (level[name],)) cur.execute(SELECT id FROM level WHERE name ?, (level[name],)) level_id cur.fetchone()[0] for item in level[items]: cur.execute( INSERT INTO skill_item(level_id, code, content) VALUES (?, ?, ?), (level_id, item.get(code, ), item[text]) ) conn.commit() conn.close()脚本里的INSERT OR IGNORE保护了重复执行不会产生重复等级每个等级先查出自身 id再插入它的技能项这样查询时能通过外键过滤。4.3 命令行查询案例sqlite3 LIKE 过滤不需要写复杂的 ORMSQLite 自带的命令行客户端就可以应付大部分查询。比如要找所有技能项里提到“安全”的内容sqlite3 /tmp/elect_raw/standard.db \ SELECT level.name, skill_item.code, skill_item.content \ FROM skill_item JOIN level ON skill_item.level_id level.id \ WHERE skill_item.content LIKE %安全%;这条命令的关键点在于JOIN level是为了把等级名称拼出来否则只能看到等级 id没法读。LIKE %安全%做的是子串匹配所以“不安全”和“安全操作”都会被命中。如果要在按等级过滤比如只看“五级”的技能项sqlite3 /tmp/elect_raw/standard.db \ SELECT code, content FROM skill_item \ WHERE level_id (SELECT id FROM level WHERE name LIKE %五级%);这里的子查询等价于JOIN但当level.name带有空格或全角括号时LIKE %五级%会比精确宽容得多。4.4 查不到的坑全角空格、项目符号、换行符实际查询时最常遇到的问题不是 SQL 写错而是文本里的隐藏字符。Word 里最常见的三个坑第一个是全角空格形如\u3000直接LIKE % 安全%是匹配不到的因为普通空格是\x20全角是\u3000。我在导入前统一做了text.replace(\u3000, )。第二个是项目符号和编号比如段落开头是●或1.这些符号会被 python-docx 读进paragraph.text里。如果查LIKE %安全%不受影响但如果按code精确查就会失败因为A001前面可能有个不可见字符。第三个是换行符\n会出现在表格单元格里比如单元格内有两个段落读出来就是 “安全操作\n注意事项”。查询时用REPLACE(content, char(10), )或者导入前清洗。针对这三个坑我习惯在导入脚本里加一个清洗函数def clean_text(value): return (value .replace(\u3000, ) .replace(\u2002, ) .replace(\u2003, ) .replace(\n, ) .strip())5. 教你一个验证技巧把解析结果转回 Markdown逐章对比原文件5.1 为什么选择 Markdown 作为对账格式解析结果如果只在 JSON 里只能靠抽查确认内容对不对很难发现某个技能项被漏掉。我会把 JSON 转回一个带标题层级和列表的 Markdown 文件再拿它和原 doc 做肉眼对比。Markdown 可读性强也方便用差异工具比较。5.2 用 Python 生成 Markdown 骨架下面代码把standard.json转换成standard_parsed.mdwith open(/tmp/elect_raw/standard.json, r, encodingutf-8) as f: data json.load(f) lines [] for level in data[levels]: lines.append(f## {level[name]}) for item in level[items]: lines.append(f- {item.get(code, )} {item[text]}) for table in data[tables]: lines.append(f## 表格{table[title]}) for row in table[rows]: lines.append(f- { | .join(row)}) with open(/tmp/elect_raw/standard_parsed.md, w, encodingutf-8) as f: f.write(\n.join(lines))生成后的 Markdown 里每个等级都成为一个二级标题每个技能项成为一个无序列表项。这样原文档如果有一个技能项是“能正确使用钳形电流表”在 Markdown 里没出现一眼就能在对比时发现。5.3 快速差异核对diff -u 与详情现在需要把原文档的文本也转成一个 Markdown 或纯文本基线。可以基于第 2.4 节的 antiword 结果或者把 docx 里的段落按顺序导出成一个original_dump.txt。然后运行diff -u /tmp/elect_raw/original_dump.txt /tmp/elect_raw/standard_parsed.mddiff的输出会带着和-标记显示哪些行是解析结果里多出来的哪些是原文件里有但解析结果丢掉的。如果看到众多无意义的行差说明原文件里包含大量分页符或空格式段落需要在对比前先过滤掉空行grep -v ^[[:space:]]*$ original_dump.txt original_clean.txt grep -v ^[[:space:]]*$ standard_parsed.md parsed_clean.txt diff -u original_clean.txt parsed_clean.txt一个实用的技巧是只看 diff 中带^的块数量如果数量超过两位数大概率是解析层级有问题而不仅仅是漏了一两个词。这时回到第 3.1 节把样式扫描的输出打印出来检查标题样式名是否写成了.matchcase里区分大小写的模式或者把“等级条目”正则改成同时匹配“职业功能”和“工作要求”两种前缀。最后再用一次 diff 确认差异收敛到个位数这个解析结果才敢拿去做技能图谱和岗位匹配。本文还有配套的精品资源点击获取