news 2026/9/17 21:55:46

SIB严重障碍量表电子化:从.doc解析到计分入库与随访

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SIB严重障碍量表电子化:从.doc解析到计分入库与随访

简介:《严重障碍量表SIB.doc》是一份面向临床医生、护理人员、老年精神科研究者及临床试验从业者的专业评估文档,用于系统测量晚期阿尔茨海默病患者的认知功能水平,帮助判断损伤程度并支持个体化照护方案与疗效跟踪。量表共51个条目,覆盖社会交往、记忆、定向力、语言、视觉空间与构造能力、注意力及行为等维度,总分由0分(损伤最严重)至100分,条目附有逐级评分标准。例如社会交往部分要求测试者主动握手并自我介绍、以手势或搀扶引导患者入座,依据患者是否自发行动、需提示或需协助分别给1至2分;记忆力部分通过自我介绍后复述姓名考察回忆;定向力围绕当前月份与所在城市提问,允许给予多选提示;语言部分则涵盖书写姓名、抄写打印文字、列举一年十二个月、命名杯子勺子等日常物品,以及阅读卡片并按指令递出手掌等任务。资源包仅含1个doc文档,约93KB,已有90人学习下载,适合需要掌握标准化施测流程与记分规则的专业读者随时查阅。

1. 一份「严重障碍量表SIB.doc」真正要处理的是什么

拿到 严重障碍量表SIB.doc 这类文件的人,多数不是想读一遍,而是要把它变成系统里能算、能存、能前后对比的东西:40 多个条目手工计分容易串行,分量表散落在几页里,随访时还要算变化量,靠 Excel 复制粘贴迟早出错。SIB(Severe Impairment Battery,严重障碍量表)用于中重度至重度痴呆患者的认知评估,常见文献描述为 40 个条目、总分 0~100、单次施测 20~30 分钟,条目门槛低、多用单字或指认式作答——这个特征决定了它天然适合电子化,也决定了文档解析比 MMSE 更麻烦:条目编号全角半角混排、选项和分值挤在同一行、不少版本到手时已经是扫描件或改过后缀的假 .doc。后面按「拆文档 → 建计分 → 落库 → 随访」把这条路走通,读者是负责量表电子化的后端与数据工程师,以及需要自己攒科研数据集的临床研究者。

2. 把 .doc 版 SIB 量表拆成可解析文本与结构化 JSON

2.1 先确认文件是不是真的 .doc:magic bytes 判别

从 OA、邮箱、IM 里流转出来的文件,后缀基本不可信。我见过把 .docx 直接改名成 .doc 的,也见过把扫描件另存成 .doc 的。老版 Word 用的是 OLE2 复合文档格式,头部是D0 CF 11 E0 A1 B1 1A E1;OOXML 系列(docx/xlsx)本质是 zip,头部50 4B 03 04;RTF 以{\rtf开头;PDF 以%PDF-开头。先判别再选工具,能省掉一半排查时间。

# sniff_doc.py —— 不依赖扩展名,只读头部 8 字节判类型 import pathlib SIGNATURES = [ (b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1", "ole2-doc"), # 老版 Word 二进制 (b"PK\x03\x04", "zip-ooxml"), # docx 等 OOXML (b"{\\rtf", "rtf"), # RTF (b"%PDF-", "pdf"), # PDF(可能是扫描件) ] def sniff(path: str) -> str: head = pathlib.Path(path).read_bytes()[:8] for sig, name in SIGNATURES: if head.startswith(sig): return name return "unknown"

read_bytes()[:8]只读文件头,几十 MB 的附件也不会把内存打满。判别结果里unknown要警惕:可能是 WPS 的私有封装,也可能是加密文档,这时候别硬解析,先让业务方给原始文件。

文件类型头部特征推荐处理路径
OLE2 二进制 .docD0 CF 11 E0LibreOffice 转 docx,或 antiword 取纯文本
OOXML .docx50 4B 03 04python-docx 直接读表格结构
RTF7B 5C 72 74 66striprtf 或 LibreOffice 统一转换
扫描 PDF / 图片25 50 44 46先 OCR,再走同一套解析

提示:量表条目原文、分值和分量表归属,一律以你手上的 SIB.doc 为准。不同修订版本条目数和计分口径存在差异,代码里的条目内容只能当占位符。

2.2 转换工具怎么选:LibreOffice headless、antiword、catdoc 的取舍

三条命令各有适用面,我一般主路径走 LibreOffice,辅路径走 catdoc 做交叉校验。

# 1) LibreOffice headless:还原度最高,能保留表格结构与单元格边界 soffice --headless --convert-to docx --outdir ./out "严重障碍量表SIB.doc" # 2) antiword:纯文本模式,速度最快,但表格会被拍平成普通行 antiword -m UTF-8.txt "严重障碍量表SIB.doc" > sib_antiword.txt # 3) catdoc:可显式指定编码,处理老的 GBK 文档更稳 catdoc -d utf-8 "严重障碍量表SIB.doc" > sib_catdoc.txt

--convert-to docx的意义在于把「条目号 / 题干 / 选项 / 分值」这几列从视觉排版还原成表格关系,后续用 python-docx 遍历doc.tables比抠文本靠谱得多。-m UTF-8.txt是 antiword 的映射文件,不做映射时中文会变乱码;-d utf-8是 catdoc 的输出编码开关,源文件是 GBK 时必加。

主辅两条路径的产物要做一致性校验:同一份 .doc,两条路径抽出的条目数应当相同,条目号的集合也应当相同。不一致说明有换行、分页或文本框干扰,回去看原始 docx 的表格结构。

2.3 用正则把 SIB 条目、选项与分值抽成 JSON

中文量表最常见的坑是全角字符。1.1.在不做归一化时是两个完全不同的模式,同一条目换个版本就匹配不上。先归一化,再匹配。

import re # 全角数字、括号、点号、冒号统一转半角,否则跨版本匹配必挂 TRANS = str.maketrans("0123456789().:", "0123456789().:") # 行首条目号 + 题干 + 可选的行尾满分标注 ITEM_RE = re.compile( r"^\s*(?P<no>\d{1,2})\s*[.、)]\s*(?P<text>.+?)" r"(?:[((](?P<max>\d{1,2})\s*分?[))])?\s*$" ) def parse_items(lines): items, cur = [], None for raw in lines: line = raw.translate(TRANS).strip() if not line: continue m = ITEM_RE.match(line) if m: cur = { "no": int(m.group("no")), "text": m.group("text").strip(), "max": int(m.group("max")) if m.group("max") else None, "options": [], } items.append(cur) elif cur is not None: # 续行、选项行统一挂到上一个条目,不新起条目 cur["options"].append(line) return items

ITEM_RE^...$逐行匹配而不是整篇扫描,因为条目题干常跨行,整篇匹配会把两个条目粘成一条。(?P<max>...)设为可选,是因为不少版本的满分只写在计分说明里而不在题干行;这些条目需要在映射表中补max,不能默认成 1。options单独收集,方便后面做选项级核对。

2.4 结构化落地:字段约定与条目数自检

解析结果落成固定结构,后面的计分引擎只认这个结构,不再碰原始文档。

{ "scale": "SIB", "source_file": "严重障碍量表SIB.doc", "source_type": "ole2-doc", "items": [ {"no": 1, "text": "……", "max": 3, "subscale": "attention", "options": []} ] }

scalesource_type是审计字段,出问题时能回溯到具体文件和解析路径。落到库之前做三项自检:条目号不重复、条目号基本连续、max求和等于文档标注的总分。第二条允许有跳号(有些版本条目号带小项后缀),但跳号超过三处就该人工看一眼。

def self_check(data, declared_total=100): nos = [i["no"] for i in data["items"]] assert len(set(nos)) == len(nos), "条目号重复" assert all(i["max"] for i in data["items"]), "存在未标注满分的条目" s = sum(i["max"] for i in data["items"]) print(f"条目数={len(nos)} 满分合计={s} 声明总分={declared_total}") return s == declared_total

这个「满分合计」校验成本极低,却很能抓错——分值解析漏一条、多抓一条,合计立刻对不上。

3. SIB 计分引擎:分量表映射、缺失值策略与边界护栏

3.1 条目到分量表的映射表怎么建

映射表是整条链路上唯一必须人工确认的东西。常见文献把 SIB 划成注意、定向、语言、记忆、视空间、结构、运用、社交互动、进食等若干分量表,但每个分量表具体覆盖哪些条目号,不同版本差异不小,别照抄网上的表。用一份 CSV 维护,格式参考如下(示例行仅示意格式):

item_nosubscalemax_score
1attention3
2attention3
20memory4

CSV 比硬编码在代码里好:版本变更时改数据不改逻辑,也方便让临床同事直接核对。加载时统一做类型转换,条目号转int,避免"1"1在字典里对不上。

3.2 计分函数:缺失值、跳转与部分完成怎么算

def score_sib(responses, mapping, policy="strict"): """responses: {item_no: score};mapping: [{no, max, subscale}]""" detail, total, answered = {}, 0, 0 for row in mapping: no, mx = row["no"], row["max"] if no not in responses: if policy == "strict": # 缺一条就拒绝出分 raise ValueError(f"条目 {no} 缺失") if policy == "zero": # 缺失记 0,重度场景下会系统性拉低总分 val = 0 else: continue # pro_rate:先跳过,最后按比例折算 else: val = responses[no] answered += 1 if not 0 <= val <= mx: raise ValueError(f"条目 {no} 得分 {val} 超出 0~{mx}") total += val detail.setdefault(row["subscale"], 0) detail[row["subscale"]] += val if policy == "pro_rate": full = sum(r["max"] for r in mapping) total = round(total * full / sum(r["max"] for r in mapping if r["no"] in responses)) return {"total": total, "subscales": detail, "answered": answered}

policy是最关键的参数。科研和随访场景我默认strict,缺一条就抛异常,宁可回去补测,也不要一个悄悄折算过的分数流进数据集。zero用在质控扫库阶段可以,但必须打标。pro_rate只在量表内部条目高度同质、且缺失是随机发生时才勉强成立,折算结果要额外记一个标志位。

0 <= val <= mx这行范围校验看着啰嗦,实际拦掉的错最多:录入把3打成33、把两道题的分加在一起填进去,都在这儿暴露。

3.3 三个必调参数的取值与影响

参数可选值影响建议默认
缺失策略strict / zero / pro_rate决定缺条目时总分是否可用strict
满分口径文档标注 / 条目累加决定总分是否与他人的数据可比文档标注,条目累加做自检
分量表折算原始分 / 百分比随访比较是否受条目缺失干扰横断面用原始分,随访用百分比

口径不一致是跨中心数据合并里最隐蔽的问题:两边都是「总分 87」,一个基于文档声明的 100 分制,一个基于条目累加得到的 96 分制,合在一起算均值就是错的。所以每次计分都要把policy和满分口径写进结果记录,而不是只存一个数字。

3.4 和人工计分对照:抽 5 份做逐条目回归

上线前一定要和人工计分对一遍,方法很土但有效:让同事在 Excel 里按条目号一列列填分,脚本逐条目 diff,定位到第一条不一致就停,比看总分更快找到根因。

def diff_with_manual(auto, manual): for no in sorted(manual): if auto.get(no) != manual[no]: return f"首处不一致:条目 {no},自动={auto.get(no)} 人工={manual[no]}" return "全部一致"

总分一致但明细不一致的情况最危险——分量表分析会整体偏掉,所以必须逐条目比,不能只比总分。

4. 落库:SIB 评分表设计、批量入库与纵向随访查询

4.1 表结构:一次评估一行,条目明细单独存

CREATE TABLE sib_assessment ( id INTEGER PRIMARY KEY, subject_code TEXT NOT NULL, -- 受试者编号,不存姓名等直接标识 assessed_on DATE NOT NULL, total_score INTEGER NOT NULL CHECK (total_score BETWEEN 0 AND 100), policy TEXT NOT NULL DEFAULT 'strict', source_doc TEXT, -- 溯源:哪份 SIB.doc 算出来的 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE sib_item_score ( assessment_id INTEGER NOT NULL REFERENCES sib_assessment(id), item_no INTEGER NOT NULL, subscale TEXT NOT NULL, score INTEGER NOT NULL, max_score INTEGER NOT NULL, PRIMARY KEY (assessment_id, item_no) );

明细单独建表的理由很实在:算分量表、做条目级变化分析、复核错分,都要用到条目粒度;只存总分的话,发现异常时只能回去翻原始文档。CHECK约束是最后一道护栏,应用层漏掉的范围校验在这里兜底。参数字段policy不要省,它是复现一次计分的必要信息。

4.2 批量入库:一个目录的 SIB.doc 一次跑完

import sqlite3, pathlib, json def import_dir(folder, conn, scorer): ok, failed = 0, [] for p in sorted(pathlib.Path(folder).glob("*.doc*")): try: data = parse_and_map(p) # 解析 + 映射 res = scorer(data["responses"], data["mapping"], policy="strict") with conn: # 单份文件一个事务 cur = conn.execute( "INSERT INTO sib_assessment(subject_code, assessed_on," " total_score, policy, source_doc) VALUES (?,?,?,?,?)", (data["subject_code"], data["assessed_on"], res["total"], "strict", p.name)) conn.executemany( "INSERT INTO sib_item_score VALUES (?,?,?,?,?)", [(cur.lastrowid, r["no"], r["subscale"], data["responses"][r["no"]], r["max"]) for r in data["mapping"]]) ok += 1 except Exception as e: # 单份失败不影响整批 failed.append((p.name, str(e))) return ok, failed

with conn保证单份文件的评估主表与明细表要么都进、要么都不进,不会出现有总分没明细的脏数据。异常按文件粒度捕获,跑完打印failed列表统一处理——几十份文件里通常只有两三份是扫描件或缺条目,没必要为它们中断整批。subject_codeassessed_on从文件名或单独的对照表来,别指望从量表正文里抠,容易抓错。

4.3 纵向随访查询:用窗口函数直接算变化量

SELECT subject_code, assessed_on, total_score, total_score - LAG(total_score) OVER ( PARTITION BY subject_code ORDER BY assessed_on) AS delta FROM sib_assessment ORDER BY subject_code, assessed_on;

LAG取同一受试者的上一次总分,delta就是相邻两次随访的变化量。第一次随访delta为 NULL,属正常。要注意两点:变化量的解读必须带上随访间隔,隔三个月的降 5 分和隔一年的降 5 分不是一回事;不同policy算出来的分数不要放进同一个序列比,WHERE policy = 'strict'该加就加。

5. SIB 短版抽取与报告生成里的具体技巧

5.1 用白名单从条目池里稳定抽出短版条目

短版量表通常只是从完整条目里挑固定几条,别重新解析一份文档,直接从同一份结构化 JSON 里按条目号白名单取,口径天然一致。

SHORT_FORM = {3, 5, 9, 12, 17, 21, 26, 33} # 条目号以实际版本为准 def extract_short(data, whitelist=SHORT_FORM): pool = {i["no"]: i for i in data["items"]} missing = whitelist - pool.keys() if missing: raise ValueError(f"短版条目缺失:{sorted(missing)}") # 版本不匹配直接报错 return [pool[no] for no in sorted(whitelist)]

missing检查是这套做法的核心价值:换了一个修订版本的 SIB.doc,条目号一漂移,脚本立刻报错,而不是安静地算出一个错的短版分数。白名单建议放进配置文件,由临床同事签字确认后再定版。

5.2 报告生成里最容易出的三类错

现象根因处理方式
总分与明细对不上报告取了解析出的分值而非计分结果报告只读计分引擎输出,禁止二次求和
缺失条目被当成 0读取时用dict.get(no, 0)读分处改用strict校验,缺就抛
随访变化量正负号反了LAG的排序方向或减数写反固定写当前 - 上一次,并单测一条上升样本

生成报告时还有个小技巧:把policy、满分口径、条目数这几个元信息一起印在报告页脚。三个月后有人质疑某个分数,翻页脚就能定位到当时的计分口径,不用去翻那次跑批的日志。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 21:50:03

Open Agents环境变量管理:如何快速用 vc env pull 搞定多环境配置

Open Agents环境变量管理&#xff1a;如何快速用 vc env pull 搞定多环境配置 【免费下载链接】open-agents An open source template for building cloud agents. 项目地址: https://gitcode.com/GitHub_Trending/op/open-agents Open Agents 是一个构建云端 AI 编程 A…

作者头像 李华
网站建设 2026/9/17 21:49:55

IAR Cp001授权校验失败排查:License Manager与主机标识

上周帮隔壁组同事收拾一台新装的开发机&#xff0c;IAR 装完之后双击图标&#xff0c;界面还没出来就弹了个框&#xff1a;Error[Cp001]: Copy protection check。他第一反应是安装包坏了&#xff0c;删了重装三遍&#xff0c;问题原封不动。这类 IAR 安装报错其实特别常见&…

作者头像 李华