Files
HIS/患者首页处理/数据处理工作区/04_质量体检/04_字段核验与数据库体检.py
2026-05-27 00:21:16 +08:00

176 lines
6.8 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
对目标首页表做字段级体检。
输出:
- 数据处理结果区/05_质量体检/患者首页_数据库体检报告.txt
- 数据处理结果区/05_质量体检/患者首页_字段空值统计.csv
- 数据处理结果区/05_质量体检/患者首页_疑似异常记录.csv
"""
from __future__ import annotations
import argparse
import os
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_OUTPUT_DIR = PROJECT_ROOT / "数据处理结果区" / "05_质量体检"
DEFAULT_TABLE = os.environ.get("PGTABLE") or os.environ.get("PG_PATIENT_TABLE", "")
def quote_pg_identifier(identifier: str) -> str:
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", identifier):
raise ValueError(f"非法 PostgreSQL 表名:{identifier}")
return '"' + identifier.replace('"', '""') + '"'
def connection_env(args: argparse.Namespace) -> dict[str, str]:
env = os.environ.copy()
env.update(
{
"PGHOST": args.pg_host or os.environ.get("PGHOST", ""),
"PGPORT": str(args.pg_port or os.environ.get("PGPORT", "")),
"PGDATABASE": args.pg_database or os.environ.get("PGDATABASE", ""),
"PGUSER": args.pg_user or os.environ.get("PGUSER", ""),
"PGPASSWORD": args.pg_password or os.environ.get("PGPASSWORD", ""),
}
)
missing = [name for name in ["PGHOST", "PGPORT", "PGDATABASE", "PGUSER", "PGPASSWORD"] if not env.get(name)]
if missing:
raise RuntimeError("缺少 PostgreSQL 连接配置:" + "".join(missing))
return env
def run_psql(sql: str, args: argparse.Namespace) -> str:
if shutil.which("psql") is None:
raise RuntimeError("未找到 psql。请先安装 PostgreSQL 客户端。")
with tempfile.TemporaryDirectory(prefix="patient_front_audit_") as tmpdir:
sql_path = Path(tmpdir) / "audit.sql"
sql_path.write_text(sql, encoding="utf-8")
completed = subprocess.run(
["psql", "--no-password", "--file", str(sql_path)],
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
env=connection_env(args),
)
if completed.returncode != 0:
raise RuntimeError(completed.stderr.strip() or completed.stdout.strip() or "psql 执行失败")
return completed.stdout
def build_empty_stat_sql(table: str) -> str:
monitored_columns = [
"inpatient_no",
"medical_record_no",
"patient_name",
"gender",
"birth_date",
"id_card_no",
"contact_address",
"admission_time",
"discharge_time",
"major_department",
"primary_diagnosis",
"primary_diagnosis_code",
"discharge_diagnoses",
"operations",
"quality_status",
"review_status",
]
selects = []
for column in monitored_columns:
identifier = quote_pg_identifier(column)
if column in {"discharge_diagnoses", "operations"}:
empty_expr = f"{identifier} IS NULL OR jsonb_array_length({identifier}) = 0"
else:
empty_expr = f"{identifier} IS NULL OR {identifier}::text = ''"
selects.append(f"SELECT '{column}' AS column_name, count(*) FILTER (WHERE {empty_expr}) AS empty_count, count(*) AS total_count FROM {table}")
return " UNION ALL ".join(selects)
def audit(args: argparse.Namespace) -> None:
output_dir = args.output_dir.resolve()
output_dir.mkdir(parents=True, exist_ok=True)
table = quote_pg_identifier(args.pg_table)
empty_csv = output_dir / "患者首页_字段空值统计.csv"
suspicious_csv = output_dir / "患者首页_疑似异常记录.csv"
report_path = output_dir / "患者首页_数据库体检报告.txt"
empty_stat_sql = build_empty_stat_sql(table)
suspicious_sql = (
"SELECT source_file, inpatient_no, medical_record_no, patient_name, review_status, "
"review_notes::text AS review_notes, contact_address, primary_diagnosis, primary_diagnosis_code "
f"FROM {table} "
"WHERE review_status <> 'auto_pass' "
"OR inpatient_no !~ '^ZY\\d{12}$' "
"OR medical_record_no !~ '^\\d{10}$' "
"OR contact_address ~ '急诊|门诊|其他医疗机构转入|医嘱离院' "
"OR primary_diagnosis_code IS NULL "
"OR primary_diagnosis_code = '' "
"ORDER BY source_file"
)
sql = f"""
\\set ON_ERROR_STOP on
\\pset pager off
\\o {str(report_path).replace("'", "''")}
SELECT '总记录数' AS item, count(*)::text AS value FROM {table}
UNION ALL
SELECT '需复核记录数', count(*)::text FROM {table} WHERE review_status <> 'auto_pass'
UNION ALL
SELECT '缺字段注释数', count(*)::text
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN pg_attribute a ON a.attrelid = c.oid
WHERE n.nspname = 'public'
AND c.relname = {args.pg_table!r}
AND a.attnum > 0
AND NOT a.attisdropped
AND col_description(c.oid, a.attnum) IS NULL;
\\o
\\copy ({empty_stat_sql}) TO '{str(empty_csv).replace("'", "''")}' WITH (FORMAT csv, HEADER true, ENCODING 'UTF8')
\\copy ({suspicious_sql}) TO '{str(suspicious_csv).replace("'", "''")}' WITH (FORMAT csv, HEADER true, ENCODING 'UTF8')
""".strip()
stdout = run_psql(sql, args)
if stdout.strip():
print(stdout.strip())
print(f"体检报告:{report_path}")
print(f"字段空值统计:{empty_csv}")
print(f"疑似异常记录:{suspicious_csv}")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="核验目标首页表字段注释、空值和疑似错位记录。")
parser.add_argument("--pg-host", default=os.environ.get("PGHOST", ""), help="PostgreSQL 主机;也可用 PGHOST")
parser.add_argument("--pg-port", default=os.environ.get("PGPORT", "5432"), help="PostgreSQL 端口;也可用 PGPORT")
parser.add_argument("--pg-database", default=os.environ.get("PGDATABASE", ""), help="PostgreSQL 数据库;也可用 PGDATABASE")
parser.add_argument("--pg-user", default=os.environ.get("PGUSER", ""), help="PostgreSQL 用户名;也可用 PGUSER")
parser.add_argument("--pg-password", default=os.environ.get("PGPASSWORD", ""), help="PostgreSQL 密码;也可用 PGPASSWORD")
parser.add_argument("--pg-table", default=os.environ.get("PG_PATIENT_TABLE", DEFAULT_TABLE), help="PostgreSQL 表名")
parser.add_argument("-o", "--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR, help="体检结果输出目录")
return parser
def main() -> int:
audit(build_parser().parse_args())
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as exc: # noqa: BLE001
print(f"错误:{exc}", file=sys.stderr)
raise SystemExit(1)