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

📅 发布时间:2026/9/18 1:54:29
慢性阻塞性肺病病历.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.2L、FEV1%pred 42%用药吸入布地奈德/福莫特罗、噻托溴铵、ICS/LABA因为词汇在封闭集合内规则抽取在这里比通用 NER 模型更可靠。常见做法是先用正则把候选片段捞出来再做上下文校验避免把别的数值错当成肺功能结果。3.2 诊断相关正则规则与代码实现下面是抽取诊断与加重状态的规则示例import re TEXT 患者男性68岁确诊COPD 5年。本次因咳嗽、咳痰加重伴发热2天入院。 查体双肺呼吸音低可闻及湿啰音。 肺功能FEV1 1.2LFEV1/FVC 58%。 用药吸入布地奈德/福莫特罗每日两次。 def extract_diagnosis(text): rules { copd: rCOPD|慢性阻塞性肺疾病|慢性阻塞性肺病|慢阻肺, 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: rFEV1\s*[:]?\s*([\d.])\s*(L|升|%), fev1_fvc_ratio: rFEV1/FVC\s*[:]?\s*([\d.])\s*%, fev1_pred: rFEV1\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后按时间顺序排列。一个常见误用是直接写rFEV1\s*([\d.])这会把FEV1/FVC 58%里的58错误捕获成 FEV1 的数值。避免方法就是让单位参与匹配并在必要时添加负向前瞻排除/FVC的出现。提示碰到FEV1 1.2L和FEV1%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 [ rCOPD|慢性阻塞性肺疾病|慢性阻塞性肺病|慢阻肺, 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(rFEV1/FVC\s*[:]?\s*([\d.])\s*%, text, re.IGNORECASE) if m: entities[fev1_fvc_ratio] float(m.group(1)) m re.search(rFEV1\s*[:]?\s*([\d.])\s*(L|升), text, re.IGNORECASE) if m: entities[fev1] {value: float(m.group(1)), unit: m.group(2)} m re.search(rGOLD\s*([1-4])\s*级, text, re.IGNORECASE) if m: entities[gold_stage] fGOLD {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, encodingutf-8) as f: json.dump(result, f, ensure_asciiFalse, indent2) print(json.dumps(result, ensure_asciiFalse, indent2))这段代码的核心逻辑分三层清洗、段落合并、实体抽取。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 L和1200 ml。要不要统一单位取决于后续分析需求如果统计 FEV1 占预计值百分比建议在结构化阶段就统一为 L降低下游使用成本。坑四GOLD 分级与加重状态互斥规则。部分病历里同时出现“GOLD 3 级”和“稳定期”这时候优先保留分级加重状态以最近一次门诊记录为准。规则抽取应该同时输出“抽到了什么”和“原文位置”方便复核时聚焦冲突点。验证阶段建议把解析成功的样本和未命中的样本分开存放定期回看未命中样本。新增正则时先跑回回归测试防止修复 A 字段时破坏 B 字段的匹配。慢性阻塞性肺病病历的解析难点从来不在深度而在细节一份能稳定复现、能看到失败样本的处理管线比一个单独调到 99% 的规则函数更有长期价值。本文还有配套的精品资源点击获取