Improve patient list OCR cleanup
This commit is contained in:
@@ -463,12 +463,30 @@ def ocr_response_to_rows(response: dict[str, Any], engine: str, image_width: int
|
|||||||
|
|
||||||
def normalize_date(text: str) -> str:
|
def normalize_date(text: str) -> str:
|
||||||
text = normalize_text(text).replace("/", "-").replace(".", "-")
|
text = normalize_text(text).replace("/", "-").replace(".", "-")
|
||||||
text = re.sub(r"(\d{4})-(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})", date_repl, text)
|
text = re.sub(
|
||||||
|
r"(\d{4})-(\d{1,2})-(\d{1,2})\s+(\d{1,4}):(\d{1,2})(?::(\d{1,2}))?",
|
||||||
|
date_repl,
|
||||||
|
text,
|
||||||
|
)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
def date_repl(match: re.Match[str]) -> str:
|
def date_repl(match: re.Match[str]) -> str:
|
||||||
year, month, day, hour, minute, second = match.groups()
|
year, month, day, hour_text, minute_text, second_text = match.groups()
|
||||||
|
if second_text is None and len(hour_text) > 2:
|
||||||
|
if len(hour_text) == 3:
|
||||||
|
hour = int(hour_text[:2])
|
||||||
|
minute = int(hour_text[2:])
|
||||||
|
else:
|
||||||
|
hour = int(hour_text[:-2])
|
||||||
|
minute = int(hour_text[-2:])
|
||||||
|
second = int(minute_text)
|
||||||
|
else:
|
||||||
|
hour = int(hour_text)
|
||||||
|
minute = int(minute_text)
|
||||||
|
second = int(second_text or 0)
|
||||||
|
if not (0 <= hour <= 23 and 0 <= minute <= 59 and 0 <= second <= 59):
|
||||||
|
return match.group(0)
|
||||||
return f"{int(year):04d}-{int(month):02d}-{int(day):02d} {int(hour):02d}:{int(minute):02d}:{int(second):02d}"
|
return f"{int(year):04d}-{int(month):02d}-{int(day):02d} {int(hour):02d}:{int(minute):02d}:{int(second):02d}"
|
||||||
|
|
||||||
|
|
||||||
@@ -485,19 +503,24 @@ def clean_patient_row(row: list[str]) -> dict[str, Any]:
|
|||||||
values["性别"] = sex_age.group(1)
|
values["性别"] = sex_age.group(1)
|
||||||
if not values["年龄"]:
|
if not values["年龄"]:
|
||||||
values["年龄"] = sex_age.group(2)
|
values["年龄"] = sex_age.group(2)
|
||||||
values["住院号"] = re.sub(r"\s+", "", values["住院号"]).upper()
|
def normalize_inpatient_no(value: Any) -> str:
|
||||||
if values["住院号"].startswith("ZV"):
|
inpatient_no = re.sub(r"\s+", "", normalize_text(value)).upper()
|
||||||
values["住院号"] = "ZY" + values["住院号"][2:]
|
if inpatient_no.startswith("ZV"):
|
||||||
if values["住院号"].startswith("ZYS"):
|
inpatient_no = "ZY" + inpatient_no[2:]
|
||||||
values["住院号"] = "ZY5" + values["住院号"][3:]
|
if inpatient_no.startswith("ZYS"):
|
||||||
if len(values["住院号"]) > 2:
|
inpatient_no = "ZY5" + inpatient_no[3:]
|
||||||
prefix, number = values["住院号"][:2], values["住院号"][2:]
|
if len(inpatient_no) > 2:
|
||||||
values["住院号"] = prefix + number.translate(str.maketrans({"O": "0", "I": "1", "L": "1", "S": "5"}))
|
prefix, number = inpatient_no[:2], inpatient_no[2:]
|
||||||
|
inpatient_no = prefix + number.translate(str.maketrans({"O": "0", "I": "1", "L": "1", "S": "5"}))
|
||||||
|
return inpatient_no
|
||||||
|
|
||||||
|
values["住院号"] = normalize_inpatient_no(values["住院号"])
|
||||||
|
|
||||||
def looks_datetime(value: Any) -> bool:
|
def looks_datetime(value: Any) -> bool:
|
||||||
return bool(
|
return bool(
|
||||||
re.fullmatch(
|
re.fullmatch(
|
||||||
r"\d{4}[-/.]\d{1,2}[-/.]\d{1,2}\s+\d{1,2}:\d{1,2}:\d{1,2}",
|
r"\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}",
|
||||||
str(value),
|
normalize_date(str(value)),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -510,8 +533,47 @@ def clean_patient_row(row: list[str]) -> dict[str, Any]:
|
|||||||
values["入院时间"] = normalize_date(values["入院时间"])
|
values["入院时间"] = normalize_date(values["入院时间"])
|
||||||
values["最后书写时间"] = normalize_date(values["最后书写时间"])
|
values["最后书写时间"] = normalize_date(values["最后书写时间"])
|
||||||
values["出院时间"] = normalize_date(values["出院时间"])
|
values["出院时间"] = normalize_date(values["出院时间"])
|
||||||
|
|
||||||
|
long_name_shift = re.fullmatch(r"(.+?)(?:\.\.\.)?\s*(男|女)", values["姓名"])
|
||||||
|
if (
|
||||||
|
long_name_shift
|
||||||
|
and values["性别"].endswith("岁")
|
||||||
|
and re.fullmatch(r"Z[YV]\w+", values["年龄"], re.IGNORECASE)
|
||||||
|
and values["住院号"]
|
||||||
|
):
|
||||||
|
old_sex = values["性别"]
|
||||||
|
old_age = values["年龄"]
|
||||||
|
old_diagnosis = values["住院号"]
|
||||||
|
values["姓名"] = normalize_text(long_name_shift.group(1))
|
||||||
|
values["性别"] = long_name_shift.group(2)
|
||||||
|
values["年龄"] = old_sex
|
||||||
|
values["住院号"] = normalize_inpatient_no(old_age)
|
||||||
|
if looks_datetime(values["诊断"]) and looks_datetime(values["入院时间"]) and looks_days(values["最后书写时间"]):
|
||||||
|
old_admission = values["诊断"]
|
||||||
|
old_last_write = values["入院时间"]
|
||||||
|
old_days = values["最后书写时间"]
|
||||||
|
old_discharge = values["住院天数"]
|
||||||
|
old_postop = values["出院时间"]
|
||||||
|
values["诊断"] = old_diagnosis
|
||||||
|
values["入院时间"] = normalize_date(old_admission)
|
||||||
|
values["最后书写时间"] = normalize_date(old_last_write)
|
||||||
|
values["住院天数"] = old_days
|
||||||
|
values["出院时间"] = normalize_date(old_discharge) if looks_datetime(old_discharge) else ""
|
||||||
|
if looks_postop(old_postop) and not values["手术后天数"]:
|
||||||
|
values["手术后天数"] = old_postop
|
||||||
|
elif not values["诊断"]:
|
||||||
|
values["诊断"] = old_diagnosis
|
||||||
|
|
||||||
|
admission_with_diagnosis = re.search(
|
||||||
|
r"(.+?)\s*-*\s*(\d{4}[-/.]\d{1,2}[-/.]\d{1,2}\s+\d{1,4}:\d{1,2}(?::\d{1,2})?)\s*$",
|
||||||
|
values["入院时间"],
|
||||||
|
)
|
||||||
|
if admission_with_diagnosis and not looks_datetime(values["入院时间"]):
|
||||||
|
values["诊断"] = normalize_text(f"{values['诊断']} {admission_with_diagnosis.group(1)}")
|
||||||
|
values["入院时间"] = normalize_date(admission_with_diagnosis.group(2))
|
||||||
|
|
||||||
shifted_by_long_diagnosis = re.search(
|
shifted_by_long_diagnosis = re.search(
|
||||||
r"(\d{4}[-/.]\d{1,2}[-/.]\d{1,2}\s+\d{1,2}:\d{1,2}:\d{1,2})$",
|
r"(\d{4}[-/.]\d{1,2}[-/.]\d{1,2}\s+\d{1,4}:\d{1,2}(?::\d{1,2})?)\s*[))\]]*$",
|
||||||
values["诊断"],
|
values["诊断"],
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
@@ -539,6 +601,25 @@ def clean_patient_row(row: list[str]) -> dict[str, Any]:
|
|||||||
values["诊断"] = values["诊断"][: shifted_by_long_diagnosis.start()].strip()
|
values["诊断"] = values["诊断"][: shifted_by_long_diagnosis.start()].strip()
|
||||||
values["入院时间"] = normalize_date(shifted_by_long_diagnosis.group(1))
|
values["入院时间"] = normalize_date(shifted_by_long_diagnosis.group(1))
|
||||||
|
|
||||||
|
if (
|
||||||
|
not looks_datetime(values["入院时间"])
|
||||||
|
and looks_datetime(values["最后书写时间"])
|
||||||
|
and looks_datetime(values["住院天数"])
|
||||||
|
and looks_days(values["出院时间"])
|
||||||
|
):
|
||||||
|
diagnosis_extra = values["入院时间"]
|
||||||
|
old_admission = values["最后书写时间"]
|
||||||
|
old_last_write = values["住院天数"]
|
||||||
|
old_days = values["出院时间"]
|
||||||
|
old_discharge = values["手术后天数"]
|
||||||
|
if diagnosis_extra:
|
||||||
|
values["诊断"] = normalize_text(f"{values['诊断']} {diagnosis_extra}")
|
||||||
|
values["入院时间"] = normalize_date(old_admission)
|
||||||
|
values["最后书写时间"] = normalize_date(old_last_write)
|
||||||
|
values["住院天数"] = old_days
|
||||||
|
values["出院时间"] = normalize_date(old_discharge) if looks_datetime(old_discharge) else ""
|
||||||
|
values["手术后天数"] = ""
|
||||||
|
|
||||||
days_with_discharge = re.fullmatch(
|
days_with_discharge = re.fullmatch(
|
||||||
r"(\d+)\s+(\d{4}[-/.]\d{1,2}[-/.]\d{1,2}\s+\d{1,2}:\d{1,2}:\d{1,2})",
|
r"(\d+)\s+(\d{4}[-/.]\d{1,2}[-/.]\d{1,2}\s+\d{1,2}:\d{1,2}:\d{1,2})",
|
||||||
str(values["住院天数"]),
|
str(values["住院天数"]),
|
||||||
@@ -551,6 +632,10 @@ def clean_patient_row(row: list[str]) -> dict[str, Any]:
|
|||||||
if looks_postop(values["出院时间"]) and not values["手术后天数"]:
|
if looks_postop(values["出院时间"]) and not values["手术后天数"]:
|
||||||
values["手术后天数"] = values["出院时间"]
|
values["手术后天数"] = values["出院时间"]
|
||||||
values["出院时间"] = ""
|
values["出院时间"] = ""
|
||||||
|
if values["出院时间"] in {"手术日", "当日", "当天"}:
|
||||||
|
if not values["手术后天数"]:
|
||||||
|
values["手术后天数"] = values["出院时间"]
|
||||||
|
values["出院时间"] = ""
|
||||||
if looks_postop(values["住院天数"]) and not values["手术后天数"]:
|
if looks_postop(values["住院天数"]) and not values["手术后天数"]:
|
||||||
values["手术后天数"] = values["住院天数"]
|
values["手术后天数"] = values["住院天数"]
|
||||||
values["住院天数"] = ""
|
values["住院天数"] = ""
|
||||||
@@ -694,6 +779,7 @@ def apply_corrections(records: list[dict[str, Any]], corrections: dict[tuple[str
|
|||||||
continue
|
continue
|
||||||
correction = corrections[key]
|
correction = corrections[key]
|
||||||
record["患者信息"].update(correction.get("患者信息", {}))
|
record["患者信息"].update(correction.get("患者信息", {}))
|
||||||
|
record["图片信息"].update(correction.get("图片信息", {}))
|
||||||
record["复核"]["人工修正"] = True
|
record["复核"]["人工修正"] = True
|
||||||
record["复核"]["复核选项"] = correction.get("复核选项", {})
|
record["复核"]["复核选项"] = correction.get("复核选项", {})
|
||||||
if correction.get("复核备注"):
|
if correction.get("复核备注"):
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import argparse
|
|||||||
import csv
|
import csv
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -22,6 +23,7 @@ DEFAULT_RESULT_JSON = Path("数据处理结果区/合并_患者列表_结构化.
|
|||||||
DEFAULT_SCHEMA_SQL = Path("数据处理工作区/06_PostgreSQL建表结构.sql")
|
DEFAULT_SCHEMA_SQL = Path("数据处理工作区/06_PostgreSQL建表结构.sql")
|
||||||
|
|
||||||
TABLE_NAME = '"Patient_Lists"'
|
TABLE_NAME = '"Patient_Lists"'
|
||||||
|
DATE_TIME_RE = re.compile(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$")
|
||||||
|
|
||||||
PATIENT_FIELDS = [
|
PATIENT_FIELDS = [
|
||||||
"batch_name",
|
"batch_name",
|
||||||
@@ -75,6 +77,16 @@ def review_note_items(review: dict[str, Any]) -> list[str]:
|
|||||||
return note_items
|
return note_items
|
||||||
|
|
||||||
|
|
||||||
|
def admission_after_discharge(admission_time: Any, discharge_time: Any) -> bool:
|
||||||
|
admission = scalar(admission_time).strip()
|
||||||
|
discharge = scalar(discharge_time).strip()
|
||||||
|
if not admission or not discharge:
|
||||||
|
return False
|
||||||
|
if not DATE_TIME_RE.match(admission) or not DATE_TIME_RE.match(discharge):
|
||||||
|
return False
|
||||||
|
return admission > discharge
|
||||||
|
|
||||||
|
|
||||||
def psql_quote(path: Path) -> str:
|
def psql_quote(path: Path) -> str:
|
||||||
return "'" + str(path.resolve()).replace("'", "''") + "'"
|
return "'" + str(path.resolve()).replace("'", "''") + "'"
|
||||||
|
|
||||||
@@ -87,6 +99,17 @@ def flatten_patient_record(record: dict[str, Any]) -> dict[str, Any]:
|
|||||||
hospital_days, hospital_days_note = clean_integer(patient["住院天数"])
|
hospital_days, hospital_days_note = clean_integer(patient["住院天数"])
|
||||||
if hospital_days_note:
|
if hospital_days_note:
|
||||||
note_items.append(hospital_days_note)
|
note_items.append(hospital_days_note)
|
||||||
|
review_status = review["状态"]
|
||||||
|
patient_name = scalar(patient["姓名"]).strip()
|
||||||
|
if not patient_name:
|
||||||
|
patient_name = "缺少姓名"
|
||||||
|
review_status = "需人工复核"
|
||||||
|
if "缺少姓名" not in note_items:
|
||||||
|
note_items.append("缺少姓名")
|
||||||
|
if admission_after_discharge(patient["入院时间"], patient["出院时间"]):
|
||||||
|
review_status = "需人工复核"
|
||||||
|
if "入院时间晚于出院时间" not in note_items:
|
||||||
|
note_items.append("入院时间晚于出院时间")
|
||||||
review_notes = ";".join(note_items)
|
review_notes = ";".join(note_items)
|
||||||
return {
|
return {
|
||||||
"batch_name": record["处理批次"],
|
"batch_name": record["处理批次"],
|
||||||
@@ -96,7 +119,7 @@ def flatten_patient_record(record: dict[str, Any]) -> dict[str, Any]:
|
|||||||
"image_path": image["图片路径"],
|
"image_path": image["图片路径"],
|
||||||
"image_name": image["图片名"],
|
"image_name": image["图片名"],
|
||||||
"image_row_no": image["图片内行号"],
|
"image_row_no": image["图片内行号"],
|
||||||
"patient_name": patient["姓名"],
|
"patient_name": patient_name,
|
||||||
"gender": patient["性别"],
|
"gender": patient["性别"],
|
||||||
"age": patient["年龄"],
|
"age": patient["年龄"],
|
||||||
"inpatient_no": patient["住院号"],
|
"inpatient_no": patient["住院号"],
|
||||||
@@ -106,7 +129,7 @@ def flatten_patient_record(record: dict[str, Any]) -> dict[str, Any]:
|
|||||||
"hospital_days": hospital_days,
|
"hospital_days": hospital_days,
|
||||||
"discharge_time": patient["出院时间"],
|
"discharge_time": patient["出院时间"],
|
||||||
"postoperative_days": patient["手术后天数"],
|
"postoperative_days": patient["手术后天数"],
|
||||||
"review_status": review["状态"],
|
"review_status": review_status,
|
||||||
"review_notes": review_notes,
|
"review_notes": review_notes,
|
||||||
"manual_corrected": bool(review.get("人工修正")),
|
"manual_corrected": bool(review.get("人工修正")),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,37 @@ CREATE INDEX IF NOT EXISTS idx_patient_lists_patient_name ON "Patient_Lists"(pat
|
|||||||
CREATE INDEX IF NOT EXISTS idx_patient_lists_review_status ON "Patient_Lists"(review_status);
|
CREATE INDEX IF NOT EXISTS idx_patient_lists_review_status ON "Patient_Lists"(review_status);
|
||||||
CREATE INDEX IF NOT EXISTS idx_patient_lists_audit_result ON "Patient_Lists"(audit_result);
|
CREATE INDEX IF NOT EXISTS idx_patient_lists_audit_result ON "Patient_Lists"(audit_result);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint
|
||||||
|
WHERE conrelid = '"Patient_Lists"'::regclass
|
||||||
|
AND conname = 'ck_patient_lists_inpatient_no_present'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "Patient_Lists"
|
||||||
|
ADD CONSTRAINT ck_patient_lists_inpatient_no_present
|
||||||
|
CHECK (btrim(inpatient_no) <> '') NOT VALID;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint
|
||||||
|
WHERE conrelid = '"Patient_Lists"'::regclass
|
||||||
|
AND conname = 'ck_patient_lists_admission_before_discharge'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "Patient_Lists"
|
||||||
|
ADD CONSTRAINT ck_patient_lists_admission_before_discharge
|
||||||
|
CHECK (
|
||||||
|
COALESCE(discharge_time, '') = ''
|
||||||
|
OR COALESCE(admission_time, '') = ''
|
||||||
|
OR admission_time !~ '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$'
|
||||||
|
OR discharge_time !~ '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$'
|
||||||
|
OR admission_time <= discharge_time
|
||||||
|
OR review_status = '需人工复核'
|
||||||
|
) NOT VALID;
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
COMMENT ON TABLE "Patient_Lists" IS 'HIS患者列表图片OCR归档后的正式患者记录单表';
|
COMMENT ON TABLE "Patient_Lists" IS 'HIS患者列表图片OCR归档后的正式患者记录单表';
|
||||||
COMMENT ON COLUMN "Patient_Lists".record_id IS '患者记录主键,数据库自动生成';
|
COMMENT ON COLUMN "Patient_Lists".record_id IS '患者记录主键,数据库自动生成';
|
||||||
COMMENT ON COLUMN "Patient_Lists".batch_name IS '处理批次名称,通常为原始图片集群文件夹名';
|
COMMENT ON COLUMN "Patient_Lists".batch_name IS '处理批次名称,通常为原始图片集群文件夹名';
|
||||||
|
|||||||
Reference in New Issue
Block a user