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 消息 竖线分隔文本 MSH | PID | OBX 按段名+字段序号定位 CDA 文档 XML 树 ClinicalDocument 命名空间 + 层级节点 FHIR 资源 JSON / XML Patient / Observation resourceType 驱动 Python 解析工具层 字符串 split / 正则 hl7apy 库 xml.etree.ElementTree lxml + XPath json.loads / json.load fhir.resources 校验 统一内部结构(融合层输入) patient_id · name · gender · birth_date observation_code · value · unit · effective_time 字段名与类型统一,供后续去重、对齐与融合使用

用 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)。异常处理建议分三层:文件读取失败、格式解析失败、字段缺失。前两层记录日志并跳过当前文档,第三层用默认值填充并标记数据质量,供后续融合阶段参考。

实践建议:先用小样本(1 条 HL7、1 份 CDA、1 个 FHIR Bundle)跑通解析,确认字段路径正确后,再批量处理。解析函数保持“输入原始文本/文件、输出统一字典”的纯函数风格,便于单元测试与复用。