4. 电子病历数据读取与解析
电子病历(EMR/EHR)数据融合的第一步,是把医院信息系统里以不同标准存储的临床文档“读进来、拆开、变成程序能处理的结构”。在医疗信息化领域,HL7 家族标准是绕不开的基础:HL7 v2 以竖线分隔的段(Segment)传递消息,CDA(Clinical Document Architecture)以 XML 承载完整临床文档,FHIR(Fast Healthcare Interoperability Resources)则用 RESTful 风格的资源(Resource)和 JSON/XML 表示数据。本节聚焦于用 Python 解析这三类格式,重点放在 XML 与 JSON 的实际读取与字段提取上。
三种标准的格式特征与解析入口
HL7 v2 消息不是 XML,而是以回车分隔段、以竖线分隔字段的文本,例如 MSH|^~\&|HIS|...,解析时通常按段名(MSH、PID、OBX 等)和字段序号定位。CDA 是严格的 XML 文档,根元素为 ClinicalDocument,通过命名空间和层级结构组织 recordTarget、component/structuredBody 等节点。FHIR 资源既可以是 JSON 也可以是 XML,一个 Patient 资源在 JSON 中表现为带 resourceType 的对象,在 XML 中则表现为同名根元素。理解“分隔文本 / XML 树 / JSON 对象”这三种数据形态,是选择解析工具的前提。
用 Python 解析 HL7 v2 消息
HL7 v2 消息的解析核心是“先分段、再分字段”。下面示例假设一条 ADT 消息,包含患者标识与一个检验结果段。示例数据为教学构造,非真实患者数据。
raw = "MSH|^~\\&|HIS|HOSP|EMR|LAB|202405101200||ADT^A01|MSG0001|P|2.5\r" \
"PID|1||P2024001^^^HOSP^MR||张三||19850312|M\r" \
"OBX|1|NM|GLU^血糖^L||6.8|mmol/L|3.9-6.1|H|||F"
segments = [s for s in raw.split("\r") if s]
msg = {}
for seg in segments:
fields = seg.split("|")
msg[fields[0]] = fields
pid = msg["PID"]
patient_id = pid[3].split("^")[0] # P2024001
name = pid[5] # 张三
gender = pid[8] # M
obx = msg["OBX"]
code = obx[3].split("^")[0] # GLU
value = float(obx[5]) # 6.8
unit = obx[6] # mmol/L
flag = obx[8] # H(偏高)
这里的关键判断是:PID-3 是复合字段,用 ^ 再拆分才能拿到患者 ID;OBX-8 的 H 表示结果高于参考范围,是后续质控与告警的重要信号。若字段缺失,应保留 None 而不是抛异常,避免一条脏数据中断整批解析。
用 ElementTree 解析 CDA 文档
CDA 是 XML,解析时先注册命名空间,再用 XPath 或逐层遍历定位节点。以下示例从 CDA 中提取患者姓名与一条检验结果,示例文档为教学构造。
import xml.etree.ElementTree as ET
tree = ET.parse("cda_sample.xml")
root = tree.getroot()
ns = {"cda": "urn:hl7-org:v3"}
# 患者姓名
name_el = root.find(".//cda:recordTarget/cda:patientRole/"
"cda:patient/cda:name/cda:given", ns)
patient_name = name_el.text if name_el is not None else None
# 检验结果:遍历 observation 节点
results = []
for obs in root.findall(".//cda:observation", ns):
code_el = obs.find("cda:code", ns)
value_el = obs.find("cda:value", ns)
if code_el is not None and value_el is not None:
results.append({
"code": code_el.get("code"),
"display": code_el.get("displayName"),
"value": value_el.get("value"),
"unit": value_el.get("unit"),
})
CDA 的坑主要在命名空间:不注册 urn:hl7-org:v3,find 会返回空。另外,value 常以属性形式携带数值和单位,而不是文本节点,需要读 get("value") 与 get("unit")。若文档较大,建议用 lxml 的 iterparse 流式处理,降低内存占用。
用 json 模块解析 FHIR 资源
FHIR 的 JSON 资源结构清晰,解析时先看 resourceType,再按资源定义取字段。下面示例解析一个 Patient 和一个 Observation。
import json
with open("fhir_bundle.json", encoding="utf-8") as f:
bundle = json.load(f)
for entry in bundle.get("entry", []):
res = entry["resource"]
rtype = res["resourceType"]
if rtype == "Patient":
pid = res["id"]
name = res["name"][0].get("text") or \
"".join(res["name"][0].get("given", []))
gender = res.get("gender")
birth = res.get("birthDate")
elif rtype == "Observation":
code = res["code"]["coding"][0]["code"]
value = res.get("valueQuantity", {}).get("value")
unit = res.get("valueQuantity", {}).get("unit")
effective = res.get("effectiveDateTime")
FHIR 的字段多为可选,取值时用 .get() 并给默认值,能显著减少 KeyError。若需要严格校验,可引入 fhir.resources 库,把字典实例化为 Patient、Observation 对象,让模型层帮忙检查必填字段与类型。
解析结果的统一与异常处理
三种格式解析完后,应映射到同一套内部字段,例如 patient_id、name、gender、birth_date、observation_code、value、unit、effective_time。映射时注意单位换算(如 mg/dL 与 mmol/L)和时间格式统一(ISO 8601)。异常处理建议分三层:文件读取失败、格式解析失败、字段缺失。前两层记录日志并跳过当前文档,第三层用默认值填充并标记数据质量,供后续融合阶段参考。