import re import json import time import hashlib import streamlit as st from transformers import pipeline st.set_page_config(page_title="Record Guard", page_icon="🛡️", layout="wide") MEDICAL_WHITELIST = [ "糖尿病", "高血壓", "高血脂", "冠心病", "惡性腫瘤", "肺炎", "發燒", "阿斯匹靈", "Metformin", "Aspirin", "胰島素", "抗生素", "心電圖", "血糖", "血壓", "X光", "HbA1c", "電腦斷層", "Glucose", "Hemoglobin", "Cholesterol", "COVID-19" ] def calculate_sha256(text): if not text: return "" return hashlib.sha256(text.encode('utf-8')).hexdigest() @st.cache_resource def load_ner_model(): return pipeline("ner", model="ckiplab/bert-base-chinese-ner", aggregation_strategy="simple") def hl7_structural_masking(text): lines = text.split('\n') masked_lines = [] found_types = [] for line in lines: if line.startswith("PID|"): fields = line.split('|') if len(fields) > 3 and fields[3]: fields[3] = "[身分證/病歷號:***]" found_types.append("HL7:身分證") if len(fields) > 5 and fields[5]: fields[5] = "[姓名:***]" found_types.append("HL7:姓名") if len(fields) > 7 and fields[7]: fields[7] = "[出生日期:***]" found_types.append("HL7:生日") if len(fields) > 11 and fields[11]: fields[11] = "[地址:***]" found_types.append("HL7:地址") if len(fields) > 13 and fields[13]: fields[13] = "[電話:***]" found_types.append("HL7:電話") masked_line = '|'.join(fields) masked_lines.append(masked_line) else: masked_lines.append(line) return '\n'.join(masked_lines), list(set(found_types)) def advanced_regex_masking(text): # 🕵️‍♂️ 專業升級:補齊 HIPAA 與台灣 PDPA 的數位與數字特徵 patterns = { "身分證": r"[A-Z][12]\d{8}", "健保卡/帳戶": r"\b\d{12,16}\b", # 抓取 12碼健保卡號 或 銀行帳號 "電話": r"(09\d{2}-?\d{3}-?\d{3})|(0\d{1,2}-?\d{6,8})", "Email": r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+", "網址/IP": r"(https?://[^\s]+)|(\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b)", # 抓取數位足跡 "出生日期": r"(\d{2,4}[年/-]\d{1,2}[月/-]\d{1,2}[日]?)" } masked_text = text found_types = [] for label, pattern in patterns.items(): matches = re.finditer(pattern, masked_text) for match in matches: if match.group() in MEDICAL_WHITELIST: continue found_types.append(label) for label, pattern in patterns.items(): if label == "出生日期": masked_text = re.sub(pattern, lambda m: m.group() if m.group() in MEDICAL_WHITELIST else f"{m.group()[:4].replace('/','').replace('-','')}年[月日隱匿:***]", masked_text) else: masked_text = re.sub(pattern, lambda m: m.group() if m.group() in MEDICAL_WHITELIST else f"[{label}:***]", masked_text) address_pattern = r"(\w{2,3}(市|縣)\w{2,3}(區|市|鎮|鄉)\w{1,5}(路|街|大道|村|里)(.+?[號樓])?)" if re.search(address_pattern, masked_text): found_types.append("地址") masked_text = re.sub(address_pattern, "[詳細地址:***]", masked_text) return masked_text, list(set(found_types)) def ai_deep_scan(text, ner_pipeline): results = ner_pipeline(text) masked_text = text sorted_results = sorted(results, key=lambda x: x['start'], reverse=True) caught_words = [] whitelist_saved = [] for ent in sorted_results: grp = ent['entity_group'] word = ent['word'] if "***" in word or "[" in word or "]" in word: continue if any(w in word for w in MEDICAL_WHITELIST) or any(word in w for w in MEDICAL_WHITELIST): whitelist_saved.append(word) continue if grp in ["PERSON", "LOC", "GPE", "FAC", "ORG"]: label_map = {"PERSON": "姓名", "LOC": "地點", "GPE": "行政區", "FAC": "機構", "ORG": "組織"} label = label_map.get(grp, "敏感資訊") masked_text = masked_text[:ent['start']] + f"[{label}:***]" + masked_text[ent['end']:] caught_words.append(word) return masked_text, caught_words, list(set(whitelist_saved)) def main(): st.title("🛡️ Record Guard - 智慧醫療隱私防禦系統") st.markdown("---") with st.sidebar: st.header("📊 系統安全設定") st.info("💡 **企業級防護標準**:\n1. 符合 HIPAA 數位足跡遮蔽 (IP/URL)\n2. 支援台灣健保卡與民國紀年\n3. 合規稽核報告 (Audit Log) 匯出") st.write("**保護白名單:**") st.caption(", ".join(MEDICAL_WHITELIST)) st.warning("⚠️ 本網站為學術專題 Demo 版本,已部署於雲端伺服器,請勿輸入『真實』的病患個資,測試請使用模擬數據。") st.subheader("📥 資料輸入區") uploaded_file = st.file_uploader("📂 上傳病歷檔案 (支援 .txt 或 .json)", type=["txt", "json"]) data_format = st.radio("或選擇下方測試資料格式(手動輸入):", ["純文字/護理紀錄", "醫療標準傳輸協定 (HL7 V2 ADT)", "結構化 JSON"], horizontal=True) input_text = "" json_data = None target_text_to_scan = "" if uploaded_file is not None: if uploaded_file.name.endswith(".json"): try: json_data = json.load(uploaded_file) st.success(f"✅ 成功載入結構化 JSON 病歷:{uploaded_file.name}") input_text = json.dumps(json_data, ensure_ascii=False, indent=2) except Exception: st.error("JSON 格式解析失敗,請檢查檔案內容。") else: input_text = uploaded_file.read().decode("utf-8") st.success(f"✅ 成功載入純文字檔案:{uploaded_file.name}") st.text_area("預覽檔案內容:", value=input_text, height=200, disabled=True) target_text_to_scan = input_text if json_data and "clinical_note" in json_data: target_text_to_scan = json_data["clinical_note"] else: # 👨‍⚕️ 更新了測試數據,加入健保卡號與 IP 網址,方便 Demo 企業級功能 if data_format == "純文字/護理紀錄": default_val = "病患:王大明,男,74年05月20日出生。健保卡號:000012345678。家屬表示居住在台北市大安區忠孝東路三段2號。今日至台大醫院複檢,主訴發燒,連續血糖監測儀數據已上傳至 http://cgm-data.hospital.tw ,設備IP為 192.168.1.15。開立 Metformin 處方並安排心電圖檢查。" elif data_format == "醫療標準傳輸協定 (HL7 V2 ADT)": default_val = "MSH|^~\\&|HIS|TPE_HOSP|EMR|TPE_HOSP|202605221016||ADT^A01|MSG00001|P|2.4\nEVN|A01|202605221016\nPID|1||A123456789^^^ROC^NI||林小華^LIN^XIAO-HUA||19900101|F|||台北市信義區松智路1號||0911-222-333|||M\nOBX|1|TX|1234^主訴^LN||持續發燒,有高血壓病史,安排 X光 檢查||||||F" else: default_val = '{\n "patient_id": "A123456789",\n "name": "陳建國",\n "contact": "0988-777-666",\n "clinical_note": "患者陳建國今日於榮總進行抽血,HbA1c 數值偏高。"\n}' input_text = st.text_area("在此貼上或輸入病歷內容:", value="", placeholder=default_val, height=200) if not input_text: input_text = default_val if data_format == "結構化 JSON": try: json_data = json.loads(input_text) target_text_to_scan = input_text except Exception: target_text_to_scan = input_text else: target_text_to_scan = input_text st.markdown("
", unsafe_allow_html=True) start_btn = st.button("🚀 啟動安全去識別化", type="primary", use_container_width=True) st.markdown("
", unsafe_allow_html=True) if start_btn and target_text_to_scan: st.subheader("🛡️ 安全防禦防護成果") original_hash = calculate_sha256(target_text_to_scan) with st.status("🛠️ 安全引擎運作中...", expanded=True) as status: st.write("正在配置本地端模型與解析器...") ner_model = load_ner_model() time.sleep(0.3) hl7_types = [] current_text = target_text_to_scan if current_text.startswith("MSH|"): st.write("偵測到 HL7 標準格式,啟動 PID 段落精準爆破...") current_text, hl7_types = hl7_structural_masking(current_text) time.sleep(0.3) st.write("執行第一層:強特徵規則分析 (含數位足跡與健保卡檢測)...") progress_bar = st.progress(0) for i in range(50): time.sleep(0.005); progress_bar.progress(i + 1) step1_text, regex_types = advanced_regex_masking(current_text) st.write("執行第二層:語意上下文分析(保護 SNOMED/LOINC 術語)...") for i in range(50, 100): time.sleep(0.005); progress_bar.progress(i + 1) final_text, ai_words, saved_medical_terms = ai_deep_scan(step1_text, ner_model) status.update(label="✅ 安全隔離完成!", state="complete", expanded=False) if json_data and not (data_format == "結構化 JSON" and not uploaded_file): if "clinical_note" in json_data: json_data["clinical_note"] = final_text else: json_data["masked_content"] = final_text output_display = json.dumps(json_data, ensure_ascii=False, indent=2) else: output_display = final_text safe_hash = calculate_sha256(output_display) st.text_area("✨ 去識別化輸出成果:", value=output_display, height=200) st.balloons() st.markdown("### 📋 資安審查報告與檔案鑑識") total_threats = regex_types + [f"姓名/機構({w})" for w in ai_words] + hl7_types threat_count = len(total_threats) col_log1, col_log2 = st.columns([2, 1]) with col_log1: if threat_count > 0: st.error(f"🚨 **個資漏洞攔截**:阻斷 {threat_count} 處外洩 ({', '.join(total_threats)})。") if saved_medical_terms: st.success(f"🩺 **臨床術語保留**:保留【{', '.join(saved_medical_terms)}】等研究關鍵詞。") st.markdown("#### 🔐 密碼學完整性驗證 (SHA-256 Hash)") st.info(f"**原始文本 Hash:** `{original_hash}`\n\n**安全文本 Hash:** `{safe_hash}`") # 🕵️‍♂️ 專業升級:匯出 JSON 格式的合規稽核軌跡 with col_log2: st.markdown("#### 📥 合規舉證 (Compliance)") audit_log = { "scan_timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), "system_version": "Record Guard Pro Max v2.0", "original_file_hash": original_hash, "sanitized_file_hash": safe_hash, "threats_mitigated": total_threats, "medical_terms_whitelisted": saved_medical_terms, "hipaa_compliance_status": "PASS", "pdpa_compliance_status": "PASS" } json_log = json.dumps(audit_log, ensure_ascii=False, indent=2) st.download_button( label="📄 下載資安稽核報告 (JSON)", data=json_log, file_name=f"audit_log_{int(time.time())}.json", mime="application/json", use_container_width=True ) st.caption("※ 下載供資安長 (CISO) 存查的鑑識日誌,無原始個資洩漏風險。") with st.expander("🔍 檢視原始數據內容 (開發者核對用)"): st.code(input_text, language="text") if __name__ == "__main__": main()