Python解析.doc文档:从电工职业标准到SQLite知识库
2026/9/20 18:16:10 网站建设 项目流程

简介:电工国家职业标准是一份面向电工行业从业者、职业院校学生及技能鉴定考生的规范性文档,系统梳理了初级工、中级工、高级工的分级报考条件与考核路径。内容以考核大纲为主线,覆盖职业道德、安全用电操作规程、识图与机械焊接基础、电工与电子技术、电力拖动、工厂变配电、微型计算机控制原理等知识模块,并针对不同等级列出了具体技能要求与工具使用规范,如三相异步电机控制、单相电机启动、测量仪表及安全防护用具操作等。整个资源包仅有1个doc文件,共147KB,便于打印或按目录快速查阅。已有244人学习浏览,对准备职业资格鉴定、制定培训计划或规范岗位技能标准均有直接参考价值。文档还包含故障判断与处理、触电急救和电气灭火等安全实操要点,可帮助读者明确从入门到高级的提升路径,系统规划学习与备考方向。

1. 先把《电工国家职业标准[详].doc》当作数据源,而不是文档

运维或数据工程师的目录里出现一份《电工国家职业标准[详].doc》时,最常见的动作是双击打开翻一翻,然后发现里面全是段落、表格、金字塔状的职业等级和考核要求。但从另一个视角看,这份 doc 比很多系统导出的报表更值得解析:它包含职业等级、技能项、培训要求、权重占比,这些信息如果用 Python 抽成结构化的 JSON 或数据库表,就能直接支撑岗位体系、培训课程包和技能图谱的搭建。我在这篇文章里不会讨论这份标准具体写了什么,只讲如何处理它——从读取 .doc 扩展名、解析标题层级、抽取表格,到把结果变成一个能grepsqlite3查询的知识库。适合正在折腾文档解析、知识库构建的工程师,以及需要把“纸质制度”转成“线上数据”的团队成员。

2. 读取《电工国家职业标准[详].doc》文件的三种路径与最小环境配置

2.1 为什么不能直接用 python-docx 打开 .doc

python-docx几乎成了 Word 解析的默认选择,但它的适用边界是.docx文件,也就是 Office 2007 之后的 XML 格式。文件扩展名写成.doc时,这是一个 OLE2 复合文档结构,直接把这个路径交给Document()会抛出PackageNotFoundErrorBadZipFile,因为 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(r'D:\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.txt

antiword.doc文本段落的还原度不错,但如果标准里的考核表是用表格画的,它提取出来的表格列对齐会乱掉,因为 antiword 只把文本按阅读顺序拼出来。textract则是一个 Python 库,底层调用 antiword 或 LibreOffice,写法变成了:

pip install textract

然后在 Python 里调用textract.process('电工国家职业标准[详].doc')。它的优点是接口统一,但依赖链较深,不太适合离线环境。我通常只拿它做预览,不放进生产流程。

2.5 选型与基础环境检查命令

三种方案各有边界,我列一个比较表方便直接判断:

方案依赖能否拿样式输出典型场景
LibreOffice 转 docx系统安装 soffice.docxLinux 服务器批量转换
win32comWindows + WordCOM 对象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', encoding='utf-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): continue

3.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_ascii=False,否则汉字会变成\uXXXX转义序列:

with open('/tmp/elect_raw/standard.json', 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2)

这个 JSON 是中间产物,后续无论是导入 SQLite 还是做技能图谱,都可以从这个文件读取。字段名称没有强制标准,但建议保持统一:等级用levels,技能项用items,表格用tables。这样下游代码不需要记住每个表叫什么名字。

4. 存入 SQLite 并提供查询命令:按等级、工种、技能代码过滤

4.1 建表结构:一张表还是三张表

解析出来的数据量不大,但为了查询时能区分“这是一个等级”“这是一个技能项”“这是权重表里的一行”,我一般会建三张表:levelskill_itemweight_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', encoding='utf-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 过滤

不需要写复杂的 ORM,SQLite 自带的命令行客户端就可以应付大部分查询。比如要找所有技能项里提到“安全”的内容:

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.md

with open('/tmp/elect_raw/standard.json', 'r', encoding='utf-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', encoding='utf-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.md

diff的输出会带着+-标记,显示哪些行是解析结果里多出来的,哪些是原文件里有但解析结果丢掉的。如果看到众多无意义的行差,说明原文件里包含大量分页符或空格式段落,需要在对比前先过滤掉空行:

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 确认差异收敛到个位数,这个解析结果才敢拿去做技能图谱和岗位匹配。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询