OpenMed HL7 v2 叙述文本提取:将 ADT/ORU/ORM 消息转为去标识化临床 NLP 文本并保留字段溯源
2026/9/18 13:57:17 网站建设 项目流程

OpenMed HL7 v2 叙述文本提取:将 ADT/ORU/ORM 消息转为去标识化临床 NLP 文本并保留字段溯源

【免费下载链接】openmedLocal-first healthcare AI: clinical NER & HIPAA PII de-identification that runs 100% on-device. 2,200+ medical models, 21 languages, Apple MLX + Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmed

HL7 v2 管道式消息(pipe-delimited message)是医院信息系统中最常见的互联格式,但 ADT、ORU、ORM 这类消息的字段结构对下游 NLP 或人工审阅并不友好,且其中携带大量 PHI。本文讲解 OpenMed 提供的extract_hl7v2_narrative叙述提取器:它复用仓库既有的 HL7 v2 解析器与结构化脱敏层,把常见 ADT、ORU、ORM 消息渲染为可读、已去标识化的叙事文本(narrative),同时为最终文本中的每个可见值保留精确的字符区间与 HL7 坐标溯源(segment 出现位置 + 字段号)。读完本文,你将掌握该 API 的两种渲染模式、双层本地隐私管线、全部可调参数,以及基于最终文本偏移量做溯源查询的完整用法。

设计定位:不是第二个解析器,而是既有脱敏链路的叙述化视图

叙述提取器在架构上明确选择“复用而非重写”:它构建在 HL7 v2 解析器与结构化脱敏器(openmed/interop/hl7v2.py)之上,而不是独立实现一套 HL7 解析逻辑,也不做完整的 HL7 一致性校验(conformance validation)。从源码可以看到,extract_hl7v2_narrative内部依次调用:

  1. redact_hl7v2(...):按规则处理结构化字段(患者标识、姓名、日期、地址、电话等);
  2. parse_hl7v2(...):解析脱敏后的消息;
  3. 文本级 PII 管线:把完整渲染文本再统一过一遍openmed.core.pii.deidentify(默认method="mask")。

这样设计的好处是职责清晰:结构化字段的规则化脱敏与自由文本的 PII 掩盖各司其职,叙述提取器只负责“排版 + 溯源投影”,因此也不存在第二套解析器带来的行为漂移。

快速开始

入口函数位于 openmed/interop/hl7v2_narrative.py,接受消息文本、UTF-8 文件路径或已解析的HL7Message对象三种输入:

from openmed.interop.hl7v2_narrative import extract_hl7v2_narrative result = extract_hl7v2_narrative("synthetic_oru.hl7") print(result.text) for span in result.spans_for("OBX", 5): print(span.source.path, result.text_for(span))

其中:

  • result.text是去标识化后的完整叙述文本;
  • result.spans_for("OBX", 5)返回所有来自 OBX 段第 5 字段(观察值)的区间;
  • result.text_for(span)取出该区间在最终文本中的实际可见内容。

关于坐标约定,源码中有明确注释:HL7V2FieldSource.segment_index是段在整条消息中的零基位置,而segment_occurrence是同一三字母段名的一基出现序号path属性拼接出的紧凑路径如OBX[2]-5,含义是“第二条 OBX 段的第 5 字段”。这一点在测试tests/unit/interop/test_hl7v2_narrative.py中有直接断言:OBX出现在消息第 4 个位置(segment_index == 3),其第一次出现即OBX[1]-5

值得强调的是,溯源记录里只保留偏移量、固定标签和 HL7 坐标,绝不保留原始字段值——这是脱敏设计的安全底线,任何原始 PHI 都只存在于处理前的中间态。

两种叙事模式:flat 与 sectioned

模式由mode参数控制,可选值限定为"flat""sectioned"(源码中以Literal["flat", "sectioned"]约束,传入其他值会立即抛出ValueError,测试test_invalid_mode_is_rejected_before_processing验证了这一点)。

flat 模式(默认)

输出紧凑的、句子式文本,适合直接喂给下游 NLP:

flat = extract_hl7v2_narrative(message, mode="flat")

渲染规则(见_render_items):各条目标以label: value形式输出,条目之间以单个空格连接;若值的末尾不是.!?之一,自动补一个句号;不同 section 之间也以空格衔接。例如对 ORU 消息渲染出的文本形如:

Message type: ORU R01. Observation 1: Clinical note (NOTE). Result 1: Patient [PERSON] called from [PHONE] about [ID_NUM]. Observation 2: Glucose (GLU). Result 2: 7.1. Units 2: mmol/L. Note 1: Follow-up email [EMAIL] belongs to [PERSON].

sectioned 模式

输出稳定的 Markdown 风格小标题,每条字段单独一行,适合人工审阅界面:

sectioned = extract_hl7v2_narrative(message, mode="sectioned") print(sectioned.text)

典型 section 固定为MessagePatientEncounterOrdersObservationsNotes(源码中的_SECTION_ORDER常量),空 section 会被整体省略;两个模式都保证 section 内保持消息原始顺序,并可通过result.sections拿到每个 section 在最终文本中的start/end偏移。sectioned 模式输出的头部形如:

## Message Message type: ADT A01 HL7 version: 2.5 ## Patient Patient ID: [ID_NUM] Patient name: [PERSON] ...

测试test_sectioned_adt_has_stable_patient_and_encounter_sections验证了 sectioned 输出对同一输入是完全确定性的(两次调用结果相等),且result.sections中的每个区间都能在result.text中切出以## section名开头的真实内容。

双层本地隐私管线

叙述提取包含两层完全在本机执行的隐私处理:

  1. 结构化层openmed.interop.hl7v2.redact_hl7v2按配置处理结构化字段——患者标识、姓名、日期、地址、电话号码等。默认字段映射(DEFAULT_FIELD_MAP)覆盖PIDPD1NK1GT1IN1IN2OBXNTE等段的常见直接标识字段,动作包括clear(清空)、hash(确定性哈希令牌)、surrogate(标签感知的假值)、date-shift(统一日期偏移)、redact_text(自由文本走 PII 管线)。其中OBX-5仅当OBX-2TX/FTDEFAULT_NOTE_VALUE_TYPES)时才作为自由文本处理。
  2. 叙述层:完整渲染后的叙述文本再整体过一次openmed.core.pii.deidentify,默认method="mask"。这一层兜底覆盖自由文本与任何其他渲染值——包括结构化层未覆盖的字段,保证返回前所有内容都已脱敏。

一个关键实现细节是:在结构化脱敏阶段,叙述提取器故意把自由文本钩子设为恒等函数_identity_deidentifier),让OBX/NTE的原文先保留到叙述层,再由第二遍管线统一处理。测试test_deidentifier_receives_complete_narrative_once证实:完整叙述文本只经过一次最终脱敏调用。

偏移量重投影:脱敏后溯源依然精确

第二遍脱敏可能改变文本长度(例如Jane Roe变成[PERSON]),因此提取器在两次处理之间用difflib.SequenceMatcher计算新旧文本的 opcodes,再把每个字段区间和 section 区间投影(project)到最终文本上(见_project_range/_project_boundary)。这意味着spanssections里的所有偏移量都索引最终返回的去标识化文本,即使占位符改变了长度也依然成立;投影后空区间(start == end)会被过滤掉。

自定义最终文本管线

deidentify_kwargs配置叙述层的最终文本管线。为了支持确定性的离线测试,可以传入一个可调用对象,它返回字符串,或返回带deidentified_text属性的对象/映射:

result = extract_hl7v2_narrative( message, deidentify_kwargs={"policy": "hipaa_safe_harbor"}, )
def fake_deidentifier(text: str, **kwargs): return text.replace("Jane Roe", "[PERSON]") # 返回 str def fake_deidentifier_obj(text: str, **kwargs): return {"deidentified_text": text} # 返回映射

_deidentified_text会依次尝试字符串、含deidentified_text键的映射、含deidentified_text属性的对象;三种形态都不满足时抛出TypeError。测试中使用的fake_deidentifier就是返回SimpleNamespace(deidentified_text=...)的典型离线替身。

参数详解

extract_hl7v2_narrative的完整签名(以源码为准):

def extract_hl7v2_narrative( message_or_path: str | Path | HL7Message, *, mode: NarrativeMode = "flat", field_map: Mapping[FieldKey | str, Any] | None = None, deidentifier: TextDeidentifier | None = None, deidentify_kwargs: Mapping[str, Any] | None = None, date_shift_days: int = 30, lang: str = "en", locale: str | None = None, seed: int | None = 0, ) -> HL7V2Narrative
参数默认值作用
mode"flat""flat"(句子式)或"sectioned"(Markdown 小节),非法值抛ValueError
field_mapNone(使用DEFAULT_FIELD_MAP转发给redact_hl7v2的结构化脱敏规则,键可用("PID", 5)"PID-5"两种写法
deidentifieropenmed.core.pii.deidentify叙述层脱敏函数;离线测试可传确定性的替身
deidentify_kwargs{}叙述层脱敏的额外关键字参数,默认并入method="mask"lang
date_shift_days30结构化层统一的日期偏移天数;默认 30 天,多次运行结果稳定(seed恒定时)
lang"en"结构化脱敏与叙述脱敏共享的语言
localeNone结构化假值(surrogate)的可选 Faker locale
seed0结构化假值生成的确定性种子

date_shift_dayslanglocaleseed与可选的field_map都会转发给既有 HL7 脱敏层。注意两点:默认 30 天偏移在多次运行间稳定;而叙述中的日期(如 OBR 的请求时间、OBX 的观察时间)仍会随完整叙述一起经过最终隐私管线。在redact_hl7v2层不传date_shift_days时则会随机选择非零偏移(_random_nonzero_shift从 ±1~365 中随机取),所以叙述提取器显式给 30 天默认值正是为了可复现。

溯源查询:从最终文本反查原始坐标

叙述结果HL7V2Narrative提供三组查询入口:

offset = result.text.index("7.1") for span in result.provenance_at(offset): print(span.source.segment) # OBX print(span.source.field_position) # 5 print(span.source.path) # OBX[2]-5
  • provenance_at(offset):返回覆盖该最终文本偏移量的所有HL7V2FieldSpan(区间端点start <= offset < end,即左闭右开;越界返回空元组);
  • spans_for(segment, field_position, *, segment_occurrence=None):按段名/字段号(可加出现序号)反查区间;段名会自动转大写;
  • text_for(span):取出区间对应的最终文本内容;
  • spans与显式别名field_mappings:等价的完整区间元组(测试断言两者恒等)。

HL7V2FieldSpan每个区间提供:

  • start/end:最终叙述文本中的偏移,end为排他端点;
  • section/label:固定的叙述上下文(如Observations/Result 2);
  • source.segment:三字母段名;
  • source.segment_index:该段在完整消息中的零基位置;
  • source.segment_occurrence:同名段的一基出现序号;
  • source.field_position:一基 HL7 字段号。

测试里result.text.index("7.1")得到的偏移,其溯源结果恰好是[("OBX[2]-5", "Result 2")],即“第二条 OBX 的第 5 字段”,与文档中OBX[2]-5的语义完全一致。

支持的渲染范围

渲染器覆盖 ADT、ORU、ORM 流程中最常用的上下文,具体到“段 → 渲染字段”的对应关系如下(实现见_collect_items):

SegmentRendered fields
MSH消息类型(_message_code,取前两个组件如ORU R01)与 HL7 版本(MSH-12
EVN事件类型(EVN-1)与事件记录时间(EVN-2,日期格式化)
PID患者 ID(PID-3)、姓名(PID-5,XPN 组件重排)、出生日期(PID-7)、管理性别(PID-8
PV1患者类别(PV1-2)、就诊位置(PV1-3,组件拼接)、主治医生(PV1-7)、入院/出院时间(PV1-44/PV1-45
ORC医嘱控制(ORC-1)、placer/filler 医嘱号(ORC-2/ORC-3
OBR申请检查项目(OBR-4,编码组件渲染为显示名 (代码))与申请时间(OBR-7
OBX观察项目(OBX-3)、结果(OBX-5)、单位(OBX-6)、参考范围(OBX-7)、异常标志(OBX-8)、结果状态(OBX-11)、观察时间(OBX-14
NTE备注文本(NTE-3

数值型枚举字段会渲染为“标签 (代码)”形式,源码内置了多组标签映射:性别_SEX_LABELSA→Ambiguous、F→Female、M→Male、N→Not applicable、O→Other、U→Unknown)、患者类别_PATIENT_CLASS_LABELSE→Emergency、I→Inpatient、O→Outpatient、P→Preadmit、R→Recurring patient)、结果状态_RESULT_STATUS_LABELSC→Corrected、F→Final、I→Specimen in lab、P→Preliminary、R→Entered not verified、S→Partial、X→Cannot obtain)。这就是测试断言Administrative sex: Male (M)Patient class: Inpatient (I)的来源。

日期字段(HL7 的YYYYMMDD[HHMM[SS]]紧凑格式)会被格式化为YYYY-MM-DD HH:MM:SS风格;_decode_escapes负责还原 HL7 转义序列(\F\\S\\R\\T\\E\\.br\换行等),保证渲染出的文本可读。OBROBXNTE的多条记录会通过计数器(order_number/observation_number/note_number)生成Requested test 1Observation 2Note 1这样的稳定标签。

未知段(如Z开头的自定义段)不会被叙述渲染器渲染,但它们在底层解析器中仍然可见,并且可以被自定义field_map覆盖脱敏,再由最终叙述层兜底。另外,文档与实现都明确:把消息映射为 FHIR 资源不属于本工具的职责范围。

实测样例与测试依据

仓库提供了两条真实可跑的合成消息作为固定测试夹具:

  • tests/unit/interop/fixtures/synthetic_phi_oru.hl7:ORU^R01 消息,含自由文本型OBX|1|TX|NOTE^Clinical note^L||Patient Jane Roe called from 555-0199 about MRN67890.、数值型OBX|2|NM|GLU^Glucose^L||7.1|mmol/L以及NTE备注;
  • tests/unit/interop/fixtures/synthetic_phi_adt.hl7:ADT^A01 消息,含EVNPIDDOE^JOHN^A19800101)、NK1PV1I患者类别、ER^01^01位置)。

单元测试 tests/unit/interop/test_hl7v2_narrative.py 覆盖了本主题的全部关键行为:

  • test_flat_oru_is_coherent_safe_and_maps_results_to_source_fields:flat 模式输出连贯、Jane Roe/邮箱/MRN67890全部消失、spans_for("OBX", 5)溯源到OBX[1]-5provenance_at精确命中OBX[2]-5、文本中不会出现..双句号;
  • test_sectioned_adt_has_stable_patient_and_encounter_sections:sectioned 输出确定性稳定、日期被统一偏移(1980-01-011980-01-31,即 +30 天默认偏移)、section 顺序与区间正确;
  • test_deidentifier_receives_complete_narrative_once:完整叙述只经过一次最终脱敏;
  • test_all_field_mappings_index_nonempty_final_text:所有field_mappings区间均为非空且落在最终文本范围内,field_mappingsspans恒等;
  • test_parsed_message_and_mapping_result_are_supported:直接传入HL7Message对象亦可;
  • test_extractor_calls_public_hl7_parser:通过 monkeypatch 证实提取器只调用公开的parse_hl7v2,没有第二套解析逻辑;
  • test_invalid_mode_is_rejected_before_processing:非法模式在真正处理前即被拒绝。

适用边界

  • 该工具是纯本地、机械式的处理链路:不启动 MLLP 监听、不做完整 HL7 一致性校验、不调用任何网络服务,符合 OpenMed“患者数据不出网络”的定位;
  • 结构化脱敏 + 叙述脱敏的双层设计保证了返回文本中不残留原始字段值,但若你的工作流需要更强的统计匿名性(如 k-匿名/重识别风险评估),请结合仓库的 重识别风险 相关文档进一步评估;
  • 若你需要结构化输出(而非叙述文本),或需要把脱敏结果映射为 FHIR 资源,请使用 HL7 v2 结构化脱敏 及 FHIR 相关集成文档,而不是在本工具范围内扩展。

综合来看,extract_hl7v2_narrative的价值在于把“HL7 管道消息 → 可读叙述 + 精确字段溯源”这条链路封装成一次调用:结构化脱敏、叙述渲染、文本 PII 掩盖、偏移量重投影四步无缝衔接,最终交付的既是适合临床 NLP/人工审阅的安全文本,又是可编程查询的溯源数据模型。

【免费下载链接】openmedLocal-first healthcare AI: clinical NER & HIPAA PII de-identification that runs 100% on-device. 2,200+ medical models, 21 languages, Apple MLX + Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmed

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询