File size: 9,137 Bytes
cca37d8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12f6e0d
b8dff18
 
 
 
 
8f37fc1
 
b8dff18
 
 
 
 
 
66a5664
7ba2b0d
 
 
 
 
 
 
 
 
 
 
66a5664
7ba2b0d
4cabd11
 
6f9c264
 
 
4cabd11
6f9c264
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4cabd11
6f9c264
4cabd11
6f9c264
 
4cabd11
 
6f9c264
4cabd11
 
 
6f9c264
4cabd11
 
6f9c264
4cabd11
6f9c264
 
4cabd11
6f9c264
 
 
 
 
 
 
 
bcc4b4f
6f9c264
 
 
bcc4b4f
6f9c264
 
 
bcc4b4f
6f9c264
 
 
 
 
 
 
 
 
 
 
8214e92
6f9c264
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# 📦 PART 1: 이름 추출기 + 태그 치환기

from transformers import AutoTokenizer, AutoModelForTokenClassification, pipeline
import re

TAG_PREFIX = "N"

def apply_name_tags(text: str, names: list, start_index: int = 100) -> tuple[str, dict]:
    """
    🏷 이름 리스트를 태그로 치환: 김철수 → N100
    반환: (태깅된 텍스트, 태그 매핑 딕셔너리)
    """
    mapping = {}
    tagged_text = text
    counter = start_index

    # ✅ 긴 이름 우선 정렬
    names = sorted(set(names), key=len, reverse=True)

    for name in names:
        tag = f"{TAG_PREFIX}{counter:03d}"
        pattern = re.compile(rf"([\s\(\[\"']*){re.escape(kw)}([가-힣\s.,;:!?()\[\]\"']*)", re.IGNORECASE)

        tagged_text, n = pattern.subn(tag, tagged_text)
        if n > 0:
            mapping[tag] = name
            counter += 1
    return tagged_text, mapping


def replace_institution_keywords(text: str, keywords: list, replace_word: str) -> str:
    """
    🏢 키워드 기반 기관명 → 치환어로 변경
    """
    for kw in keywords:
        pattern = re.compile(
            rf'([\s\(\["']*){re.escape(kw)}([가-힣\s.,;:!?()\[\]"'"]*)',
            re.IGNORECASE
        )
        text = pattern.sub(lambda m: m.group(1) + replace_word + m.group(2), text)
    return text

   

# 📦 PART 2 (Extended & Fixed): 호칭/조사 확장기 + 태그 매핑 보정기 - 특수문자 오류 수정판

import re

# ✅ 확장된 호칭 리스트
COMMON_SUFFIXES = [
    # 📁 가정/관계 기반
    '어머니', '아버지', '엄마', '아빠', '', '누나', '언니', '오빠', '동생',
    '', '아들', '조카', '사촌', '이모', '고모', '삼촌', '숙모', '외삼촌',
    '할머니', '할아버지', '외할머니', '외할아버지', '장모', '장인', '며느리', '사위',
    '부인', '와이프', '신랑', '올케', '형수', '제수씨', '매형', '처제', '시누이',

    # 📁 사회/교육/직업 호칭
    '학생', '초등학생', '중학생', '고등학생', '수험생', '학부모', '선생', '선생님', '교사',
    '교감', '교장', '담임', '반장', '조교수', '교수', '연구원', '강사', '박사', '석사', '학사',
    '보호자', '피해자', '아동', '주민', '당사자', '대상자', '담당자',

    # 📁 직장/조직 직급
    '대표', '이사', '전무', '상무', '부장', '차장', '과장', '대리', '사원', '팀장', '본부장',
    '센터장', '소장', '실장', '총무', '직원', '매니저', '지점장', '사무장',

    # 📁 의료/기타
    '의사', '간호사', '간병인', '기사님', '어르신', '', ''
]

# ✅ 실전급 조사 리스트
COMMON_JOSA = [
    # ✅ 기본 조사
    '', '', '', '', '', '', '', '',

    # ✅ 처소/방향/대상
    '', '에서', '에게', '께서', '으로', '', '부터', '까지', '한테',

    # ✅ 강조/대조/비교
    '보다', '보다도', '마저', '조차', '조차도', '까지도', '밖에', '만큼', '만큼은',
    '이라도', '이든지', '이나마', '이건', '이란', '이라서', '이지만',

    # ✅ 연결형 조사
    '이며', '이나', '이거나', '이니까', '이라면', '처럼', '대로', '하고', '그리고', '', '',

    # ✅ 보조/종결형 어미
    '이기도', '이었던', '이었지만', '이어서', '이었다면', '', '', '', '이란', '이라는',

    # ✅ 특수형 조사/조합형
    '같은', '같아서', '까지는', '뿐만 아니라', '와는', '와도', '하고도', '으로서', '으로써'
]

def expand_variation_patterns(text: str, mapping: dict) -> str:
    """
    👓 태그된 텍스트에서 성+이름+호칭+조사 형태를 다시 태깅
    """
    for tag, base in mapping.items():
        prefix = r'[\\s\\(\\["\\\']*'  # 공백, 괄호, 따옴표 포함된 안전 패턴
        suffix = f"(?:{'|'.join(COMMON_SUFFIXES)})?"
        josa = f"(?:{'|'.join(COMMON_JOSA)})?"
        pattern = re.compile(rf'{prefix}{re.escape(base)}{suffix}{josa}', re.IGNORECASE)

        text = pattern.sub(lambda m: m.group(0).replace(base, tag), text)
    return text


def boost_mapping_from_context(text: str, mapping: dict) -> dict:
    """
    📌 태깅된 텍스트에서 각 태그의 실제 확장된 표현 감지해 mapping 보정
    """
    updated = {}
    for tag, base in mapping.items():
        idx = text.find(tag)
        if idx == -1:
            updated[tag] = base
            continue
        window = text[max(0, idx - 100): idx + 100]
        pattern = re.compile(rf'([\s\(\["\']*){re.escape(kw)}([가-힣\s.,;:!?()\[\]"\'"]*)', re.IGNORECASE)
        match = pattern.search(window)
        if match:
            updated[tag] = match.group(0)
        else:
            updated[tag] = base
    return updated


    
# 📦 PART 3: 민감정보 마스커 + 학교/학년/학과 마스커

import re

def postprocess_sensitive_patterns(text: str) -> str:
    """
    🔐 이메일, 주민등록번호, 계좌번호, 카드번호, 전화번호, 주소 마스킹
    """
    text = re.sub(r"[\w\.-]+@[\w\.-]+", "******@***.***", text)  # 이메일
    text = re.sub(r"(\d{6})[- ]?(\d{7})", "******-*******", text)  # 주민번호
    text = re.sub(r"(\d{3})[- ]?(\d{4})[- ]?(\d{4})", "***-****-****", text)  # 카드/전화
    text = re.sub(r"(\d{1,3})동", "***동", text)
    text = re.sub(r"(\d{1,4})호", "****호", text)
    return text

def to_chosung(text: str) -> str:
    """
    🧠 초성 변환기: 학교명, 학과명 등에 적용
    """
    CHOSUNG_LIST = [chr(i) for i in range(0x1100, 0x1113)]
    result = ""
    for ch in text:
        if '가' <= ch <= '힣':
            code = ord(ch) - ord('가')
            cho = code // 588
            result += CHOSUNG_LIST[cho]
        else:
            result += ch
    return result

def mask_school_names(text: str) -> str:
    """
    🏫 학교명 → 초성 변환 마스킹 (연세대학교 → ㅇㅅ대학교)
    """
    def replace_school(m):
        return to_chosung(m.group(1)) + m.group(2)
    return re.sub(r"([가-힣]{2,20})(초등학교|중학교|고등학교|대학교)", replace_school, text)

def mask_department_names(text: str) -> str:
    """
    🏢 학과명 → 초성 마스킹 (국문학과 → ㄱㅁ학과)
    """
    return re.sub(r"([가-힣]{2,20})학과", lambda m: to_chosung(m.group(1)) + "학과", text)

def mask_grade_class(text: str) -> str:
    """
    🎓 학년/반 정보 마스킹 (2학년 3반 → *학년 *반)
    """
    return re.sub(r"(\d)학년(\s?(\d)반)?", "*학년 *반", text)



# 📦 PART 4: 기관 키워드 치환기 + Gradio UI 실행기

import re
import gradio as gr
from part1_name_extract_and_tag import extract_names, apply_name_tags
from part2_suffix_expansion_and_mapping import expand_variation_patterns, boost_mapping_from_context
from part3_sensitive_school_masker import (
    postprocess_sensitive_patterns,
    mask_school_names,
    mask_department_names,
    mask_grade_class
)

def replace_institution_keywords(text: str, keywords: list, replace_word: str) -> str:
    """
    🏢 키워드 기반 기관명 → 치환어로 변경
    """
    for kw in keywords:
        pattern = re.compile(rf'([\s\(\["'‘“]*){re.escape(kw)}([가-힣\s.,;:!?()"'”’]*)', re.IGNORECASE)
        text = pattern.sub(lambda m: m.group(1) + replace_word + m.group(2), text)
    return text

def apply_full_masking(text: str, keyword_str: str, replace_word: str):
    # 1. 키워드 치환
    keywords = [k.strip() for k in keyword_str.split(",") if k.strip()]
    text = replace_institution_keywords(text, keywords, replace_word)

    # 2. 민감정보 + 학교 학과 학년 마스킹
    text = postprocess_sensitive_patterns(text)
    text = mask_school_names(text)
    text = mask_department_names(text)
    text = mask_grade_class(text)

    # 3. 이름 추출 + 태깅
    names = extract_names(text)
    tagged, mapping = apply_name_tags(text, names)

    # 4. 파생 표현 확장
    tagged = expand_variation_patterns(tagged, mapping)
    mapping = boost_mapping_from_context(tagged, mapping)

    # 5. 매핑 출력 정리
    mapping_text = "\n".join([f"{k} → {v}" for k, v in mapping.items()])
    return tagged, mapping_text

# UI 실행
with gr.Blocks() as demo:
    gr.Markdown("🧠 **v5.0 마스킹 통합 시스템** — 키워드, 이름, 개인정보, 학교 마스킹")
    input_text = gr.Textbox(lines=15, label="📄 원문 텍스트")
    keyword_input = gr.Textbox(lines=1, label="기관 키워드 (쉼표로 구분)", value="굿네이버스, 사회복지법인 굿네이버스")
    replace_input = gr.Textbox(lines=1, label="치환할 텍스트", value="우리기관")
    run_button = gr.Button("🚀 실행")
    masked_output = gr.Textbox(lines=15, label="🔐 마스킹 결과")
    mapping_output = gr.Textbox(lines=10, label="🏷️ 태그 매핑", interactive=False)
    run_button.click(fn=apply_full_masking, inputs=[input_text, keyword_input, replace_input], outputs=[masked_output, mapping_output])

demo.launch()