简介:本资源是一份面向医学信息处理开发者、临床科研人员及健康大数据初学者的CMID临床医学数据JSON样本文件,聚焦于结构化医学数据提取与标准化交换场景。资源提供1个完整可用的CMID.json文件(1.05MB),采用标准JSON格式组织患者基本信息、病史、检验结果等多层级临床字段,键值清晰、嵌套合理,可直接用于解析测试、接口模拟或教学演示。文件已打包为zip压缩包,结构简洁,无冗余内容,便于快速导入Python/JavaScript等主流语言环境进行读取、验证与二次开发。目前已有170人学习下载,适合需要真实医学数据样例开展JSON解析实践、构建轻量级医疗数据中间件或理解CMID数据建模逻辑的学习者。
1. CMID医学数据提取:不是通用JSON解析器,而是专为临床文本结构化设计的轻量级转换工具
你手头有一批CMID格式的医学数据文件——可能是电子病历导出片段、检验报告原始日志、或某院内系统API返回的嵌套响应体。它们长得像JSON,但字段命名不规范(比如"patiName"和"patient_name"混用)、存在空值占位符("null"字符串而非JSONnull)、甚至夹杂HTML标签或Base64编码的影像元数据。这时候,用jq硬切、用Pythonjson.loads()直接解析,十有八九会报JSONDecodeError: Expecting property name enclosed in double quotes,或者解析出来一堆None和乱码。CMID医学数据提取工具就是为这种“伪JSON”场景而生:它不追求标准兼容性,而是内置了23条医学领域专用清洗规则(如自动补全缺失的"visitDate"字段、将"age":"35Y"标准化为整数35、剥离<br>标签但保留换行语义),把脏乱的CMID原始输出,稳稳落地成可被Pandas读取、能进SQL表、能喂给BERT微调的干净JSON对象。适合医院信息科工程师、临床研究助理、以及正在做真实世界研究(RWS)但被数据预处理卡住3天以上的算法同学——它解决的不是“怎么读JSON”,而是“怎么让医学数据在JSON容器里真正可用”。
2. CMID数据结构解析与清洗逻辑:为什么不能直接用标准JSON库?
CMID(Clinical Medical Information Dictionary)并非ISO标准,而是国内多家三甲医院联合制定的临床数据交换内部规范。其核心矛盾在于:语义优先,语法妥协。标准JSON要求严格双引号、无尾逗号、纯UTF-8;而CMID允许单引号包裹字段名、容忍末尾逗号、接受GBK编码的中文键名,甚至在"diagnosisList"数组中混入字符串"N/A"而非null。直接丢给json.loads(),等于让一个只认交通信号灯的自动驾驶车开进没有标线的乡村小路。
2.1 CMID典型脏数据模式与清洗映射表
下表列出CMID原始数据中最常出现的5类非标结构,以及本工具内置的对应清洗策略。这些规则全部硬编码在cmid_cleaner.py的_apply_medical_rules()方法中,不依赖外部配置:
| 原始数据片段 | 问题类型 | 工具清洗动作 | 清洗后效果 | 临床意义 |
|---|---|---|---|---|
"birthDay": "1985-03-12 " | 字符串尾部空格 | strip()+ 格式校验 | "birthDay": "1985-03-12" | 避免日期比较时因空格导致"1985-03-12 " != "1985-03-12" |
"allergy": "青霉素,头孢类" | 中文逗号分隔字符串 | 拆分为["青霉素", "头孢类"] | "allergy": ["青霉素", "头孢类"] | 使过敏史支持多值查询(如"头孢类" in data["allergy"]) |
"labResult": "7.2" | 数值型字段存为字符串 | 尝试转float,失败则保留原字符串 | "labResult": 7.2 | 支持数值计算(如avg(labResult))而非字符串拼接 |
"reportHtml": "<p>镜下见: <b>腺体增生</b></p>" | HTML标签混入JSON值 | 提取纯文本(保留<br>换行) | "reportHtml": "镜下见: 腺体增生" | 降低NLP模型噪声,同时保留关键诊断词 |
"examTime": "2023/05/20 14:30" | 非ISO日期格式 | 统一转为"2023-05-20T14:30:00" | "examTime": "2023-05-20T14:30:00" | 适配FHIR标准时间字段,支持时序分析 |
提示:所有清洗动作均采用“安全降级”原则——若某字段无法按规则解析(如
"age": "未知"),工具不会抛异常,而是将其标记为{"_cmid_clean_error": "age: unknown value"}并保留原始键值,确保整条记录不丢失。这是临床数据不可丢弃性的底线。
2.2 核心清洗引擎:CmidJsonParser类设计与参数控制
工具主入口是CmidJsonParser类,其初始化参数直接决定清洗强度。以下代码块展示了如何根据数据质量选择策略:
from cmid_parser import CmidJsonParser # 场景1:数据质量较好(如新上线HIS系统导出),仅需基础清洗 parser_light = CmidJsonParser( strict_mode=False, # 关闭严格模式:容忍单引号、尾逗号 auto_convert_types=True, # 自动转数字/布尔值("true"→True, "123"→123) remove_html_tags=True, # 剥离HTML标签(默认开启) normalize_dates=True # 日期格式标准化(默认开启) ) # 场景2:老旧系统导出(含大量GBK编码、乱码字段),启用深度修复 parser_heavy = CmidJsonParser( strict_mode=True, # 强制重写为标准JSON语法(修复单引号等) encoding="gbk", # 显式声明源编码,避免decode错误 repair_malformed=True, # 启用启发式修复(如补全缺失的}) max_repair_depth=3 # 修复嵌套深度限制,防无限循环 )参数说明:
strict_mode=False是默认值,适用于80%的CMID数据;设为True时,工具会先用正则预处理原始字符串(如将'key': 'val'替换为"key": "val"),再交给json.loads(),代价是速度下降40%,但兼容性提升至99%。encoding参数必须显式指定。CMID数据常见编码有gbk、gb2312、utf-8-sig(带BOM的UTF-8)。若不指定,工具会尝试utf-8→gbk→latin-1三级fallback,但可能引入乱码。repair_malformed启用后,会调用jsonrepair库的轻量版(已打包进cmid_parser),对{ "test": "value", }这类末尾逗号进行静默修复,但不修复语法严重错误(如缺少引号的键名{ test: "value" }),此时仍会报错并提示具体位置。
2.3 从原始CMID文件到标准JSON的完整转换流程
假设你拿到一个名为cmid_raw_20240512.json的文件,内容是某患者10次门诊记录的混合体。执行以下步骤完成端到端转换:
# 步骤1:安装工具(已打包所有依赖,无需额外安装jsonrepair等) pip install cmid-parser==1.3.2 # 步骤2:命令行快速转换(推荐新手) cmid-parse --input cmid_raw_20240512.json \ --output clean_cmid.json \ --encoding gbk \ --repair # 步骤3:Python脚本精细控制(推荐生产环境) python convert_cmid.pyconvert_cmid.py内容如下:
import json from cmid_parser import CmidJsonParser # 初始化解析器:针对该批数据定制参数 parser = CmidJsonParser( encoding="gbk", repair_malformed=True, auto_convert_types=True, # 关键:启用临床术语标准化(将"心梗"映射为"急性心肌梗死") standardize_terms=True ) # 读取原始CMID文件(注意:用open()而非json.load()!) with open("cmid_raw_20240512.json", "r", encoding="gbk") as f: raw_content = f.read() # 读取原始字符串,保留所有脏字符 # 执行清洗与解析 try: clean_data = parser.parse(raw_content) # 返回标准Python dict/list # 验证:检查是否包含必需字段 assert "patientId" in clean_data, "缺失patientId字段" assert isinstance(clean_data.get("visitRecords"), list), "visitRecords应为列表" # 写入标准JSON文件(ensure_ascii=False保留中文) with open("clean_cmid.json", "w", encoding="utf-8") as f: json.dump(clean_data, f, ensure_ascii=False, indent=2) print("✅ 转换成功!共处理", len(clean_data.get("visitRecords", [])), "条就诊记录") except Exception as e: print("❌ 转换失败:", str(e)) # 输出错误上下文(行号+附近10字符) if hasattr(e, 'position'): print("错误位置:第", e.position // 100 + 1, "行附近")逻辑说明:
- 第7行
raw_content = f.read()是关键——必须读取原始字符串,因为json.load()会直接报错,而parser.parse()需要原始字节流做预处理。 - 第17行
assert是临床数据质量守门员:CMID规范强制要求patientId存在,visitRecords必须是数组。若断言失败,说明原始数据已损坏,需退回源头核查。 - 第23行
json.dump(..., ensure_ascii=False)确保中文不被转义为\u4f60\u597d,方便人工校验。
3. 常见问题排查:那些让临床数据工程师拍桌的5个CMID翻车现场
CMID数据清洗不是黑匣子,每个失败都有迹可循。以下是我在3家三甲医院部署时踩过的坑,按发生频率排序,每条都附带可复现的测试用例和绕过方案。
3.1 现象:UnicodeDecodeError: 'gbk' codec can't decode byte 0xad in position 1234
原因:文件实际是UTF-8编码,但BOM头被截断(常见于Excel另存为CSV再转JSON的中间产物),导致工具误判为GBK。0xad是UTF-8中""(软连字符)的首字节,在GBK中非法。
解决:
- 先用
file -i cmid_raw.json确认真实编码(Linux/macOS)或用Notepad++查看编码标识; - 若确认为UTF-8,强制指定
--encoding utf-8; - 若BOM缺失且无法确认,用
chardet库探测:import chardet with open("cmid_raw.json", "rb") as f: raw = f.read(10000) # 读前10KB足够探测 encoding = chardet.detect(raw)['encoding'] # 返回'utf-8'或'gbk' parser = CmidJsonParser(encoding=encoding)
3.2 现象:解析后"diagnosisCode"字段全为None,但原始文件中明明有"ICD10:J45.901"
原因:CMID规范中diagnosisCode字段名存在变体——老系统用"diagCode",新系统用"diagnosisCode",而工具默认只识别后者。字段名未标准化是CMID最大痛点。
解决:
- 启用字段名映射功能(v1.3.2新增):
parser = CmidJsonParser( field_mapping={ "diagCode": "diagnosisCode", "icd_code": "diagnosisCode", "dx_code": "diagnosisCode" } ) - 或在解析后手动归一化:
# 解析后遍历所有记录 for record in clean_data["visitRecords"]: if "diagCode" in record: record["diagnosisCode"] = record.pop("diagCode")
3.3 现象:"labItems"数组中部分项目解析为{"name": "血红蛋白", "value": "120g/L"},部分为{"name": "白细胞计数", "value": "6.5"},单位不统一
原因:CMID未强制要求单位字段,部分系统将单位写入value字符串,部分写入独立unit键。工具默认不拆解value字符串。
解决:
- 启用单位分离模式(需配合正则):
parser = CmidJsonParser( split_units=True, # 启用单位分离 unit_patterns=[r"(\d+\.?\d*)\s*(g/L|×10⁹/L|mmol/L|U/L)"] # 自定义正则 ) # 效果:{"value": "120g/L"} → {"value": 120.0, "unit": "g/L"} - 单位正则必须用原始字符串(
r""),否则\s会被转义。
3.4 现象:"prescriptionList"中药品名称含"阿莫西林克拉维酸钾(7:1)",但清洗后变成"阿莫西林克拉维酸钾(7"
原因:括号被误判为JSON结构符。CMID中大量使用中文括号()和英文括号(),而工具默认只处理英文括号,遇到"("会尝试匹配结束括号,导致截断。
解决:
- 禁用括号平衡检测(仅当确定无嵌套JSON时):
parser = CmidJsonParser( balance_parentheses=False # 关键!关闭括号配对检查 ) - 或升级到v1.4.0(已修复:对
(的处理改为仅在{或[后出现时才触发配对)。
3.5 现象:转换后文件体积暴涨3倍,打开发现大量"_cmid_clean_error"字段
原因:原始数据中存在大量"notes": "NULL"、"remark": "N/A"等占位字符串,工具将其识别为需修复的错误值,生成冗余错误标记。
解决:
- 预处理阶段批量替换占位符(推荐):
# Linux/macOS:用sed预清理 sed -i 's/"NULL"/null/g; s/"N\/A"/null/g; s/"--"/null/g' cmid_raw.json - 或在解析器中禁用占位符检测:
parser = CmidJsonParser( detect_placeholders=False # 不将"NULL"等视为错误 )
注意:
detect_placeholders=False会跳过所有占位符检查,若数据中真有"value": "NULL"表示“数值为空”,则会被当作有效字符串保留,需业务侧二次判断。
4. 多文件批量处理与临床数据管道集成:从单次转换到自动化流水线
单个JSON文件处理只是起点。真实场景中,你面对的是每月数百GB的CMID数据流——门诊日志按天切分、检验报告按设备编号归档、手术记录按主刀医生分类。手工逐个转换不可行,必须构建可审计、可重跑、可监控的管道。
4.1 基于Shell的医院级批量转换脚本
以下脚本已在某省人民医院稳定运行14个月,日均处理2.3TB CMID数据。核心设计原则:失败隔离、进度可见、结果可溯。
#!/bin/bash # batch_cmid_convert.sh INPUT_DIR="/data/cmid_raw/202405" # 原始数据目录 OUTPUT_DIR="/data/cmid_clean/202405" # 清洗后目录 LOG_DIR="/var/log/cmid_pipeline" # 日志目录 # 创建输出与日志目录 mkdir -p "$OUTPUT_DIR" "$LOG_DIR" # 记录开始时间与参数 echo "=== Pipeline Start $(date) ===" >> "$LOG_DIR/batch.log" echo "Input: $INPUT_DIR, Output: $OUTPUT_DIR" >> "$LOG_DIR/batch.log" # 遍历所有.json文件(排除临时文件) find "$INPUT_DIR" -name "*.json" ! -name ".*" | while read file; do # 提取相对路径,保持目录结构 rel_path="${file#$INPUT_DIR/}" output_file="$OUTPUT_DIR/$rel_path" # 创建输出子目录 mkdir -p "$(dirname "$output_file")" # 执行转换,捕获详细日志 echo "Processing: $rel_path" >> "$LOG_DIR/batch.log" if cmid-parse --input "$file" \ --output "$output_file" \ --encoding gbk \ --repair \ --verbose 2>> "$LOG_DIR/$(basename "$file").err.log"; then echo "✅ Success: $rel_path" >> "$LOG_DIR/batch.log" # 计算MD5校验和,用于数据完整性验证 md5sum "$output_file" >> "$LOG_DIR/checksums.md5" else echo "❌ Failed: $rel_path (see $(basename "$file").err.log)" >> "$LOG_DIR/batch.log" # 失败文件移动到隔离区,不阻塞后续任务 mv "$file" "$INPUT_DIR/_failed/" fi done echo "=== Pipeline End $(date) ===" >> "$LOG_DIR/batch.log"关键设计点:
! -name ".*"排除隐藏文件(如.DS_Store),避免cmid-parse误读;--verbose输出详细步骤(如“修复末尾逗号”、“标准化日期”),便于审计;- 失败文件移入
_failed/而非删除,保留原始证据; checksums.md5供下游系统校验数据一致性,例如Spark作业启动前执行md5sum -c checksums.md5。
4.2 与Spark数据湖的无缝对接:直接读取CMID清洗结果
清洗后的JSON文件天然适配Spark的spark.read.json(),但需注意两个CMID特有陷阱:
| 陷阱 | Spark默认行为 | CMID适配方案 | 代码示例 |
|---|---|---|---|
| 字段类型不一致 | 同一字段在不同文件中为string或int(如"age"在A文件是"35",B文件是35),Spark推断为string,导致df.select(avg("age"))报错 | 启用mergeSchema=true强制合并schema | spark.read.option("mergeSchema", "true").json("/data/cmid_clean/202405") |
| 嵌套过深 | CMID中"treatmentPlan"可能含5层嵌套,Spark默认maxRecursionDepth=20,超深结构被截断为null | 显式提高递归深度 | spark.read.option("maxRecursionDepth", "50").json(...) |
生产环境推荐的完整读取代码:
from pyspark.sql import SparkSession from pyspark.sql.types import * # 初始化Spark(关键:设置足够内存) spark = SparkSession.builder \ .appName("CMID-ETL") \ .config("spark.sql.adaptive.enabled", "true") \ .config("spark.sql.adaptive.coalescePartitions.enabled", "true") \ .getOrCreate() # 定义CMID专用schema(提升性能,避免推断) cmid_schema = StructType([ StructField("patientId", StringType(), False), StructField("visitDate", TimestampType(), True), StructField("diagnosisCode", ArrayType(StringType()), True), StructField("labResults", ArrayType( StructType([ StructField("item", StringType(), True), StructField("value", DoubleType(), True), StructField("unit", StringType(), True) ]) ), True) ]) # 读取清洗后的JSON(自动合并多文件schema) df = spark.read \ .option("multiLine", "true") \ # 支持换行JSON(CMID常见) .option("mergeSchema", "true") \ .option("maxRecursionDepth", "50") \ .schema(cmid_schema) \ .json("/data/cmid_clean/202405") # 验证:检查是否有null字段占比过高(数据质量问题信号) df.select([ (count(when(col(c).isNull(), c)) / count("*")).alias(f"{c}_null_rate") for c in ["patientId", "visitDate", "diagnosisCode"] ]).show()4.3 临床数据质量看板:用清洗日志反哺源头治理
清洗过程产生的日志不仅是排错依据,更是优化上游系统的金矿。我们用batch.log自动生成周度质量报告:
| 指标 | 计算方式 | 临床意义 | 目标阈值 |
|---|---|---|---|
| 字段缺失率 | grep "缺失patientId字段" batch.log | wc -l/ 总文件数 | 源系统必填字段未录入 | < 0.1% |
| 编码错误率 | grep "UnicodeDecodeError" batch.log | wc -l/ 总文件数 | 数据导出环节编码混乱 | 0 |
| 占位符滥用率 | grep '"NULL"' cmid_raw/*.json | wc -l/ 总行数 | 医生录入习惯问题(应选下拉框而非填"NULL") | < 5% |
| 日期格式违规率 | grep "examTime.*[0-9]\{4\}/[0-9]\{2\}/[0-9]\{2\}" batch.log | wc -l/ 总日期字段数 | HIS系统未强制日期控件 | < 1% |
提示:将此表嵌入医院BI系统,每周邮件发送给信息科主任和医务处。当"占位符滥用率"连续2周>8%,即触发流程:通知相关科室停止手填"NULL",改用系统预设的"未查"选项。这比写10页整改报告更有效。
5. 进阶技巧:用CMID清洗结果驱动真实世界研究(RWS)分析
清洗完成不是终点,而是临床研究的起点。CMID标准JSON的真正价值,在于它能让RWS分析从“手工扒表”升级为“代码可复现”。以下是我用清洗后数据支撑3个RWS项目的实战技巧。
5.1 构建患者队列:用JSONPath精准定位复杂条件
RWS第一步永远是定义队列。CMID清洗后,visitRecords数组天然支持JSONPath查询。例如,筛选“近6个月确诊2型糖尿病且使用胰岛素治疗”的患者:
import jsonpath_ng as jp import json # 加载清洗后的JSON with open("clean_cmid.json") as f: data = json.load(f) # JSONPath表达式(比SQL更直观描述嵌套逻辑) # $.visitRecords[?(@.diagnosisCode[?(@ =~ /E11.*/i)] && @.prescriptionList[?(@.drugName =~ /胰岛素/i)])] jsonpath_expr = jp.parse( "$.visitRecords[?(@.diagnosisCode[?(@ =~ /E11.*/i)] && " "@.prescriptionList[?(@.drugName =~ /胰岛素/i)])]" ) # 执行查询(返回匹配的visitRecords子集) matches = [match.value for match in jsonpath_expr.find(data)] print(f"找到 {len(matches)} 条符合条件的就诊记录") # 提取患者ID去重 patient_ids = list(set([m["patientId"] for m in matches])) print("目标患者ID:", patient_ids)为什么不用SQL?
- SQL需先
explode()展开visitRecords数组,再join回主表,代码冗长; - JSONPath直接在内存操作,毫秒级响应,适合交互式探索;
- 表达式
/E11.*/i支持ICD-10模糊匹配(E11.001、E11.9均命中),而SQL需写LIKE 'E11%'。
5.2 时间序列特征工程:从visitRecords生成动态指标
CMID的核心优势是保留完整时序。以下代码将某患者所有就诊记录,转化为可用于LSTM的时序特征矩阵:
import pandas as pd from datetime import datetime def build_timeseries_features(patient_data): """输入:清洗后的patient_data dict 输出:DataFrame,每行=一次就诊,列=时序特征""" records = patient_data.get("visitRecords", []) if not records: return pd.DataFrame() # 按visitDate排序(CMID不保证顺序) records.sort(key=lambda x: x.get("visitDate", "1970-01-01")) features = [] for i, rec in enumerate(records): # 基础字段 feat = { "visit_id": rec.get("visitId", ""), "days_since_first": (datetime.fromisoformat(rec["visitDate"]) - datetime.fromisoformat(records[0]["visitDate"])).days, "visit_order": i + 1, } # 实验室指标(取最近一次非空值,向前填充) labs = rec.get("labResults", []) for lab in labs: if lab.get("item") == "HbA1c" and lab.get("value"): feat["hba1c_latest"] = lab["value"] break # 用药状态(二值化) drugs = [d.get("drugName", "").lower() for d in rec.get("prescriptionList", [])] feat["on_insulin"] = 1 if any("胰岛素" in d for d in drugs) else 0 feat["on_metformin"] = 1 if any("二甲双胍" in d for d in drugs) else 0 features.append(feat) return pd.DataFrame(features) # 应用到所有患者 all_ts = [] for patient_file in ["p001.json", "p002.json"]: with open(patient_file) as f: data = json.load(f) ts_df = build_timeseries_features(data) ts_df["patient_id"] = data["patientId"] all_ts.append(ts_df) full_ts = pd.concat(all_ts, ignore_index=True) print(full_ts.head())关键技巧:
days_since_first将绝对日期转为相对天数,消除患者起始时间差异;hba1c_latest实现“最近一次有效值”逻辑,避免用NaN污染模型;on_insulin等字段为后续生存分析(如胰岛素起始时间对肾病进展的影响)提供暴露变量。
5.3 与大模型协同:用CMID JSON生成结构化临床摘要
清洗后的JSON是大模型的优质输入。我们用LLM将visitRecords压缩为一段符合《中国2型糖尿病防治指南》表述的摘要:
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM # 加载医疗领域微调的T5模型(如"uer/t5-base-finetuned-medical") tokenizer = AutoTokenizer.from_pretrained("uer/t5-base-finetuned-medical") model = AutoModelForSeq2SeqLM.from_pretrained("uer/t5-base-finetuned-medical") def generate_clinical_summary(patient_data): # 构造Prompt:明确指令+CMID结构示例 prompt = f"""请根据以下患者就诊记录,生成一段不超过200字的临床摘要,要求: 1. 按时间顺序描述关键事件 2. 突出诊断、重要检验结果、主要用药 3. 使用专业术语,避免口语化 就诊记录(JSON格式): {json.dumps(patient_data['visitRecords'][:3], ensure_ascii=False, indent=2)} """ inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=1024) outputs = model.generate(**inputs, max_length=256, num_beams=4, early_stopping=True) summary = tokenizer.decode(outputs[0], skip_special_tokens=True) return summary # 示例调用 with open("p001.json") as f: data = json.load(f) print(generate_clinical_summary(data)) # 输出:"患者2024年3月首次确诊2型糖尿病(HbA1c 9.2%),予二甲双胍治疗;5月随访HbA1c降至7.5%,加用甘精胰岛素;6月复查尿微量白蛋白/肌酐比值升高..."为什么有效?
- CMID JSON的结构化程度远高于自由文本,模型无需学习“从病历中找HbA1c”,直接定位
labResults数组; visitRecords[:3]限制输入长度,避免token超限,同时覆盖初诊、随访、调药关键节点;- Prompt中强调“按时间顺序”,利用CMID已排序的
visitRecords,模型无需自行推理时序。
从那以后我每次处理新一批CMID数据,都强制走一遍file -i探编码、head -c 1000看开头、grep -n "patientId"验必填字段——这三步花不了2分钟,却能避开80%的解析失败。真正的效率不是写更快的代码,而是用最笨的办法守住数据入口的质量底线。希望帮到你。
本文还有配套的精品资源,点击获取