
简介《医院信息系统基本功能规范》PDF文档面向医院信息化主管人员、HIS开发与实施工程师以及医疗信息化专业师生用于解决系统功能界定不清、开发缺少统一评审依据的问题可作为医院立项、选型与验收时的参照标准。资源包内仅1个PDF文件大小约6.74MB完整收录规范正文与《医院信息系统方案》两部分内容前者按总则、数据与数据字典标准化、临床诊疗、药品管理、经济管理、综合管理与统计分析、外部接口等章节逐项规定门诊与住院医生工作站、护士工作站、检验、输血、医学影像、手术麻醉、挂号收费、入出转、物资设备、财务核算、病案统计、院长查询、病人咨询等分系统的设计目标、法规依据与功能要求后者补充系统定义、开发意义、国内外发展、体系结构、网络方案选择及信息化配置报价附件。文档同时附有修订说明交代标准化独立成章、新增临床信息系统与医保、社区卫生、远程医疗接口等修订要点便于读者把握规范演进脉络。目前已有193人学习下载适合需要理解医院信息系统整体框架与功能边界的读者查阅参考。1. 拿到《医院信息系统基本功能规范》.pdf先想清楚它要变成什么做 HIS、电子病历、互联网医院的产品和研发硬盘里大概率都躺着一份《医院信息系统基本功能规范》.pdf用它的方式也基本一致CtrlF 搜关键词翻到那一页截图贴进评审文档。问题在于这份文件的价值不是阅读而是条目。它列的是一个个功能点某子系统应当具备什么能力、哪些是必须实现的、哪些属于可选。真正好用的形态是一张可检索、可勾选、可对比版本的条目表评审时能直接引用原文页码投标时能一键导出应答清单。所以工程上要做的不是把它转成排版漂亮的 Word而是走一条 PDF 解析的链路先判断文本层情况再按坐标抽块再识别功能要求表格最后落成结构化数据并建索引。下面按这个顺序写一遍每一步都给出可复现的命令和代码。2. 解析《医院信息系统基本功能规范》.pdf 前的文本层探底与页面预处理直接上 pdfplumber 或者 PyMuPDF 抽文字结果整页空白或者满屏乱码十有八九是没先摸清这份 PDF 的文本层。规范类文件在网上流传的版本很多有正规排版的电子版也有拍照扫描后拼起来的版本处理路线完全不同先探底能省掉后面几小时的返工。2.1 用 pdffonts 与 pdftotext 三分钟判断文本层poppler-utils 里的两个命令足够做初判不需要写代码# 1) 看字体是否嵌入、是否有 Unicode 映射uni 列为 no 时抽出来多半是乱码 pdffonts 医院信息系统基本功能规范.pdf | head -20 # 2) 看前 8 页能否抽出文字-layout 保留原始空白位置便于判断表格是否靠空格对齐 pdftotext -layout -f 1 -l 8 医院信息系统基本功能规范.pdf - | sed -n 1,60p # 3) 统计页面尺寸确认是否混入横向页回跳高亮要靠它换算坐标 pdfinfo 医院信息系统基本功能规范.pdf | grep -E Page size|Pagespdffonts的emb表示字体是否嵌入uni表示是否存在 ToUnicode 映射表。两者都是 no 时抽取出来的字符是字形的编码值会显示成一堆( ) *之类的符号这种文件必须走字形映射修复或者直接 OCR。pdftotext -layout抽出的内容如果表格列能对齐说明原文件是文本层且排版规整后面按坐标抽块会很顺。现象pdffonts 特征推荐处理路线抽得出、能正常阅读embyes 或 uniyes直接按坐标抽块最快抽得出但是乱码unino先修 ToUnicode或整页渲染后 OCR整页无文字无字体记录或只有 Type3300 DPI 渲染位图纠偏后 OCR部分页正常、部分页空白混合按页分流正常页走抽块空白页走 OCR提示不要一上来就对整本做 OCR。OCR 的错字形近字、表格线被识别成竖线会让后面的条目对账非常痛苦能抽文本层就不要 OCR。2.2 PyMuPDF 按坐标抽块把版面还原成可计算的数据文本层正常的情况下抽块比抽纯文本有用得多因为每个 span 都带坐标和字体信息。import fitz # PyMuPDF doc fitz.open(医院信息系统基本功能规范.pdf) page doc[12] print(page size(pt):, page.rect.width, page.rect.height) # A4 约 595 x 842 d page.get_text(dict) for b in d[blocks]: if b[type] ! 0: # 0文本块1图片块图片块单独处理 continue for line in b[lines]: for span in line[spans]: x0, y0, x1, y1 span[bbox] print(f{y0:7.1f} {x0:7.1f} {span[font]:18s} {span[size]:5.1f} {span[text]})坐标单位是 pt1/72 英寸原点在页面左上角y 向下增大这一点和常见的数学坐标系相反排序时容易踩。font和size是区分层级的依据规范文档里章标题通常用黑体且字号偏大表头和正文可能同字号只能靠字体名SimHei 与 SimSun 的差别或者加粗标记来区分这类规则要写成配置项而不是散落在代码里的 if。同一个逻辑行可能被拆成多个 span中英文混排、加粗片段需要按 y 坐标聚类再拼接def page_lines(page, y_tol3.0): 把 span 按 y 坐标聚成行返回按阅读顺序排好的行文本 spans [] for b in page.get_text(dict)[blocks]: if b[type]: continue for line in b[lines]: for s in line[spans]: if s[text].strip(): spans.append(s) rows, cur, last_y [], [], None for s in sorted(spans, keylambda s: (s[bbox][1], s[bbox][0])): y s[bbox][1] if last_y is None or abs(y - last_y) y_tol: cur.append(s) else: rows.append(cur) cur [s] last_y y if cur: rows.append(cur) return [.join(s[text] for s in r) for r in rows]y_tol控制多近算同一行正常电子版取 2~4pt 就够如果页面是纠偏后的扫描件基线抖动会变大可以放宽到 6pt但放太宽会把相邻两行并成一行出现条目正文和表格线文字混在一起的情况这种情况下应该回到按表格结构解析而不是继续加宽容忍度。2.3 中文字体缺失与位图页的纠偏、漂白加深遇到纯图片页处理顺序是渲染、纠偏、二值化、OCR。渲染倍率直接决定后面能不能识别规范文档的表格线很细低于 200 DPI 容易断线300 DPI 是比较稳的下限。中文识别还要注意 OCR 引擎的语言参数要显式设为中文用默认的英文模型跑中文结果会全是噪声。import cv2, numpy as np, fitz doc fitz.open(医院信息系统基本功能规范.pdf) page doc[30] pix page.get_pixmap(dpi300) img np.frombuffer(pix.samples, np.uint8).reshape(pix.height, pix.width, pix.n) gray cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) if pix.n 3 else img # 纠偏用前景像素的最小外接矩形估倾斜角 coords np.column_stack(np.where(gray 200)) angle cv2.minAreaRect(coords.astype(np.float32))[-1] if angle -45: angle 90 h, w gray.shape M cv2.getRotationMatrix2D((w / 2, h / 2), angle, 1.0) gray cv2.warpAffine(gray, M, (w, h), flagscv2.INTER_CUBIC, borderValue255) # 漂白加深自适应阈值去灰底、加粗笔画让 OCR 更稳 bw cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 31, 15) cv2.imwrite(page_30_bw.png, bw)这段里三个参数值得单独说。dpi300是渲染倍率不是原始分辨率扫描件原图本身模糊的话提高 dpi 也救不回来得回到采集环节。blockSize31是自适应阈值的邻域大小纸张发黄、装订阴影重的时候要加大到 51否则大片灰底会被判成黑。C15是阈值偏移量调小到 10 以内可以避免笔画糊死、表格线粘连在一起调太大则细笔画会被漂白掉表格竖线断开后面按线条检测表格就会失败。注意纠偏角度建议限制在 ±3° 内。表格类文档倾斜超过 3° 通常意味着是手机拍摄件透视变形不是旋转能修好的这种情况先人工确认再决定是否引入透视校正。3. 把《医院信息系统基本功能规范》.pdf 的功能条目表解析成结构化 JSON整份文件里价值密度最高的是功能要求表格它不是纯文本段落而是序号 功能名称 功能要求的规整结构。表格解析的核心是先确定哪些行属于同一张表再确定每一列的语义最后处理跨页。3.1 表格线检测与无框表的兜底策略有框表格优先用线条检测。较新版本的 PyMuPDF 内置了find_tables()会结合线条和文本对齐推断单元格边界import fitz doc fitz.open(医院信息系统基本功能规范.pdf) page doc[15] for t in page.find_tables(): print(bbox:, t.bbox, rows:, t.row_count, cols:, t.col_count) for row in t.extract(): print(row) # 二维列表空单元格是 Nonet.bbox是表格在页面中的矩形必须和页码一起存下来前端做条目回跳高亮就靠它。extract()返回的二维列表里空单元格是None而不是空字符串导出 CSV 或写库前要统一成否则后面做字符串比较会不停踩坑。老版本没有find_tables()或者表格线条被漂白处理弄断时就自己找横线。get_drawings()能取出页面上的所有绘制对象包括线段和矩形def h_lines(page, min_w200): 抽取近似水平的表格横线返回合并后的 y 坐标列表 ys [] for d in page.get_drawings(): for item in d[items]: if item[0] l: # 线段 p1, p2 item[1], item[2] if abs(p1.y - p2.y) 0.5 and abs(p2.x - p1.x) min_w: ys.append((p1.y p2.y) / 2) elif item[0] re: # 矩形取上下边 r item[1] if r.width min_w: ys [r.y0, r.y1] ys.sort() merged [] for y in ys: # 合并 1pt 内的重复线 if not merged or y - merged[-1] 1.0: merged.append(y) return mergedmin_w200是算作表格横线的最小宽度A4 去掉页边距后正文宽度约 500pt取 200 能有效滤掉段落上下的装饰线和页眉分隔线。合并 1pt 内的重复线是必要的因为同一条线常被拆成多段绘制不合并会导致行切分出现零高度行。无框表的兜底思路是纯坐标聚类行的切分依据是 y 坐标的明显空隙列的切分依据是 x 坐标的投影直方图找到直方图上的低谷作为列边界。这条路在中文混排文档里准确率一般建议只用于少量表格结果全部标记低置信度转人工。3.2 跨页表头、合并单元格与条目编号续接跨页是最容易出错的地方。一张功能表跨三页第二、三页顶部会重复表头如果不过滤条目表里会多出几条功能名称/功能要求的伪条目。判断方式是对比当前页第一行与上一页表头的相似度import re, difflib HEADER_SIM 0.8 def is_same_header(a: str, b: str) - bool: a re.sub(r\s, , a or ) b re.sub(r\s, , b or ) if not a or not b: return False return difflib.SequenceMatcher(None, a, b).ratio() HEADER_SIM def fix_item_seq(items): 补齐跨页丢掉的章节前缀例如 3.2.1 在续页被写成 1 out, chapter [], None for it in items: m re.match(r^(\d(?:\.\d)*), it.get(item_no) or ) if m and . in m.group(1): chapter m.group(1).rsplit(., 1)[0] elif m and chapter: it[item_no] f{chapter}.{m.group(1)} out.append(it) return outHEADER_SIM0.8是相似度阈值。调低到 0.6 会把功能名称/功能要求和序号/功能项目这种相近但不同的表头误判成重复导致真正的表头被丢弃调到 0.95 则会漏掉因为多了一个字或少了一个空格而变化的表头。实践中先用一批样本调出阈值再固定下来写进配置。fix_item_seq只处理一种情况章节号在续页被简写成短编号。如果原文本来就用短编号体系这个函数会把不该补的前缀补上去所以入库后必须人工抽样确认别把启发式规则当成事实。字段类型说明item_idTEXT稳定主键建议用 section_path item_no 拼接section_pathTEXT章节路径如门诊医生工作站/处方处理item_noTEXT规范原文条目号保留原样不做归一化item_nameTEXT功能名称requirementTEXT功能要求正文mandatoryINTEGER1 必选0 可选-1 原文未标注source_pageINTEGER零基页码用于前端回跳source_bboxTEXTx0,y0,x1,y1 页面坐标parse_confREAL解析置信度用于排序人工复核队列3.3 字段设计、JSON 落库与置信度打分字段里最有价值的是source_page和source_bbox它们让每一条解析结果都能回到原文位置。评审被质疑这条是不是规范要求的直接点一下跳到原页高亮比截图可信得多。import sqlite3 SCHEMA CREATE TABLE IF NOT EXISTS his_item( item_id TEXT PRIMARY KEY, section_path TEXT, item_no TEXT, item_name TEXT, requirement TEXT, mandatory INTEGER DEFAULT -1, source_page INTEGER, source_bbox TEXT, parse_conf REAL DEFAULT 1.0); def save(items, dbhis_spec.db): con sqlite3.connect(db) con.executescript(SCHEMA) con.executemany( INSERT OR REPLACE INTO his_item VALUES(?,?,?,?,?,?,?,?,?), [(i[item_id], i[section_path], i[item_no], i[item_name], i[requirement], i.get(mandatory, -1), i[source_page], i[source_bbox], i.get(parse_conf, 1.0)) for i in items]) con.commit() con.close()item_id用section_path item_no拼接重跑解析时可以幂等覆盖不会产生重复条目。mandatory用-1表示原文没有区分必选和可选千万不要用 0 代替否则统计必选功能条数时会系统性偏小直接影响投标应答的覆盖度评估。parse_conf可以用简单的启发式打分单元格文本里是否出现异常字符、行数与表头列数是否一致、该行是否跨页断裂几项加权得到 0~1 的分数低于 0.9 的一律进人工队列优先复核。提示解析脚本要支持只重跑某几页整本重跑在调试表格规则时太慢而且容易把已经人工修正过的条目覆盖掉。常见做法是加一张人工修正表落库时以人工值为准。4. 《医院信息系统基本功能规范》条目库的检索、对账与 PDF 转 Word 取舍条目落库之后接下来要解决两件事一是让非技术同事能用自然语言查到条目二是每次规范换版或者解析规则调整后能快速判断有没有丢掉内容。4.1 pdf转word 与直接抽结构的两条路很多人的第一反应是先把 PDF 转 Word再用 Word 的表格功能导出。这条路在规范文档上代价不小。第一版面重排。原 PDF 里跨页的合并单元格转成 Word 后经常被拆成多个小表列对齐关系丢失表头会被复制到每一页导致条目重复计数。第二页码信息彻底丢失。条目和原文页码的对应关系断了前端点击回跳原页就实现不了而这恰恰是规范条目库最被认可的功能。第三一次性产出。转出来的 docx 是一份成品文件规范换版后要全部重来而结构化解析的规则和字段是可以复用的。需求场景建议路线只需要阅读、批注、打印用 PDF 阅读器或按需转 Word需要条目化、可检索、可勾选坐标抽块 表格解析落结构化库需要逐条引用原页码必须保留 source_page 与 source_bbox需要对外正式发文Word 定稿后再导出 PDF不要拿解析结果直接排版提示转 Word 工具处理规范里的合并表头时会把表头行重复输出到每一页。用 Word 里的条目数去核对解析结果会得到偏大的数字对账基准应该始终是原 PDF。4.2 SQLite FTS5 建中文全文索引与查询语句条目量通常只有几百到几千条不需要上 Elasticsearch。SQLite 的 FTS5 就足够唯一要解决的是中文分词默认的 unicode61 分词器不切中文整段中文会变成一个 token搜处方是搜不到的。常见做法是先用 jieba 分词把词用空格连起来再写入索引表。import jieba, sqlite3 def build_fts(dbhis_spec.db): con sqlite3.connect(db) con.executescript( CREATE VIRTUAL TABLE IF NOT EXISTS his_item_fts USING fts5(item_id UNINDEXED, item_no, item_name, requirement, section_path, tokenizeunicode61); ) seg lambda s: .join(jieba.cut_for_search(s or )) rows con.execute( SELECT item_id,item_no,item_name,requirement,section_path FROM his_item) data [(r[0], seg(r[1]), seg(r[2]), seg(r[3]), seg(r[4])) for r in rows] con.execute(DELETE FROM his_item_fts) con.executemany(INSERT INTO his_item_fts VALUES(?,?,?,?,?), data) con.commit() con.close()cut_for_search是搜索引擎模式会把门诊医生工作站额外切出门诊医生工作站召回率更高代价是索引体积大约翻倍。对几千条数据来说这个代价可以忽略。如果环境里的 SQLite 支持 trigram 分词器也能省掉 jieba但 trigram 要求查询词至少三个字符「处方」「医嘱」这类两字词会直接查不到做医疗术语检索并不划算。查询时用snippet()直接返回命中片段前端拿来渲染高亮SELECT f.item_id, f.section_path, f.item_no, f.item_name, snippet(his_item_fts, 3, b, /b, …, 16) AS hit FROM his_item_fts f WHERE his_item_fts MATCH 门诊 AND (处方 OR 医嘱) ORDER BY rank LIMIT 20;MATCH里的布尔语法支持 AND、OR、NOT 和括号组合比 LIKE 灵活得多。snippet()的第 2、3 个参数是命中词的前后标记第 4 个是省略号第 5 个是片段包含的词数。rank是 FTS5 的内置排序辅助列要按列加权就换成bm25()ORDER BY bm25(his_item_fts, 0.0, 3.0, 1.0, 0.5, 0.2)这五个数字依次对应item_id、item_no、item_name、requirement、section_path的权重。item_id给 0 是因为它不该参与排序item_name给 3.0 是因为命中功能名称的条目通常就是用户想要的那条正文命中只能算相关。权重不要凭感觉乱调拿一批真实查询词做回归看前 5 条的人工判定结果再定。4.3 用条目对账和版本 diff 校验解析完整性解析规则每次调整都要验证有没有丢内容三个对账指标最实用。条目数对账各章节的条目数是否与目录页标注一致目录没有标条数的就跳过这一项。字符覆盖率抽出的条目正文字符总数除以pdftotext -layout抽出的正文页字符总数低于 0.95 说明有整张表没被识别到通常是表格线断开导致的。编号连续性同一章节内item_no应当单调递增出现回退说明跨页续接规则失效。规范换版时用条目级 diff 定位改动点import difflib, sqlite3 def diff_versions(db_old, db_new): q SELECT item_id, requirement FROM his_item load lambda p: dict(sqlite3.connect(p).execute(q)) a, b load(db_old), load(db_new) added [k for k in b if k not in a] removed [k for k in a if k not in b] changed [] for k in a.keys() b.keys(): if (a[k] or ) ! (b[k] or ): ratio difflib.SequenceMatcher(None, a[k] or , b[k] or ).ratio() changed.append((k, round(ratio, 3))) return added, removed, sorted(changed, keylambda x: x[1])changed里相似度越低说明改动越大升序排列后先看最前面几条通常就是新版规范真正调整的功能点。这里有个前提两版的条目编号体系要一致。如果新版做了章节重排直接 diff 会得到一大堆 added 和 removed毫无参考价值得先做章节映射再比。章节映射没有通用解法常见做法是人工维护一张新旧章节对照表条目级 diff 只用于对照表里已经匹配上的章节。5. 让《医院信息系统基本功能规范》.pdf 在系统里可点可查条目库本身是给系统用的一线同事更常做的是在页面上搜一条、点开原文看一眼。这一步要解决坐标换算、弹窗预览和长期质量保障。5.1 从条目坐标到 PDF 原页高亮数据库里存的是 PDF 页面坐标pt前端 PDF.js 用的是相对页面的百分比需要归一化import fitz doc fitz.open(医院信息系统基本功能规范.pdf) def to_pct(bbox: str, page_no: int): bbox 形如 x0,y0,x1,y1返回相对页面的百分比矩形 page doc[page_no] w, h page.rect.width, page.rect.height x0, y0, x1, y1 [float(v) for v in bbox.split(,)] return {left: x0 / w, top: y0 / h, width: (x1 - x0) / w, height: (y1 - y0) / h}除以每页实际的宽高而不是写死 595/842是因为规范文档里常混入横向页和不同纸张尺寸的插页写死尺寸会让高亮框在横向页上偏出页面。前端拿到这组百分比后在 PDF.js 的 viewer 容器上盖一层绝对定位的 div用边框或半透明底色标记即可缩放时不会错位。5.2 element-ui 弹窗预览与 OnlyOffice 的适用边界如果只需要点开看一眼原文用 element-ui 的 dialog 套一个 iframe走浏览器内置阅读器成本最低el-dialog :visible.syncpdfVisible width80% top4vh append-to-body iframe :src/spec/spec.pdf#page${page}zoompage-width stylewidth:100%;height:78vh;border:0/iframe /el-dialog三个细节append-to-body必须加否则弹窗会被父级的 overflow 裁掉出现半截白屏zoompage-width让不同宽度的页面统一铺满容器避免横向滚动#page参数只有浏览器内置阅读器认用第三方 viewer 时跳页要调它自己的 API。路径必须同源跨域 iframe 里浏览器会拒绝渲染 PDF。另外要确认响应头里没有Content-Disposition: attachment带了这个头会直接触发下载而不是预览。如果流程里有批注、修订留痕、多人同时看同一份文件的诉求浏览器内置阅读器就不够了需要独立的文档服务用 Docker 部署比较省事docker run -d --name onlyoffice-ds -p 8080:80 \ -e JWT_ENABLEDtrue -e JWT_SECRETchange-me \ -v /data/onlyoffice/logs:/var/log/onlyoffice \ onlyoffice/documentserverJWT_ENABLED打开之后业务后端生成文档配置时必须带上签名否则文档服务会直接拒载这是最容易在联调时卡住的一项。日志目录挂到宿主机是为了容器升级重建后不丢排查线索。如果文件是通过 alist 这类文件列表服务分发给前端的要注意它给出的是下载直链还是带签名的临时直链预览服务通常只接受后者直链文件名里带中文时还需要做 URL 编码否则会 404。5.3 用黄金抽样集做解析回归解析规则一旦开始调就会陷入这里修好那里坏的循环。y_tol 放宽了跨页条目并进来了但相邻两行也被合并了二值化参数调狠了灰底去干净了表格竖线也断了。凭感觉调参数不可靠。务实做法是准备一个 30~50 条的人工核对集覆盖跨页条目、合并单元格条目、含特殊符号的条目脚本逐条比对关键片段import sqlite3 def regression(gold, dbhis_spec.db): gold: [{item_id: ..., must_contain: [处方, 审核]}] con sqlite3.connect(db) bad [] for g in gold: row con.execute( SELECT item_no, requirement FROM his_item WHERE item_id?, (g[item_id],)).fetchone() if not row: bad.append((g[item_id], missing)) continue miss [k for k in g[must_contain] if k not in (row[1] or )] if miss: bad.append((g[item_id], miss)) return badmust_contain里只放条目中稳定出现的术语片段不要放数字。页码、条目编号会随版本变化放进去只会制造假告警。返回的bad列表长度就是这次参数调整的副作用指标变长了优先回滚参数而不是继续叠加补丁规则。黄金集和解析脚本要一起进版本库规则改了、期望值改了diff 里都能看见比口头约定靠谱得多。真正省事的做法是把这一步接进 CI解析脚本一提交就自动跑bad不为空直接拦住合入。本文还有配套的精品资源点击获取