简介:本资源是浙江联众卫生信息科技有限公司发布的电子病历(EMR)核心表结构说明文档,面向ZJHIS、EMR3、EMR及EMRJK用户,以及需要理解医院信息系统数据结构的开发、实施与维护人员。文档经占贵顺、葛向东多次修订,覆盖患者分类、科室病区、职工用户、系统参数、医疗收费等关键表,并细化到字段含义,可直接用于系统配置、数据维护与二次开发。包内共1个doc文件,压缩包大小约6.07MB,便于查阅与打印。目前已有349人学习,适合希望快速掌握电子病历库表关系、提升数据管理效率的医疗信息化从业者。
1. 联众电子病历表结构文档.doc:集成开发前,先读懂这张“数据地图”
第一次拿到《联众电子病历表结构文档.doc》时,别急着双击打开就翻“患者表”。我的习惯是反过来:先花半小时把文档目录扫一遍,凡是带“字典”“状态”“类型”字样的表先翻完,再回头看业务表。原因很简单——电子病历系统的表和字段命名并不直观,同一个患者标识在不同表里可能叫 PATIENT_ID、PAT_NO,甚至是一串拼音缩写,直接钻业务表很容易在几百个字段里迷路。这份文档要回答的无非三个问题:数据存在哪张表、字段怎么命名、状态怎么流转。它帮集成工程师写对接 SQL,帮实施人员做数据迁移映射,帮做互联互通测评的同事梳理数据元来源。适合正在做 HIS/EMR 对接、病历数据抽取或老系统升级迁移的从业者。这篇笔记就把我拿到这类文档后的处理顺序讲清楚:先建立分域认知,再拆核心表,最后落查询与排错。
2. 读懂表结构之前:先分清分域、主外键和时间戳
电子病历系统背后通常有两百张以上的表,但真正核心的业务表不超过三十张。拿到文档的第一件事不是逐表通读,而是给表分域。分域能力决定了你后面写 JOIN 的时候,能不能第一时间判断“这个字段该去哪张表找”。
2.1 电子病历数据模型的四个分域:患者、就诊、文书、医嘱
主流电子病历共享文档规范的数据组织方式,普遍围绕“一次就诊”展开,而不是围绕“一个患者”或者“一个科室”。两者的差别很大:围绕患者组织,所有数据堆在患者主索引下,表会越来越宽;围绕就诊组织,数据按就诊事件切分,语义更接近临床流程。常见的分法是把核心表归成四个域,外加一个支撑域。
患者主索引域解决“这个人是谁”,每次建档产生一条记录,生命周期极长;就诊域解决“这次来院发生了什么”,患者每住一次院、每看一次门诊,都会新增一条;临床文档域解决“医生写了什么”,包括首次病程、日常病程、出院小结、手术记录等;医嘱与申请域解决“开了什么、执行了什么”,包括长期医嘱、临时医嘱、检验申请、检查申请。支撑域是字典表,负责解释其他表里的代码字段。
这样拆的根本原因是生命周期不同。患者主索引一旦建立基本不变,就诊记录随每次就诊增加,文书存在草稿、签名、作废等版本状态,医嘱有时效性。如果混在一张宽表里,更新和一致性都是灾难。所以患者主索引表注定是“一人一条”,就诊表是“一次一条”,文书表则是“一次就诊多份文书”,层级关系非常清晰。理解这个分域,再去看文档目录,你就能快速判断一份表属于哪个域,而不是被表名前缀带着走。
2.2 文档里必须先读的三处:字段类型、主外键、索引说明
表结构文档通常按“表清单、字段明细、索引说明”三块组织。字段明细表是核心,行是字段,列是属性。我拿到后只看三列:字段说明、可空性、默认值。字段说明里一般会写枚举含义,比如状态字段注明“0 草稿、1 已签名、2 作废”;可空性决定你写查询时能不能信任这个字段;默认值则能帮你区分“没填”和“填了空值”。
为了更直观地说明这些列的用途,我一般会在心里过一张对应表:
| 文档列 | 回答什么问题 | 写 SQL 时怎么用 |
|---|---|---|
| 字段名 | 对接参数到底叫什么 | 做字段映射,别用中文备注当列名 |
| 类型/长度 | 值能不能塞进去 | 校验数据长度,避免写库截断 |
| 可空 | 历史数据是不是一定都有值 | 决定用 INNER JOIN 还是 LEFT JOIN |
| 默认值 | 不传的时候系统会给什么 | 区分“未填”与“真实空值” |
| 说明/枚举 | 状态字段的具体含义 | 写 CASE WHEN 做枚举归一化 |
还有一个很容易忽略的点:电子病历表很少用纯自增主键做主关联,常见做法是 PATIENT_ID + VISIT_ID + 业务序号做联合业务键。原因很简单,医疗数据经常要跨院区、跨系统合并,自增 ID 在合并时几乎必然冲突,业务键反而稳定。有些厂商还会同时保留物理主键(自增 ID)和业务唯一键两套,文档里只标“主键”,你得靠字段名里的 ID、NO、SEQ 后缀去判断哪个是真正的业务关联键。
2.3 用最小链路表快速建立表间脉络,别被两百张表吓住
两百张表里,至少一半是字典表、日志表、参数表。做集成时真正高频 JOIN 的业务表就是主索引、就诊、文书、医嘱、申请和报告这几张。所以拿到文档先找目录里带 DICT、DIC、ZD 字样的表群,这些是字典;再找带 LOG、JOURNAL 字样的,是日志。这两类先放一边,它们不参与业务主链路的 JOIN。
我习惯把必需的表压缩成一张“最小链路表”,先画主干,再补分支:
| 分域 | 最小表清单 | 关系基数 | 对应业务动作 |
|---|---|---|---|
| 患者主索引 | patient_master_index | 一人一条 | 建档、发卡 |
| 就诊 | visit_info | 一次一条 | 挂号、入出院 |
| 文书 | emr_document / emr_doc_section | 一次多条 | 病历书写、签名 |
| 医嘱 | medical_order | 一次多条 | 开立、停嘱、执行 |
| 申请与报告 | exam_apply / lab_result | 一次多条 | 开检查、回传报告 |
| 字典 | dict_dept / dict_doctor / dict_diagnosis | 固定少量 | 翻译代码字段 |
这张链路表建立之后,你再看文档里的任何一张业务表,都能在 30 秒内判断它挂在主链路的哪个位置。判断方法很简单:看这张表里有没有 patient_id 和 visit_id。两个都有,挂在就诊和文书之间;只有 patient_id,挂在患者主索引下的附属表;只有 visit_id,挂在就诊域下的子表。这个判断几乎不会出错。
3. 核心表结构与字段命名:从患者主索引到病历文书
分域建立之后,下一步就是逐域拆表。这一章我用三组典型的建表结构来说明字段设计逻辑。实际拿到的文档里表名可能是拼音缩写,字段也可能叫 BRXX、ZYXX 之类的名字,这不重要,关键看字段语义和关联关系能不能和你心里的标准模型对上。
3.1 患者主索引与就诊表:为什么每个业务表都要带 PATIENT_ID
电子病历表结构里有一条铁律:几乎所有业务表都会冗余 patient_id。按数据库范式来讲这属于冗余,但医疗场景必须这么做——因为患者可能跨科室、跨年份反复就诊,如果每次关联都要先查主索引表,不仅慢,而且一旦主索引被合并(同一个患者建了两个档),历史数据会跟着出问题。冗余患者标识后,按患者归档、按患者检索都只需要走单表。
下面是一份语义化后的建表结构,注意看字段类型和注释:
CREATE TABLE patient_master_index ( patient_id VARCHAR(32) NOT NULL COMMENT '患者主索引ID,全局唯一', id_type CHAR(2) NOT NULL COMMENT '证件类型:01身份证 02护照 03军官证', id_no VARCHAR(64) NOT NULL COMMENT '证件号码', patient_name VARCHAR(64) NOT NULL COMMENT '患者姓名', birth_date DATE NULL COMMENT '出生日期', gender CHAR(1) NOT NULL COMMENT '性别:1男 2女 9未知', create_time DATETIME NOT NULL COMMENT '建档时间', update_time DATETIME NULL COMMENT '最后更新时间', PRIMARY KEY (patient_id), UNIQUE KEY uk_id_type_no (id_type, id_no) ) COMMENT '患者主索引表'; CREATE TABLE visit_info ( visit_id VARCHAR(32) NOT NULL COMMENT '就诊ID,一次就诊唯一', patient_id VARCHAR(32) NOT NULL COMMENT '患者主索引ID', visit_type CHAR(1) NOT NULL COMMENT '就诊类型:1门诊 2住院 3急诊', visit_date DATETIME NOT NULL COMMENT '就诊时间', dept_code VARCHAR(16) NOT NULL COMMENT '就诊科室编码', doctor_no VARCHAR(16) NULL COMMENT '经治医师编号', in_time DATETIME NULL COMMENT '入院时间', out_time DATETIME NULL COMMENT '出院时间', status CHAR(1) NOT NULL DEFAULT '0' COMMENT '状态:0在院 1出院 2作废', PRIMARY KEY (visit_id), KEY idx_patient (patient_id), KEY idx_visit_date (visit_date) ) COMMENT '就诊记录表';这里有几个参数值得细看。主键用 VARCHAR(32) 而不是 BIGINT,是因为患者主索引 ID 往往由院区代码、建档年份、流水号拼成,比如“H0012024000123”,这种业务编码本身就携带信息,适合做跨系统对接。性别用 CHAR(1) 且允许‘9未知’,而不是 NULL,是因为很多报表框架处理 NULL 时容易丢维度,显式的‘9’能保住统计口径。唯一键放在 (id_type, id_no) 上,是因为身份证、护照、军官证各自有编号体系,单独拿 id_no 做唯一键会撞号。
就诊表的 visit_id 同样用 VARCHAR(32),取值通常是院区代码加就诊流水。status 字段的默认值是‘0’,说明系统默认按“在院”写入,出院后才翻转为‘1’;‘2作废’则用于挂错号、重复挂号等情况。注意这里 status 和 out_time 没有强绑定——有 out_time 不一定代表已出院,必须看 status。这个坑在集成报表时非常常见,后面避坑章节还会展开。
3.2 病历文书表:主表与段落表的拆分逻辑
病历文书是电子病历系统里最复杂的表,因为一份文书既是结构化数据,又是大段自由文本。厂商普遍采用的方案是“主表 + 段落表 + 版本链”。主表存文书头信息(类型、标题、签名状态),段落表存文书正文拆分出的各个小节,版本表或版次字段记录每次修改。这样做有三个好处:按小节抽取病历做质控或上报时,一条 SQL 就能定位到“主诉”这一节;MEDIUMTEXT 大字段单独存放,避免主表行过大拖慢索引扫描;版本留痕满足医疗合规的修改可追溯要求。
CREATE TABLE emr_document ( doc_id VARCHAR(32) NOT NULL COMMENT '文书ID', visit_id VARCHAR(32) NOT NULL COMMENT '就诊ID', patient_id VARCHAR(32) NOT NULL COMMENT '患者ID', doc_type VARCHAR(16) NOT NULL COMMENT '文书类型:首次病程/日常病程/出院小结/手术记录等', doc_name VARCHAR(128) NOT NULL COMMENT '文书名称', version_no INT NOT NULL DEFAULT 1 COMMENT '版本号,每次修改自增', status CHAR(1) NOT NULL DEFAULT '0' COMMENT '状态:0编辑中 1已签名 2作废', create_time DATETIME NOT NULL COMMENT '创建时间', sign_time DATETIME NULL COMMENT '签名时间', PRIMARY KEY (doc_id), KEY idx_visit (visit_id, doc_type) ) COMMENT '病历文书主表'; CREATE TABLE emr_doc_section ( section_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '段落ID', doc_id VARCHAR(32) NOT NULL COMMENT '文书ID', section_type VARCHAR(16) NOT NULL COMMENT '段落类型:主诉/现病史/既往史/诊断/处置', section_content MEDIUMTEXT NULL COMMENT '段落正文', sort_no INT NOT NULL DEFAULT 0 COMMENT '段落顺序', PRIMARY KEY (section_id), KEY idx_doc (doc_id) ) COMMENT '病历文书段落表';段落表的 section_type 是集成的关键。很多对接需求是“查某段时间内所有主诉里包含‘胸痛’的患者”,如果文书正文是一整块 TEXT,你得先取回全文再在应用层做匹配,既慢又费内存。段落表拆分后,直接 WHERE section_type='主诉' AND section_content LIKE '%胸痛%' 就能完成定位。主表的 version_no 也要留意——默认值 1,修改一次加 1,查询最新文书时不是简单 ORDER BY create_time,而是取 version_no 最大的一行。正文里出现了多个版本的记录,全部按 doc_id 分组后取最大 version_no,才是当前生效版本。
3.3 医嘱表与状态机:ORDER 里最容易被用错的三个字段
医嘱表是电子病历系统里最“活”的表。一条医嘱从开立到执行再到停止,中间要经历多个状态,因此表结构里必然有状态字段和时间字段的组合。但组合得越多,越容易被误读。
CREATE TABLE medical_order ( order_id VARCHAR(32) NOT NULL COMMENT '医嘱ID', visit_id VARCHAR(32) NOT NULL COMMENT '就诊ID', patient_id VARCHAR(32) NOT NULL COMMENT '患者ID', order_type CHAR(1) NOT NULL COMMENT '医嘱类型:1长期 2临时', order_content VARCHAR(512) NOT NULL COMMENT '医嘱内容', order_status CHAR(1) NOT NULL COMMENT '状态:0新开 1执行 2停止 3作废', create_doctor VARCHAR(16) NOT NULL COMMENT '开嘱医师编号', create_time DATETIME NOT NULL COMMENT '开立时间', stop_time DATETIME NULL COMMENT '停止时间', exec_time DATETIME NULL COMMENT '实际执行时间', PRIMARY KEY (order_id), KEY idx_visit (visit_id, order_type) ) COMMENT '医嘱表';第一个容易被用错的字段是 stop_time。很多人写“已停止的医嘱”时直接 WHERE stop_time IS NOT NULL,结果把作废的医嘱也捞进来了。正确做法是看 order_status = '2',stop_time 只是辅助记录时间点,不参与状态判断。第二个是 exec_time,它的 NULL 表示“尚未执行”,而不是“没有记录”。如果把 NULL 当成数据缺失去填充,会把业务语义改坏。第三个是 create_time,它代表开立时间,不是数据写入数据库的时间。医嘱可能在开立后几分钟才真正入库,两者之间的差值在接口调试时会造成诡异的时间偏移。
再补充一个状态机层面的注意点:order_status 的流转是单向的,0 到 1 再到 2,或者 0 直接到 3。不允许从 2 改回 1,也不允许写一个“已停止又恢复”的状态。如果业务上有恢复需求,系统通常的做法是新建一条医嘱,而不是复用原记录。集成方如果按“先停后开”的逻辑设计接口,就要接受两条记录同时存在的事实,并靠 create_time 判断先后。
4. 用表结构文档做集成查询:三张核心表串起一次就诊的病历
分域和表结构都清楚了,接下来进入实操。集成开发最常见的需求是:给一个 patient_id,把某次就诊的病历文书和医嘱记录拉出来。这节直接给可执行的 SQL 和参数说明。
4.1 从患者到文书:一次典型病历查询的 SQL 串联
一条标准链路是患者主索引 → 就诊表 → 文书主表 → 段落表。注意 JOIN 的驱动顺序和 WHERE 里的过滤条件。
SELECT p.patient_name, p.id_no, v.visit_id, v.visit_type, v.visit_date, d.doc_type, d.doc_name, d.status AS doc_status, d.sign_time, s.section_type, s.section_content FROM patient_master_index p JOIN visit_info v ON v.patient_id = p.patient_id JOIN emr_document d ON d.visit_id = v.visit_id JOIN emr_doc_section s ON s.doc_id = d.doc_id WHERE p.patient_id = 'P0000001' AND v.visit_type = '2' AND d.status = '1' ORDER BY d.doc_type, s.sort_no;JOIN 顺序的设计意图是“先窄后宽”。患者主索引按主键定位只返回一行,就诊表按患者过滤后最多几十行,文书表再多也到不了百行,段落表虽然行数大,但通过 doc_id 的索引关联后,实际扫描范围被压到最小。把段落的表放最后,是让最宽的表在驱动时已经拿到了最紧的过滤条件。
WHERE 里的 d.status = '1' 是关键。很多文档表里草稿、编辑中的文书也在库里,不过滤会把医生没写完的草稿当成正式病历抽出去。这个错误的后果在病历上报场景尤其严重——质控系统会把草稿里的脏数据统计进去。另外 v.visit_type='2' 表示只看住院,如果需求是门诊和住院都要,就删掉这个条件,但要注意门诊和住院的 visit_date 语义不同,门诊用就诊时间,住院用入院时间。
拿到这个查询结果,再和文档字段说明对照,基本就能判断这套表结构的字段是否如文档所述。如果发现 JOIN 后返回的行数翻倍,比如同一份文书出现两遍,优先怀疑 emr_document 和 emr_doc_section 是不是一对多看成了一对一,或者文书主表存在历史版本且没有过滤 version_no。
4.2 多表 JOIN 的性能坑:用视图和中间表缓解
电子病历库通常和 HIS 业务库共用一套数据库实例,线上负载很重。直接把 4.1 的三 JOIN 查询做成高频接口,早晚会压到业务库。常见做法是创建一张“病历宽表”视图,把高频查询字段预关联好。
CREATE VIEW v_patient_emr AS SELECT p.patient_id, p.patient_name, p.id_no, p.gender, v.visit_id, v.visit_type, v.visit_date, v.dept_code, d.doc_id, d.doc_type, d.doc_name, d.sign_time FROM patient_master_index p JOIN visit_info v ON v.patient_id = p.patient_id JOIN emr_document d ON d.visit_id = v.visit_id WHERE d.status = '1';视图能做到逻辑统一,但别神化它。底层依旧是 JOIN,查询条件一变,视图不一定能帮你省任何事。比如你要按“疾病诊断”过滤,可视图里根本没诊断字段,还是得回到原表。我的经验是:查询条件固定、查询字段固定的报表和接口,用物化宽表或定时刷新的中间表;查询条件飘忽不定的分析场景,直接走原表加索引。判断标准很简单——看你能不能在 30 秒内把查询条件列全。能列全的,宽表;列不全的,别硬套视图。
中间表的刷新还要注意频率和增量策略。批量导入场景建议按 visit_date 做每日增量,回刷只处理昨天和今天的数据;不要为了省事全表 TRUNCATE 重建,那会把临时失败的影响面放大到全库。
4.3 按文档核对字段:用 information_schema 做一致性核查
字段对不上是集成联调时最耗时的坑。与其对着文档猜,不如直接把真实库的字段结构导出来做 diff。下面这条 SQL 会输出指定表的真实字段清单,包括类型、长度、可空和注释:
SELECT t.TABLE_NAME, c.COLUMN_NAME, c.DATA_TYPE, c.CHARACTER_MAXIMUM_LENGTH, c.IS_NULLABLE, c.COLUMN_COMMENT FROM information_schema.COLUMNS c JOIN information_schema.TABLES t ON c.TABLE_NAME = t.TABLE_NAME WHERE t.TABLE_SCHEMA = 'emr_db' AND c.TABLE_NAME IN ('patient_master_index', 'visit_info', 'emr_document') ORDER BY t.TABLE_NAME, c.ORDINAL_POSITION;把文档里列出的表名替换进 IN 列表,跑一遍,再用 Excel 或脚本和文档的字段清单做对比。差异通常集中在两类:文档有但库没有(说明字段被砍或从未上线),以及库有但文档没有(说明后来加了字段但文档没更新)。前者可能是文档版本比库新,后者更常见,是开发阶段打补丁留下的一堆“备用字段”。
我一般会把这个查询的结果直接导出成 CSV,然后写几行 Python 对比文档字段名。这个习惯帮我至少避免过三次上线当天才发现字段不存在的尴尬。后面第 6 章会给一个更彻底的自动化方案,这里先记住这个查询入口。
5. 避坑指南:表结构文档与实际库不一致的 5 个翻车现场
拿到文档不等于拿到真相。做得多了你会发现,文档只是“系统设计时的想法”,实际库早就被人为改成另一副模样。这一章列五个我踩过或见过的典型坑,全部按“现象 → 原因 → 解决”的顺序写,方便你排查时对照。
5.1 字段命名对不上:文档里叫 STATUS,库里叫 BZ
现象:完全按文档写 SQL,SELECT status FROM visit_info,直接报“字段不存在”。查 information_schema 才发现,库里这个字段叫 bz,注释是“状态”,其实是开发阶段 DBA 嫌字段名太长改的。
原因:开发阶段 DBA 调整过字段命名,文档没同步;更隐蔽的情况是多院区共用一套程序,但不同院区的数据库结构有小版本差异,某个院区手工加过列。
解决:上线前把 4.3 的 information_schema 核查脚本跑一遍,凡是文档与库不一致的字段,一律以库为准,同时保留一张“文档名/库名”对照映射表。这张映射表就是你的后悔药,后续其他系统对接时可以直接复用,不用每次联调都翻文档。
5.2 文档标注“非空”,实际库里大量空值
现象:文档里 IS_NULLABLE 列写着 NO,你写查询时放心地用这字段做 WHERE 过滤,结果一大批数据被莫名过滤掉,报表数字对不上。
原因:字段是上线后加的,存量历史数据没有回填;或加字段时用了 DEFAULT NULL,又没有跑更新脚本刷历史数据。
解决:先在真实库跑一次空值率统计,SQL 很简单:SELECT COUNT(1) total, COUNT(your_field) filled FROM visit_info;。如果 filled 远小于 total,这个字段就不能作为核心过滤条件。折中方案是把它放进 LEFT JOIN 的关联条件里,或者放进 SELECT 子句做展示,而不是放进 WHERE。否则一条 NULL 就能让整行数据消失,排查起来极难发现。
5.3 长度不够:VARCHAR(50) 存不下诊断描述
现象:对接写入时报“Data too long for column”,或数据被静默截断。
原因:文档里设计的字段长度是基于旧业务的,临床这边扩展了录入内容,比如诊断描述越来越长,甚至直接把一整段病历贴进去。
解决:写迁移脚本前先做一次扫描:SELECT MAX(LENGTH(diag_desc)) FROM visit_diag;,跟文档标称长度对比。超长的字段分两类处理——偶尔超长的,把 DDL 长度放宽到 500 或改用 TEXT;经常超长且要参与查询的,考虑拆字段或改存段落表。动手改库之前先备份原表,这是血泪经验。不要做“按文档长度截断”这种事,临床数据被截断等于数据损坏,后期回溯根本找不到丢的是什么。
5.4 一个枚举字段,两套代码表
现象:同一个 gender 字段,A 院区存‘1’/‘2’,B 院区存‘M’/‘F’;甚至同一张表里,新数据用 0/1,老数据用 N/Y。统计性别分布时数字永远对不上。
原因:字典表升级后老接口还在写旧值;或院区合并时只合了主数据,业务表的历史数据没清洗。
解决:写查询时用 CASE WHEN 归一化。比如把‘M’和‘1’都转成‘1’,‘F’和‘2’都转成‘2’,其余归为‘9’。同时做一个质量监控 SQL,定期查未知枚举值的占比,超过阈值就提醒。这套规则必须落到你们自己的接口文档里,不要只抄厂商原文档——原文档写的是“设计值”,实际数据是什么,只有跑完统计才知道。遇到这种两套代码表并存的情况,别试图去改历史数据,风险太大,归一化逻辑放应用层是最稳的。
5.5 时间字段的语义陷阱:create_time 到底是谁的创建时间
现象:统计分析“上午开立的医嘱占比”,结果数据偏移严重,和护士站手工记录对不上。
原因:同一个字段名在不同表里语义不同。医嘱表的 create_time 是开立时间,但有些系统里护士补录医嘱时,create_time 保留的是原始开立时间,补录操作本身只更新 update_time。你如果按 create_time 分组统计,把补录的单子全部算到了历史时刻上。
解决:语义歧义只能靠“文档 + 实测”双重确认。实测方法很简单:找一条刚录入的医嘱,对比 create_time 和数据库写入时间。如果两者相差很大,说明 create_time 不是入库时间,得另找 input_time 之类的字段。把实测结论写进自己的接口字段说明里,这是文档之外最可靠的依据。
6. 把 .doc 表结构文档变成可检索的 SQL 字典:一个周末能做完的自动化脚本
前面 4.3 提到用 information_schema 核对字段,但手工对几百张表不现实。更彻底的做法是:把这份 .doc 表结构文档转成一套机器可读的 Markdown 或 SQL 脚本,之后无论是字段查找还是前后 diff 都变成文本处理。这一章的脚本目标是“一小时内完成文档数字化”。
先说格式。.doc 是老式二进制格式,python-docx 库只能读 .docx,不能直接读 .doc。常见做法是把转换交给 LibreOffice 的无头模式,一条命令就能完成:
soffice --headless --convert-to docx "联众电子病历表结构文档.doc" --outdir ./output转换完成后,拿到 ./output 下的同名 .docx 文件,再用 python-docx 解析表格。下面这个脚本会把文档里所有字段清单表打印成 Markdown 风格的文本:
import glob from docx import Document docx_file = glob.glob("./output/*.docx")[0] doc = Document(docx_file) # 常见表结构文档里,每张表是一个 docx 表格对象, # 第一行通常是表头:序号 字段名 类型 长度 可空 默认值 说明 for table in doc.tables: rows = table.rows if not rows: continue header = [cell.text.strip() for cell in rows[0].cells] # 只处理看起来像字段清单的表头,跳过目录表、用例说明表 if "字段名" not in header and "列名" not in header: continue print(f"## 表: {header[1] if len(header) > 1 else 'unknown'}") for row in rows[1:]: cells = [cell.text.strip() for cell in row.cells] print(" | ".join(cells)) print()逻辑说明:脚本先拿到表头,判断它是不是一份字段清单;是则打印表名和所有字段行。核心价值不是格式多漂亮,而是把“人类阅读的 Word 表格”变成“可 grep、可 diff 的纯文本”。跑完后,你想找“哪个表有 sign_time 字段”,一条 grep 命令就能解决,不用再翻几百页 Word。
脚本有个容易翻车的点:字段清单表里出现合并单元格时,python-docx 会把合并区域的值重复读到每一行里,导致导出结果里有大量重复内容。处理办法是先抽两到三张最大的表,人工核对一遍列顺序,确认没有合并单元格干扰再批量跑。如果发现合并单元格很多,退一步转成 HTML 再解析,往往比直接读 docx 更稳。
进阶玩法是把导出的字段清单再和 information_schema 做一次字段级 diff,输出“文档有但库没有”和“库有但文档没有”两张清单。我在一次数据迁移项目里靠这个 diff 提前发现了一张旧表字段被整个业务替换掉的问题,避免了迁移后接口大面积报错的翻车。现在拿到任何电子病历表结构文档,我都不会直接对着写 SQL,而是先跑一遍转换脚本,再做一次字段 diff。这个习惯帮我省掉了至少一个周末的联调时间。希望这篇笔记能帮你在集成时少踩几个坑。
本文还有配套的精品资源,点击获取