news 2026/9/18 1:54:24

慢性阻塞性肺病病历.doc结构化:从格式解析到临床数据抽取

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
慢性阻塞性肺病病历.doc结构化:从格式解析到临床数据抽取

简介:《慢性阻塞性肺病病历.doc》是一份临床医学教学用标准病历文档,适合医学生、规培医师及临床带教老师参考,完整记录了一例63岁土家族女性COPD患者的入院诊疗过程。资源共1个doc文件,压缩包仅55KB,内容包含主诉、现病史、过去史、个人史、体格检查、辅助检查及初步诊断,重点展示了慢性阻塞性肺病合并慢性肺源性心脏病、右心衰竭的典型临床演变。文档中可见反复咳嗽、白色泡沫痰、气急、下肢浮肿、口唇发绀等表现,并涉及抗生素控制感染、平喘、利尿等治疗策略,以及吸烟、环境暴露等风险因素分析。通过此病例可学习病历书写格式、COPD诊断依据、辅助检查判读(血常规、尿常规)及慢病管理思路。该资源已有94人学习浏览,尤其适合呼吸内科教学查房、出科考核前复习或病历书写参考。

1. 慢性阻塞性肺病病历 .doc 是一道数据解析题,不是文档题

拿到一份“慢性阻塞性肺病病历.doc”,多数人第一反应是打开 Word 看一眼。但对做医疗信息化、科研数据治理或慢病管理平台的人来说,这份文件意味着另一件事:它里面的诊断、肺功能数值、用药方案,全都埋在一段非结构化文本里。真正要解决的不是“能不能读”,而是“怎么把 .doc 里的信息稳定地取出来,变成数据库里能查询的一行记录”。旧的 .doc 格式是二进制复合文档,直接按文本处理必然翻车;更麻烦的是,病历里充满了缩写、单位混用和省略写法,比如 FEV1 后面只跟一个数字。这篇文章会从拆解 .doc 格式开始,一路走到结构化输出和结果验证。适合正在做病历数据结构化、临床科研数据抽取的工程师,也适合想把手头历史病历盘活的业务方参考。

2. 解开慢性阻塞性肺病病历 .doc 的格式外壳

2.1 为什么 Python 直接读不了 .doc

早年的 Word .doc 文件不是纯文本,而是 OLE2 复合文档,文件头有固定的魔数D0 CF 11 E0 A1 B1 1A E1。这意味着用open()按文本模式读取,只会得到一串乱码;用pandas.read_csv之类的方式更无从谈起。很多人第一步就栽在这里:拿到“慢性阻塞性肺病病历.doc”,尝试用常见的文本处理库直接解析,结果要么报错要么乱码,于是误判为文件损坏。其实文件没坏,只是外壳没打开。

新版的 python-docx 库只能处理 .docx,它内部是 ZIP 结构的 XML 文档,和 .doc 是完全不同的容器格式。社区里常提到的 textract、antiword 各有局限:antiword 只抽出纯文本,表格结构和段落边界全部丢失;textract 的安装依赖多,在处理带复杂表格的住院病历时稳定性一般。我一般不会在这些工具上死磕,而是用 LibreOffice 将 .doc 批量转成 .docx,再用 python-docx 读取结构化内容。这条链路同时保留了段落和表格,比直接转纯文本信息损失小得多。

2.2 用 LibreOffice 批量转换的最小命令

LibreOffice 在无界面服务器上可以 headless 运行,转换命令稳定且可脚本化。以下是在 Linux 或 macOS 环境下的常见做法:

soffice --headless --convert-to docx --outdir ./converted ./慢性阻塞性肺病病历.doc

参数含义:--headless表示不启动图形界面;--convert-to docx指定目标格式,LibreOffice 会根据扩展名自动选择 Word 2007 过滤器;--outdir指定输出目录,缺省时输出到当前目录。执行后,./converted/慢性阻塞性肺病病历.docx就是可被 python-docx 解析的文件。

批量处理几十份病历时,可以写一个循环:

mkdir -p ./converted for f in ./病历批次/*.doc; do soffice --headless --convert-to docx --outdir ./converted "$f" done

第一次运行可能稍慢,因为 LibreOffice 要初始化用户配置目录;后续每次转换大约耗时几百毫秒到一两秒,取决于文件大小和段落复杂程度。转换完成后建议抽样打开几份,重点检查表格是否完整、页眉页脚是否混入正文。有些 .doc 文件里嵌入了医学影像报告或旧的 OLE 对象,转换后可能变成空白或图片占位符,这部分内容本就难以结构化,后续处理时可以跳过。

转换完成后,用 python-docx 读取段落和表格:

from docx import Document doc = Document("./converted/慢性阻塞性肺病病历.docx") for p in doc.paragraphs: text = p.text.strip() if text: print(f"[段落] {text}") for idx, table in enumerate(doc.tables): print(f"[表格 {idx}]") for row in table.rows: cells = [c.text.strip() for c in row.cells] print(" | ".join(cells))

这段代码先遍历所有段落,过滤空行,再遍历表格逐行打印。Document对象会把 .docx 的 XML 结构解析成 headings、paragraphs、tables 三个层次,表格里的每个单元格通过row.cells按顺序取出。肺功能报告、用药清单这类信息在原始病历里多半以表格承载,所以优先保证表格的行列结构不被拍平。

提示:如果转换后的文本出现大量空格和制表符混杂,先不要急着清洗。先确认是源文件本身的排版问题,还是转换过程中产生的,避免误删真实内容。

3. 从慢性阻塞性肺病病历文本中抽取诊断与关键指标

3.1 病历文本的特征与规则适配

结构化的第二步是内容抽取。慢性阻塞性肺病病历虽然写法各异,但核心信息高度集中:诊断名、GOLD 分级、急性加重状态、肺功能指标(FEV1、FVC、FEV1/FVC 比值)、用药方案。每一类都有相对固定的表达方式,例如:

  • 诊断:COPD慢性阻塞性肺疾病慢阻肺AECOPD
  • 加重状态:急性加重期稳定期
  • 肺功能:FEV1/FVC < 70%FEV1 1.2LFEV1%pred 42%
  • 用药:吸入布地奈德/福莫特罗噻托溴铵ICS/LABA

因为词汇在封闭集合内,规则抽取在这里比通用 NER 模型更可靠。常见做法是先用正则把候选片段捞出来,再做上下文校验,避免把别的数值错当成肺功能结果。

3.2 诊断相关正则规则与代码实现

下面是抽取诊断与加重状态的规则示例:

import re TEXT = """ 患者男性,68岁,确诊COPD 5年。本次因咳嗽、咳痰加重伴发热2天入院。 查体:双肺呼吸音低,可闻及湿啰音。 肺功能:FEV1 1.2L,FEV1/FVC 58%。 用药:吸入布地奈德/福莫特罗,每日两次。 """ def extract_diagnosis(text): rules = { "copd": r"COPD|慢性阻塞性肺疾病|慢性阻塞性肺病|慢阻肺", "exacerbation": r"急性加重[A-Za-z]*|AECOPD|加重期", "stability": r"稳定期|缓解期" } result = {} for key, pattern in rules.items(): matches = re.findall(pattern, text, re.IGNORECASE) result[key] = list(set(matches)) if matches else [] return result print(extract_diagnosis(TEXT))

输出结果为{'copd': ['COPD'], 'exacerbation': ['急性加重'], 'stability': []}。正则中的[A-Za-z]*是为了兼容“急性加重期”“急性加重”等写法;re.IGNORECASE处理大小写混用。每条规则独立匹配、互不干扰,方便后续对某一类结果单独复核。

这里没有直接使用深度学习模型的原因很简单:病历相比新闻文本,句式高度套路化,常见实体类型不超过十种,规则写清楚后准确率能到 95% 以上,而且每一条匹配都有据可查。医疗数据审查时,能解释为什么抽取到这个字段比模型黑盒更有说服力。只有当病历来源复杂、存在大量口语化记录时,才需要考虑在规则之上叠加医疗 BERT 模型。

3.3 肺功能指标的数值提取:带单位的正则写法

肺功能数据是 COPD 病历里最常被检索的数值字段,也是污染最严重的。单看“FEV1 1.2”这个片段无法判断单位是 L 还是百分比,所以抽取时要同时捕获数值和紧随其后的单位。

def extract_pft(text): patterns = { "fev1": r"FEV1\s*[::]?\s*([\d.]+)\s*(L|升|%)", "fev1_fvc_ratio": r"FEV1/FVC\s*[::]?\s*([\d.]+)\s*%", "fev1_pred": r"FEV1\s*%pred\s*[::]?\s*([\d.]+)\s*%" } result = {} for key, pattern in patterns.items(): m = re.search(pattern, text, re.IGNORECASE) if m: value, unit = m.groups() if m.groups() else (m.group(1), "") result[key] = {"value": float(value), "unit": unit} return result print(extract_pft(TEXT))

正则FEV1\s*[::]?\s*([\d.]+)\s*(L|升|%)的含义是:先匹配FEV1,允许中间出现零个或多个空格以及一个冒号,然后捕获数值部分,最后捕获单位。re.search只取第一个匹配,因为病历中一般只有一个核心肺功能结论;如果存在多次测试,再改为findall后按时间顺序排列。

一个常见误用是直接写r"FEV1\s*([\d.]+)",这会把FEV1/FVC 58%里的58错误捕获成 FEV1 的数值。避免方法就是让单位参与匹配,并在必要时添加负向前瞻排除/FVC的出现。

提示:碰到FEV1 1.2LFEV1%pred 42%出现在同一份病历里时,两条规则会各自命中。此时输出结构里应该用一个type字段标记每一条指标的语义,便于后续按字段写入数据库,而不是简单合并成一个列表。

4. 将慢性阻塞性肺病病历映射为结构化 JSON 的完整流程

4.1 字段映射表:从病历语言到数据模型

要从一份患者病历文档里稳定地产出结构化数据,先得定义目标字段集。以慢性阻塞性肺病病历为例,我一般先约定下面这些必需字段:

目标字段病历中的典型原文数据类型说明
diagnosisCOPD;慢性阻塞性肺疾病string[]可能包含多条诊断
exacerbation_status急性加重期 / 稳定期string非空时优先取加重状态
fev1FEV1 1.2Lfloat + unit保留原始单位
fev1_fvc_ratioFEV1/FVC 58%float用于气流受限判断
gold_stageGOLD 2 级;中重度string病历中可能直接给出
medication布地奈德/福莫特罗;噻托溴铵string[]按行或按顿号拆分

字段集不要一开始设计得很大,先把“诊断、肺功能、用药”三类高频信息做扎实,再逐步扩展。COPD 的 GOLD 分级如果病历里没写,不要靠 FEV1%pred 强行推算,因为推算依据不同会导致口径不一致。

4.2 完整抽取代码:段落过滤、模式匹配、结果输出

下面给出一个从转换后的 docx 到 JSON 的完整链路示例:

import json import re from docx import Document def clean_text(text): # 去除全角空格和多余空白,保留换行 text = text.replace("\u3000", " ").replace("\t", " ") return re.sub(r"[ \t]+", " ", text).strip() def extract_medical_entities(text): entities = { "diagnosis": [], "exacerbation_status": None, "fev1": None, "fev1_fvc_ratio": None, "gold_stage": None, "medication": [] } dx_rules = [ r"COPD|慢性阻塞性肺疾病|慢性阻塞性肺病|慢阻肺", r"哮喘|支气管扩张|肺气肿" ] for rule in dx_rules: matches = re.findall(rule, text, re.IGNORECASE) entities["diagnosis"].extend(matches) m = re.search(r"急性加重期|AECOPD|急性加重", text, re.IGNORECASE) if m: entities["exacerbation_status"] = m.group(0) m = re.search(r"FEV1/FVC\s*[::]?\s*([\d.]+)\s*%", text, re.IGNORECASE) if m: entities["fev1_fvc_ratio"] = float(m.group(1)) m = re.search(r"FEV1\s*[::]?\s*([\d.]+)\s*(L|升)", text, re.IGNORECASE) if m: entities["fev1"] = {"value": float(m.group(1)), "unit": m.group(2)} m = re.search(r"GOLD\s*([1-4])\s*级", text, re.IGNORECASE) if m: entities["gold_stage"] = f"GOLD {m.group(1)}" med_matches = re.findall(r"吸入[^,。;\n]+|布地奈德|福莫特罗|噻托溴铵|沙美特罗", text) entities["medication"] = list(set(med_matches)) return entities doc = Document("./converted/慢性阻塞性肺病病历.docx") full_text = "\n".join( [p.text for p in doc.paragraphs if p.text.strip()] ) for table in doc.tables: for row in table.rows: row_text = " ".join(cell.text.strip() for cell in row.cells) full_text += "\n" + row_text cleaned = clean_text(full_text) result = extract_medical_entities(cleaned) with open("./output/structured_copd.json", "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False, indent=2) print(json.dumps(result, ensure_ascii=False, indent=2))

这段代码的核心逻辑分三层:清洗、段落合并、实体抽取。clean_text处理全角空格和连续空白,避免正则因隐藏字符失配;段落合并把表格内容追加到纯文本之后,保证肺功能报告和用药清单即使以表格形式存在也能被后续规则命中;extract_medical_entities里每个字段独立抽取,避免一次大正则互相干扰。

用药部分使用了多个候选词并行捕获,因为不同病历对吸入制剂的写法差异极大,有的写通用名“噻托溴铵”,有的写商品名。捕获后用set去重,防止同一药物出现两个别名时被计入两次。假如同一个药品商品名和历史病历药品备注之间存在交叉匹配,这个粗糙规则可能会多抓,但宁多勿漏的原则在第一步抽取里优先级更高。

结构化输出到 JSON 之后,后续无论是写数据库、做可视化,还是用于医保接口对接,都变成常规操作。这里选择 JSON 作中转格式是因为它天然支持嵌套结构,后续要改造成 CSV 或 DataFrame 也容易。

5. 执行结果验证与 CODP 病历解析的常见坑位

5.1 用基准集验证抽取结果

规则写了,输出也有了,但没人能保证每条正则百分之百正确。严谨的做法是准备一份人工标注基准集,逐字段评估抽取效果。常见做法是拿 30 到 50 份已清洗的病历,人工标好诊断、FEV1、FVC 比值三个字段,然后跑一次批量抽取,对比结果:

def evaluate_field(predicted, gold): gold_set = set(gold) pred_set = set(predicted) tp = len(pred_set & gold_set) precision = tp / len(pred_set) if pred_set else 0 recall = tp / len(gold_set) if gold_set else 0 f1 = 2 * precision * recall / (precision + recall) if (precision + recall) else 0 return {"precision": precision, "recall": recall, "f1": f1}

把同一份病历抽取出的字段列表放进predicted,人工标注放进gold,逐字段计算 F1。低于 0.9 的字段建议回炉调整正则。这套评估方法不依赖任何机器学习库,一张表就能跑完,在项目初期足够定位问题。

5.2 四个高频解析坑及应对手段

坑一:Word 自动换行拆散关键片段。源文件里的FEV1/FVC 58%可能会在 Word 中因自动换行变成两行,转换后读入 Python 时文本变成FEV1/FVC 58%,正则匹配失败。应对手段是预处理时先去掉段落间的孤行换行符号,或者给正则加上\s*允许中间有换行。

坑二:表格合并单元格导致重复取值。住院病历的用药表经常出现跨行合并单元格,python-docx 读取后会重复返回同一单元格内容。去重的简单做法是对row.cells的结果做顺序去重,保留第一个非空值即可。

坑三:单位混写。同一份 PDF 扫描转 Word 的历史病历里,FEV1 的单位可能是 L、升、ml,数值也可能写成1.2 L1200 ml。要不要统一单位,取决于后续分析需求;如果统计 FEV1 占预计值百分比,建议在结构化阶段就统一为 L,降低下游使用成本。

坑四:GOLD 分级与加重状态互斥规则。部分病历里同时出现“GOLD 3 级”和“稳定期”,这时候优先保留分级,加重状态以最近一次门诊记录为准。规则抽取应该同时输出“抽到了什么”和“原文位置”,方便复核时聚焦冲突点。

验证阶段建议把解析成功的样本和未命中的样本分开存放,定期回看未命中样本。新增正则时先跑回回归测试,防止修复 A 字段时破坏 B 字段的匹配。慢性阻塞性肺病病历的解析难点从来不在深度而在细节,一份能稳定复现、能看到失败样本的处理管线,比一个单独调到 99% 的规则函数更有长期价值。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 1:53:54

Chrome设备模拟调试移动端页面的完整指南

1. 为什么要在Chrome里调试特定机型的屏幕效果做前端的人大概都撞过这类场景&#xff1a;设计稿明明切得完美无缺&#xff0c;代码在电脑上怎么看都正常&#xff0c;结果测试或者客户甩过来一张截图&#xff0c;说在某某安卓机上页面错乱了&#xff0c;文字溢出、按钮错位、背景…

作者头像 李华
网站建设 2026/9/18 1:53:47

Chrome视频加速全攻略:从控制台到扩展,彻底告别播放器倍速限制

平时刷视频最烦什么&#xff1f;在线课程老师讲话太慢&#xff0c;明明会了还要等进度条&#xff1b;纪录片铺垫太长&#xff0c;就想看个关键结论&#xff1b;回看比赛集锦&#xff0c;前摇后摇都是广告。你可能会说&#xff0c;网站自带倍速播放啊&#xff0c;可很多平台的倍…

作者头像 李华
网站建设 2026/9/18 1:49:42

VimWiki Markdown语法配置:让.md文件直接成为Wiki页面

VimWiki Markdown语法配置&#xff1a;让.md文件直接成为Wiki页面 【免费下载链接】vimwiki Personal Wiki for Vim 项目地址: https://gitcode.com/GitHub_Trending/vi/vimwiki VimWiki 是 Vim 里的一款个人 Wiki 插件&#xff0c;而它的 Markdown 语法配置 功能&#…

作者头像 李华
网站建设 2026/9/18 1:49:39

所有权跟随组织单元,TaoToken 只管 Key 和 Base URL

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 1:47:23

点错一个菜单会卡在哪?Label Studio 路由守卫拆解

点错一个菜单会卡在哪&#xff1f;Label Studio 路由守卫拆解 【免费下载链接】label-studio Label Studio is a multi-type data labeling and annotation tool with standardized output format 项目地址: https://gitcode.com/GitHub_Trending/la/label-studio 你在左…

作者头像 李华
网站建设 2026/9/18 1:46:48

单线与多线固态激光雷达怎么选?线数、角分辨率与场景匹配

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华