4. 数据格式基础(下):HL7 v2.x消息结构、FHIR资源模型、DICOM文件结构、自定义二进制协议解析
好,咱们接着聊数据格式。上一节我们把基础概念过了一遍,这一节要动真格的了。HL7 v2.x、FHIR、DICOM,还有那些自定义的二进制协议——这些才是你在医院系统里天天要打交道的「硬骨头」。
我个人习惯把这类格式分成两类:一类是「文本可读型」,比如 HL7 和 FHIR,你拿记事本打开还能猜个大概意思;另一类是「二进制黑箱型」,比如 DICOM 和自定义协议,不借助工具你根本不知道里面是什么。咱们一个一个来拆。
4.1 HL7 v2.x 消息结构:医院里的「电报」
HL7 v2.x 是什么?说白了,它就是医院信息系统之间互相喊话的「暗号」。你想想看,HIS 要给 LIS 发一个检验申请,总不能说「喂,帮我查个血常规」吧?得有一套大家都懂的格式。
HL7 v2.x 的消息结构,核心就三个字:段、字段、组件。
- 段(Segment):每一行是一个段,以三个大写字母开头。比如 MSH 是消息头,PID 是患者信息,OBR 是医嘱信息。
- 字段(Field):段里面用竖线
|分隔的就是字段。比如 PID 段里,第 5 个字段通常是患者姓名。 - 组件(Component):字段里面用
^分隔的更小单位。比如姓名这个字段,可能拆成姓^名^中间名。
来,看个实际例子。这是我以前在集成平台上抓的一条真实消息(脱敏后的):
MSH|^~\&|HIS|XX医院|LIS|XX医院|202403121030||ORM^O01|123456|P|2.3
PID|1||10086||张三^张^^先生||19800101|M|||北京市海淀区^^^北京^100000
OBR|1||20240312001||血常规^CBC|||202403121030|||||||李医生
嗯,这里要注意:MSH 段第 2 个字段 ^~\& 是编码字符,它告诉解析器用什么符号来分隔组件、重复字段和转义字符。很多新手在这里栽跟头——解析出来的字段全是乱的,其实就是编码字符没配对。
^ 当成特殊字符过滤了。所以,传输 HL7 消息时,一定要确认编码字符没有被转义或过滤。
4.2 FHIR 资源模型:新一代的 RESTful 风格
FHIR 是 HL7 的新一代标准,它把医疗数据抽象成一个个「资源(Resource)」。说白了,就是把患者、医嘱、检查报告这些东西,都变成 JSON 或 XML 格式的 RESTful API 资源。
我个人觉得 FHIR 最大的好处是:前端开发人员终于能看懂了。以前 HL7 v2.x 那套竖线分隔符,前端小哥看了直摇头。FHIR 用 JSON,结构清晰,还有标准的 HTTP 方法(GET、POST、PUT、DELETE)。
举个例子,上面那条 HL7 v2.x 消息,如果用 FHIR 的 Patient 资源来表示,大概是这样的:
{
"resourceType": "Patient",
"id": "10086",
"name": [{
"use": "official",
"family": "张",
"given": ["张三"]
}],
"gender": "male",
"birthDate": "1980-01-01",
"address": [{
"city": "北京市",
"district": "海淀区",
"postalCode": "100000"
}]
}
你看,是不是直观多了?但这里有个坑:FHIR 虽然标准统一,但不同厂商的实现千差万别。我遇到过一家厂商,他们把患者姓名放在 name.text 字段里,而不是标准的 name.family 和 name.given。你按标准解析,拿到的就是空值。
4.3 DICOM 文件结构:医学影像的「集装箱」
DICOM 是医学影像领域的标准,CT、MRI、X光、超声,这些设备的输出格式都是 DICOM。它的文件结构,你可以想象成一个「集装箱」:
- 文件头(Preamble):前 128 个字节,通常是 0,用于兼容其他格式。紧接着是 4 个字节的 "DICM" 标识。
- 数据集(Data Set):由一个个数据元素(Data Element)组成。每个数据元素包含:标签(Tag)、值表示(VR)、值长度(VL)、值域(VF)。
标签是 DICOM 的核心,它是一个 4 字节的十六进制数,比如 (0010,0010) 表示患者姓名,(0008,0060) 表示检查部位。我刚开始学 DICOM 时,背这些标签背得头大。后来发现,其实不用背,记住几个常用的就行,其他的查 DICOM 标准字典。
来看一个 DICOM 文件头的解析示例(用 Python 的 pydicom 库):
import pydicom
ds = pydicom.dcmread("example.dcm")
print(ds.PatientName) # 输出:张三
print(ds.StudyDate) # 输出:20240312
print(ds.Modality) # 输出:CT
print(ds[0x0010, 0x0010]) # 通过标签访问,效果同上
4.4 自定义二进制协议解析:没有标准,就自己造
最后聊聊自定义二进制协议。这东西在医疗设备里太常见了——监护仪、呼吸机、输液泵,这些设备厂商往往有自己的私有协议。说白了,就是「没有标准,就自己造」。
解析自定义二进制协议,我总结了一套「三步法」:
- 找帧头帧尾:大多数协议会用固定的字节作为帧头(比如 0xAA、0x55)和帧尾(比如 0x0D、0x0A)。找到它们,你才能把一条完整的消息从数据流里切出来。
- 定长度和校验:帧头后面通常跟着数据长度字段,告诉你这条消息有多长。然后是校验字段(比如 CRC16、累加和),用来验证数据有没有被篡改。
- 解析载荷:根据协议文档,把载荷里的每个字节映射成具体的含义。比如第 5 个字节是心率,第 6-7 个字节是血压。
举个例子,假设某监护仪的协议格式如下:
帧头(1B) | 长度(2B) | 设备ID(1B) | 数据(可变) | 校验(1B) | 帧尾(1B)
0xAA | 0x000C | 0x01 | ... | 0x?? | 0x0D
用 Python 解析的代码片段:
import struct
def parse_monitor_data(raw_bytes):
if raw_bytes[0] != 0xAA:
return None # 帧头不对,丢弃
length = struct.unpack('>H', raw_bytes[1:3])[0]
device_id = raw_bytes[3]
data = raw_bytes[4:4+length-5] # 减去帧头、长度、设备ID、校验、帧尾
checksum = raw_bytes[4+length-5]
# 校验逻辑省略...
return {
'device_id': device_id,
'heart_rate': data[0], # 假设第1个字节是心率
'blood_pressure': data[1] # 假设第2个字节是血压
}
小结
这一节我们聊了四种数据格式:HL7 v2.x 的竖线分隔、FHIR 的 JSON 资源、DICOM 的标签结构、自定义二进制协议的帧格式。每种格式都有自己的脾气,但核心思路是一样的——搞清楚「数据是怎么组织的」,然后「按规则拆解」。
下一节,我们会把这些格式串起来,讲讲实际项目中如何做数据转换和映射。嗯,那才是真正考验功力的时候。