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()