Spaces:
Running
Running
| 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() | |
| 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("<br>", unsafe_allow_html=True) | |
| start_btn = st.button("🚀 啟動安全去識別化", type="primary", use_container_width=True) | |
| st.markdown("<br>", 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() |