한국어 PII 검출 + 가명화 Python 라이브러리
88
stars
108
commits
Python
primary language
Sep 9, 2026
updated
English | 한국어
한국어 문서의 개인정보를 검출하고 가역적으로 가명화하는 Python 라이브러리. 외부 ML 의존성 없이 룰 + 사전 + 체크섬만으로 동작. 공공 문서에서 특히 강하며, 어떤 ML 파이프라인의 전처리 레이어로도 활용 가능.
공개 측정값은 제품 자격증명이 아니라 분포별 참고 근거입니다. KDPII 대화체 4,891건에서 F1 0.66, 행정·서식형 생성 평가셋 540건에서 F1 0.79를 기록했습니다. 데이터셋, 라벨 정의와 채점 조건이 달라지면 결과도 달라지므로 보편적 순위나 고객 환경의 정확도로 해석하지 않습니다. 상세 조건은 벤치마크 문서를 따릅니다.
| 항목 | 운영 정의 |
|---|---|
| 주 고객 | 공공·규제 산업의 문서 RAG, 데이터 전처리, 개인정보보호 팀 |
| 고객 과업 | 외부 전송·인덱싱 전에 한국형 구조적 PII를 가명화하고 복원 이력을 통제 |
| 보장 범위 | 형식·사전·체크섬 기반의 재현 가능한 검출, 가명화와 Vault 감사 |
| 비보장 범위 | 일반 NER, 자유 대화·SNS 전반, 법적 익명성 판정, 모든 도메인의 동일 정확도 |
| 운영 조건 | 고객 문서의 라벨 분포와 confusion count를 별도 측정하고 PERSON·ADDRESS 처리 정책 승인 |
| 상태 해석 | Software: Stable은 패키지 안정성이다. 고객 환경은 도메인 qualification 전까지 미승격 |
결정적 식별자와 PERSON·ADDRESS 같은 문맥형 라벨을 하나의 전체 F1으로 합쳐 운영 결정을 내리지 않습니다. 고객별 승격에서는 critical identifier, 문맥형 라벨, Vault 운영을 따로 검증해야 합니다.
from ko_pii import Anonymizer, ProcessingMode
result = Anonymizer(mode=ProcessingMode.STRICT, strategy="tokenize").process(
"신청인 홍길동 (880101-1234568) 연락처 010-1234-5678"
)
print(result.text)
# 신청인 <PERSON_1> (<RRN_1>) 연락처 <PHONE_1>
print(result.vault.reveal("<RRN_1>")) # 880101-1234568 (권한자만 복원)
print(result.combined_risk.combined_risk.name) # CRITICAL
조각 단위 입력은 각 조각을 따로 검사하지 말고, 최종 판정 전까지 보관하십시오.
from ko_pii import PreForwardAnonymizer
stream = PreForwardAnonymizer(max_chars=100_000)
stream.feed("주민번호 880101-")
stream.feed("1234568 입니다")
safe_result = stream.finalize() # 이 시점 전에는 원문을 downstream으로 보내지 않음
이 API는 토큰별 저지연 출력을 가장하지 않습니다. 식별자가 임의의 chunk 경계에서 나뉠 수 있으므로, 개인정보 경계에서는 완성된 메시지를 가명화한 뒤 전달하는 것이 기본 계약입니다.
원본:
신청인 홍길동 (880101-1234568) 연락처 010-1234-5678
주소: 서울특별시 강남구 테헤란로 152
tokenize (토큰 치환 + Vault 복원 가능):
신청인 <PERSON_1> (<RRN_1>) 연락처 <PHONE_1>
주소: <ADDRESS_1>
partial (일부만 가림 — 실무 양식):
신청인 홍OO (880101-1******) 연락처 010-****-5678
주소: 서울특별시 강남구 ***
redact (카테고리명 치환):
신청인 [성명] ([주민등록번호]) 연락처 [전화번호]
주소: [주소]
처음이시면:
mode=ProcessingMode.STRICT+strategy="tokenize"추천. 가장 안전한 기본 설정 (MEDIUM 위험도 이상 차단 + Vault 복원 가능).
ko-pii report.hwp --strategy tokenize (표·머리말·꼬리말·메타데이터 모두 처리)RAG·LLM 파이프라인은 미정제 비정형 데이터를 그대로 인덱싱·검색해 모델에 넣습니다 — PII 가 벡터 DB 와 응답 양쪽에 노출됩니다.
ko-pii 는 RAG 의 인제스트(벡터 DB 진입 전)와 검색(LLM 전달 전) 양단에서 PII 를 차단합니다. 같은 인물은 같은 토큰으로 치환해 문맥을 보존하고(LlamaIndex·LangChain 연동 제공), Vault 로 권한 기반 복원과 감사 추적을 지원합니다.
010)·제로폭 문자 삽입 등 유니코드 우회를 정규화로 무력화 (검출 offset 은 원본 보존)DetectionResult (label/start/end/text/confidence) 표준 객체 출력. ML 파이프라인 앞단에 끼워넣기 편함[file] extras)| 도메인 | 권장 설정 | 비고 |
|---|---|---|
| 공공 문서 (공문서·민원·인사) | STRICT + tokenize | 기본값. 가장 잘 맞는 도메인 |
| LLM 학습 데이터 전처리 | PARANOID + tokenize 또는 redact | 누수 차단 우선 |
| 의약품·바이오 | STRICT + exclude={"AGE","HEIGHT","WEIGHT"} | "체중 1kg당" 같은 용법 오탐 방지 |
| 금융·보험 | STRICT + tokenize | RRN·카드·계좌 결정적 검출 |
| 일반 사무 (사내 문서) | BALANCED + partial | 읽기 편한 부분 마스킹 |
# 의약품 도메인 — PERSON FP 방지 + 신체속성 오탐 방지
anon = Anonymizer(
mode=ProcessingMode.STRICT,
strategy="tokenize",
exclude={"AGE", "HEIGHT", "WEIGHT"}, # "체중 1kg당" 오탐 방지
)
# PERSON FP 가 많다면 — 도메인 사전 주입
# src/ko_pii/dictionaries/common_words.py 에 의약품 성분명·제조사명 추가
# 예: "이부프로펜", "한미약품", "메트포르민" → PERSON 에서 자동 제외
pip install ko-pii # 현재 PyPI stable
# main의 1.16 API를 태그 배포 전에 검증하려면
python3 -m pip install \
"ko-pii @ git+https://github.com/Marker-Inc-Korea/ko-pii.git@main"
extras 옵션 (필요 시):
pip install "ko-pii[file]" # HWP/HWPX/DOCX/PDF
pip install "ko-pii[security]" # Vault AES-256-GCM
Python 3.10 이상. 코어는 표준 라이브러리만 사용.
from pathlib import Path
from ko_pii import Anonymizer, ProcessingMode
anon = Anonymizer(mode=ProcessingMode.PARANOID, strategy="tokenize")
for path in Path("./공문서/").glob("*.hwp"):
result = anon.process(path.read_text(encoding="utf-8"))
Path(f"./가명화/{path.name}").write_text(result.text, encoding="utf-8")
# vault.json 분리 보관 (권한 있는 사용자만 복원 가능)
result.vault.save(f"./vault/{path.stem}.json")
pip install "ko-pii[file]"from ko_pii import Anonymizer, ProcessingMode, RiskLevel
anon = Anonymizer(mode=ProcessingMode.AUDIT) # 차단 X, 검출만 보고
result = anon.process(incoming_petition_text)
# 결합 위험도가 CRITICAL 이면 담당자에게 알림
if result.combined_risk.combined_risk >= RiskLevel.CRITICAL:
notify_admin(
identifiers=result.combined_risk.distinct_identifiers, # ["RRN"]
quasi=result.combined_risk.distinct_quasi, # ["ADDRESS", "PERSON", "PHONE"]
)
# 응대 직원에게는 가명화 버전 제공
masked = Anonymizer(mode=ProcessingMode.STRICT, strategy="partial").process(
incoming_petition_text
).text
partial 전략으로 일부만 마스킹 (880101-1******)# 코드 어디서든 logger.info("...") 호출 시 자동 가명화
import logging
from ko_pii import Anonymizer, ProcessingMode
_anon = Anonymizer(mode=ProcessingMode.STRICT, strategy="redact")
class PIIFilter(logging.Filter):
def filter(self, record):
record.msg = _anon.process(str(record.msg)).text
return True
logging.getLogger().addFilter(PIIFilter())
logging.info("신청인 홍길동 (880101-1234568) 처리 완료")
# → "신청인 [성명] ([주민등록번호]) 처리 완료"
| 평가 도메인 | 문서 수 | ko-pii | openai/PF | Presidio |
|---|---|---|---|---|
| 생성 평가셋 (행정/서식체, 검증 gold) | 540 | 0.790 | 0.451 | 0.483 |
| KDPII (한국어 일상 대화) | 4,891 | 0.660 | 0.264 | 0.273 |
| KLUE-NER (신문기사 풀네임) | 5,000 | 0.419 | 0.155 | 0.000 |
아래 결과는 명시한 데이터셋과 단일 채점 조건의 측정값입니다. 보편적 정확도 순위나 고객 환경의 운영 적합성을 자동으로 의미하지 않습니다:
| 시스템 | 지연/문서 | 처리량 | 100만 문서 비용 | 특성 |
|---|---|---|---|---|
| ko-pii | 0.19 ms | ~5,350/s | ~$0 (CPU 1코어 ~3분) | 결정적 · 체크섬 검증 · 온프레미스 |
| Presidio | 4.2 ms | ~238/s | ~$0 | 한국 카테고리 다수 미지원 |
| openai/privacy-filter | 481 ms | ~2/s | ~$0 | 한국 카테고리 7종만 |
→ 0.19 ms/문서(~5,350 문서/s)로 Presidio(4.2ms)보다 22배 빠르고, 결정적이라 같은 입력에 항상 같은 결과 · 검출마다 근거 조항/evidence 부착. 속도·비용·정성 전체 비교: 종합 비교표(한글).
match_forms_overlap, person_min_length=3)로 전체 4,891문서 재측정 — openai/privacy-filter (660M ML) · Microsoft Presidio 등 룰·NER 도구 대비 우위.data/generated_eval.jsonl)했다. 자기 주입 데이터는
아니지만 생성 데이터의 분포 편향과 평가 불확실성은 남으므로 고객 문서 성능을 대체하지
않는다 (상세 BENCHMARK §3b).docs/BENCHMARK.md · docs/EVALUATION_REPORT.md. (KLUE 는 이전 방법론 수치)운영 전 권장: 사용하시는 도메인의 실제 문서 30~100건을 직접 테스트해보세요. 도메인마다 성능 차이가 있습니다.
common_words.py 도메인 사전 주입, exclude={"PERSON"}, 또는 하이브리드 분류기(dev/classifier)를 권한다. (재현: python -c "from ko_pii.eval.klue_ner import load_klue_ner, evaluate_person; print(evaluate_person(load_klue_ner('data/klue_ner/klue-ner-v1.1_dev.tsv')).format())")1588-2024)는 제품·연식 번호와, 무구분자 13~19자리는 바코드·IMEI와 형식이 같다. 길이-브랜드 일관성·단어경계 문맥으로 상당수 거르지만, recall 우선이라 형식만으로 완전 구분은 불가 — 최종 정밀 판단은 주변 문맥(검토 큐) 또는 하이브리드 분류기(dev/classifier)의 몫상세 평가: docs/EVALUATION_REPORT.md.
# 기본
ko-pii input.txt --mode STRICT --strategy tokenize \
--vault vault.json -o output.txt --report report.html
# 배치 (디렉토리 일괄, 병렬)
ko-pii ./incoming/ --batch --workers 4 --output-dir ./anonymized/
# Vault 암호화 + 감사 로그
KPII_VAULT_PASSWORD=secret ko-pii doc.hwp \
--vault vault.kvault --audit-log audit.jsonl
from ko_pii import Anonymizer, ProcessingMode
anon = Anonymizer(mode=ProcessingMode.STRICT, strategy="tokenize")
result = anon.process(text)
print(result.text) # 가명화된 텍스트
print(result.vault.reveal("<RRN_1>")) # 원본 복원 (권한자만)
print(result.summary["by_label"]) # {"RRN": 1, "PHONE": 1, "PERSON": 1}
# 검출 결과의 결합 위험도 자동 평가
print(result.combined_risk.combined_risk.name) # CRITICAL
print(result.combined_risk.distinct_identifiers) # ["RRN"]
print(result.combined_risk.distinct_quasi) # ["PERSON", "PHONE"]
# k-익명성 평가 (집단 데이터)
from ko_pii.analytics import k_anonymity
report = k_anonymity(records, quasi_keys=["age", "city", "job"], threshold=5)
print(report.k) # 최소 그룹 크기
print(report.satisfies_threshold) # True/False
print(report.rationale) # ["준식별자 ['age', 'city', 'job'] 기준 N개 그룹", ...]
from ko_pii.tabular import anonymize_records
import csv
rows = list(csv.DictReader(open("employees.csv")))
# 헤더 "성명/주민번호/연락처/주소" → 자동으로 PERSON/RRN/PHONE/ADDRESS 매핑
anon_rows, vault = anonymize_records(rows, strategy="tokenize")
print(anon_rows[0])
# {'성명': '<PERSON_1>', '주민번호': '<RRN_1>', '연락처': '<PHONE_1>', '주소': '<ADDRESS_1>'}
SQL 스키마처럼 오탐이 곧 쿼리 차단으로 이어지는 경로에서는 부분 문자열 매핑 대신
근거와 신뢰도를 반환하는 strict API를 사용합니다. product_name은 name으로 분류되지
않고, 단독 name은 자동 차단이 아니라 낮은 신뢰도의 검토 대상으로 표시됩니다.
from ko_pii.tabular import classify_schema_columns
evidence = classify_schema_columns(["성명", "주민등록번호", "product_name", "name"])
print(evidence["성명"]) # PERSON, confidence=1.0
print(evidence["name"].confidence) # 0.60 (ambiguous)
assert "product_name" not in evidence
confidence 낮은 검출 → 검토 큐에 저장 → 사용자가 FP/OK/FN 마킹 → 누적 마킹에서 사전 추천 패치 자동 생성 (자동 반영 X, 사람 검토 후 반영).
result = anon.process(text)
# 1. confidence 낮아 REVIEW 분류된 검출 (모드별 자동 분류)
for record in result.review_items():
d = record.detection
print(d.text, d.confidence, d.evidence)
# 2. 별도 JSONL 큐에 저장 → 사용자가 verdict 마킹
from ko_pii.review.queue import ReviewQueue
q = ReviewQueue("review.jsonl")
q.enqueue_review_records(result.review_items(), document=text)
# 3. 누적 마킹 → 패치 파일 생성 (common_words 후보 / 이름 후보)
from ko_pii.review.feedback import apply_feedback
apply_feedback(
queue_path="review.jsonl",
output_dir="feedback_patches/",
min_repeat=2, # 같은 토큰이 2회 이상 FP → 후보 (사전 오염 방지)
)
# → feedback_patches/common_words_additions.txt (PERSON FP 후보)
# → feedback_patches/names_to_add.txt (FN 표시 이름)
# → feedback_patches/summary.json
from ko_pii.patterns.rrn import detect
for r in detect("신청인 880101-1234568"):
print(r.label, r.text, r.confidence, r.legal_basis)
# RRN 880101-1234568 1.0 개인정보보호법 제24조의2
전체 라벨 키는
ko-pii --labels(CLI) 또는from ko_pii.labels import ALL_LABELS, LABEL_INFO(Python) 로 조회할 수 있습니다.
| 카테고리 | 검증 | 위험도 |
|---|---|---|
| RRN (주민등록번호) | 13자리 + 날짜 + 한국 체크섬 | CRITICAL |
| FRN (외국인등록번호) | gender 5-8 + 체크섬 | CRITICAL |
| 사업자등록번호 | 국세청 가중합 체크섬 | HIGH |
| 법인등록번호 | 법인 체크섬 (RRN 우선) | MEDIUM |
| 운전면허번호 | 지방청 코드 11-28 화이트리스트 | HIGH |
| 여권번호 | prefix (M/S/PP/PD 등) + 8자리 | CRITICAL |
| 신용카드 | BIN 화이트리스트 + Luhn | CRITICAL |
| 필지고유번호 (PNU) | 19자리 + 시·도 코드 | LOW |
| 카테고리 | 키워드 |
|---|---|
| 건강보험증 | 건강보험 / 의료보험 / 보험증 |
| 처방번호 | 처방번호 / Rx / 교부번호 |
| 의약품 코드 | 약품코드 / KD코드 + 한국 GS1 |
| 팩스번호 | 팩스 / FAX |
| 계좌번호 | 계좌 / 은행명 60+ (3-way anchor) |
| 사번 | 사번 / 공무원번호 / 직원번호 / 임용번호 |
| 민원번호 | 민원 / 청구 / 정보공개 / 행정심판 |
| 사건번호 | 사건유형 (가합/고합/구합/헌가 등) |
| 카테고리 | 검증 |
|---|---|
| 전화번호 | 모바일 010-019 / 서울 02 / 지방 031-064 / VoIP 070 / 대표 15xx-18xx / +82 국제 |
| 이메일 | RFC 5322 |
| IP | IPv4 옥텟 + IPv6 RFC 4291 |
| URL | http(s) / ftp |
| 우편번호 | 시·도 첫자리 매핑 |
| 차량번호 | 신형 NN[가-힣]NNNN + 용도 한글 화이트리스트 |
| 공문서번호 | 부처명 + 형식 |
| 카테고리 | 사전 규모 |
|---|---|
| 인명 (PERSON) | 성씨 286 + 직책 인접 + 17 거부 룰 |
| 주소 (ADDRESS) | 광역 17 + 기초 226 + 빈출 동 240 + 법정동 10K (anchor 조건부) + 건물 접미사 38 + 동/호/층 bridge 확장 |
| 국적 (NATIONALITY) | 국가명 70+ (대한민국·미국·일본 등) |
| 학력 (EDUCATION) | 대학 ~330 + 약칭 |
| 전공 (MAJOR) | 학과 ~400 (KEDI 분류) |
| 직책 (POSITION) | titles 250+ (정부·경찰·소방·군·검사·법관·민간) |
| 카테고리 | 검증 | 위험도 |
|---|---|---|
| 생년월일 | 날짜 + 키워드/풀네임/년생 marker | HIGH |
| 나이 | "32세 / 32살 / 환갑 / 12개월 아기 / 30대" | INFO |
| 신장 | "175cm / 1.75m" 50–250 | INFO |
| 체중 | "70kg / 70킬로" 1–300 | INFO |
준식별자 (Quasi-Identifier) 단독으로는 식별 불가지만 다른 정보와 결합 시 재식별 위험.
analytics/combined_risk가 자동 평가.
각 PII 검출은 단순 정규식 매칭이 아닌 multi-gate: 접두어 라벨 / 키워드 anchor / 문맥 사전 / 형식 검증 의 조합.
라벨 직후 1~4글자 한글 → 강한 PERSON 후보.
| 도메인 | 라벨 |
|---|---|
| 기본 | 성명 이름 성함 이 름 |
| 민원·행정 | 신청인 신청자 민원인 청구인 보호자 대리인 당사자 |
| 결재 | 기안자 결재자 검토자 보고자 수신자 발신자 참조 |
| 사법 | 원고 피고 고소인 피고소인 증인 감정인 |
| 경찰·소방 | 피의자 피해자 용의자 참고인 신고자 수사관 출동대장 |
| 인사 | 평가자 피평가자 면담자 추천인 |
| 의료 | 환자 |
라벨 variant 7종 인식: 성명: 홍길동 / [성명] 홍길동 / (성명) 홍길동 / <성명> 홍길동 등.
김부장 김포시 이화여대 → 거부~은데 ~는데 ~라서 ~까지 끝 → 거부보건복지부 행정안전부 → 거부박씨 김모씨 ○○○ 시민 → 거부✓ 성명: 김도윤 (필드 라벨)
✓ 박지훈 과장님께 (직책 인접)
✓ 홍길동(洪吉童) (한자 병기)
✓ 880101-1234568 (RRN — 체크섬)
✓ 120-81-47521 (사업자 — 국세청 체크섬)
✓ 4242-4242-4242-4242 (카드 — Luhn)
✓ M12345678 (여권)
✗ 김부장이 협조 안 함 (호칭 = 거부)
✗ 보건복지부는 검토 후 (부처명)
✗ 시행일자: 2026-05-20 (비-생일 거부)
✗ 881301-1000004 (RRN — 13월 무효)
✗ A12345678 (여권 — A prefix 거부)
| 모드 | 차단 기준 | 용도 |
|---|---|---|
PARANOID | LOW 이상 모두 차단 | 외부 공개·LLM 전송 전 |
STRICT | MEDIUM 이상 차단 | 실무 표준 (기본값) |
BALANCED | HIGH 이상 차단 | 내부 협업 |
PERMISSIVE | CRITICAL 만 차단 | 분석가 작업 |
AUDIT | 차단 없음, 검출만 보고 | 감사·통계 |
| 전략 | 880101-1234568 → | 가역 | 설명 |
|---|---|---|---|
tokenize | <RRN_1> | ✓ | 토큰 치환, Vault 에 원본 보관 |
redact | [주민등록번호] | ✗ | 카테고리명으로 치환 |
partial | 880101-1****** | ✗ | 일부만 가림 (실무 표준) |
asterisk | ************** | ✗ | 별표 마스킹 |
hashed | <RRN:abc123> | ✗ | 해시 (같은 값 → 같은 토큰) |
fpe | 771202-2345671 | ✗ | 형식 유지 암호화 (FPE) |
| 기능 | 설명 | 설치 |
|---|---|---|
| HWP/HWPX/DOCX/PDF 파서 | 한컴오피스·MS Word·PDF 자동 파싱 (본문 + 표 + 머리말 + 메타데이터). 아래 파서 상세 참고 | [file] |
| Vault 암호화 | AES-256-GCM + PBKDF2 480k 반복 | [security] |
| 감사 로그 (JSONL) | 모든 reveal() 호출 기록 (개인정보보호법 제29조) | 코어 |
| 배치 처리 | 디렉토리 일괄 + 병렬 워커 | 코어 |
| 검토 큐 | confidence 낮은 검출 → 사람 검토 → 오탐 어휘 자동 학습 | 코어 |
| HTML 리포트 | 정탐 초록 / 오탐 빨강 / 미탐 노랑 시각화 | 코어 |
| 한자/로마자 변형 | 洪吉童 → 홍길동, Hong Gildong → 홍길동 | 코어 |
| RAG 연동 | LlamaIndex·LangChain 검색 결과 PII 마스킹 (검색→마스킹→LLM) | [llamaindex] / [langchain] |
| 룰+ML 하이브리드 | 룰+ML 결합 모드 4종 + 임계값으로 검출 민감도 조절 (opt-in) | [classifier] |
| 포맷 | 사용 라이브러리 | 비고 |
|---|---|---|
| HWP 5.x | olefile | OLE 바이너리 레코드 직접 파싱, 본문 텍스트 추출 |
| HWPX | stdlib (zipfile + xml) | ZIP+XML 구조, 외부 의존성 없음 |
| DOCX | stdlib (zipfile + xml) | ZIP+XML 구조, 외부 의존성 없음 |
| XLSX | stdlib (zipfile + xml) | sharedStrings + sheet XML |
| pdfplumber (우선) / pypdf (fallback) | 텍스트 레이어만 추출 (스캔 PDF는 OCR 필요) |
PDF 참고: PDF는 글자 좌표 기반이라 칸별 공백·줄바꿈 삽입이 흔합니다. ko-pii는 내장 정규화 엔진(
text_normalizer)으로 PII 패턴 중간의 불필요 공백/줄바꿈을 자동 보정합니다. pdfplumber가 pypdf보다 레이아웃 분석이 우수하므로 pdfplumber 설치를 권장합니다.
신뢰하지 않는 RAG 문서를 처리할 때는 일반 read_text() 대신 파일 크기, ZIP 멤버 수,
개별·전체 압축 해제량, 압축률, 경로 탈출, XML DTD·엔티티, 추출 문자 수를 먼저 검사하는
bounded API를 사용합니다.
from ko_pii.io_ import FileReadPolicy, read_text_bounded
document = read_text_bounded(
"notice.hwpx",
policy=FileReadPolicy(max_file_bytes=8 * 1024 * 1024),
)
print(document.sha256, document.archive_members)
text = document.text
기본 bounded allowlist는 TXT·Markdown·CSV·TSV·HWPX·DOCX·XLSX입니다. PDF와 구형 HWP는
파서 자체가 결정론적 페이지·압축 해제 budget을 제공하지 않으므로, 별도 메모리·시간 제한
worker에서 실행하는 배포만 allowed_extensions로 명시적으로 활성화해야 합니다.
검색된 문서를 LLM 에 넣기 전에 PII 를 마스킹합니다. 한 번의 검색 결과 안에서 같은 인물은 같은 토큰(<PERSON_1>)으로 치환돼 문맥이 보존되고, vault 를 넘기면 답변 생성 후 vault.reveal() 로 복원할 수 있습니다.
# LlamaIndex — node postprocessor (검색 → 마스킹 → LLM)
from ko_pii.integrations.llamaindex import KoPiiNodePostprocessor
qe = index.as_query_engine(
node_postprocessors=[KoPiiNodePostprocessor(mode="STRICT")]
)
# LangChain — Runnable 체인에 그대로 삽입
from ko_pii.integrations.langchain import KoPiiRedactor
chain = retriever | KoPiiRedactor(mode="STRICT") | prompt | llm
코어는 ML 없이 동작하되, 정확도가 더 필요하면 ML 을 얹을 수 있습니다. 하이브리드는 두 종류이며 서로 다른 기능입니다:
| ① 토큰 NER 하이브리드 (span 교체) | ② 문서 분류기 하이브리드 (confidence 결합) | |
|---|---|---|
| 무엇 | 룰=결정적 ID(체크섬), ML=퍼지(이름·주소 등) 검출 자체를 교체 | 문서 수준 "PII 있음/없음" 분류기로 룰 결과를 보강 (span 추가 없음) |
| 사용 | Anonymizer(secondary_detector=..., merge_mode="role_split") | ko_pii.classifier.HybridAnonymizer |
| 성능 | 외부 검증 F1 0.97 (docs/HYBRID_NER.md) | 검토 트리거·민감도 조절용 |
① 토큰 NER 하이브리드 — docs/HYBRID_NER.md 레시피로 직접 학습한 NER 모델을 꽂으면 됩니다:
from ko_pii import Anonymizer
from ko_pii.integrations.hf_token_ner import HFTokenNERAdapter
ml = HFTokenNERAdapter("out/ner_fuzzy/final") # 직접 학습한 모델 (레시피 참조)
anon = Anonymizer(secondary_detector=ml, merge_mode="role_split")
result = anon.process(text) # 모든 전략(tokenize/partial/redact)·Vault 그대로 동작
② 문서 분류기 하이브리드 — [classifier] extra 로 결합 방식과 민감도를 직접 설정:
| 결합 모드 | 동작 | 용도 |
|---|---|---|
SCORE | 룰 검출 + 분류기 확신도 보강 | 기본 |
GATED | 분류기 점수 < 임계값이면 룰 skip | 속도 우선 |
REVIEW_FLAG | 룰 0건인데 분류기 높으면 '검토 권고' | 사람 검토 트리거 |
UNION_BLOCK | 한쪽이라도 PII로 보면 차단 | 보수적 마스킹 |
classifier_threshold · gate_threshold 로 룰 ↔ ML 결합 비율(민감도)을 세밀하게 조절합니다.
from ko_pii import Anonymizer, ProcessingMode
from ko_pii.classifier import PIIClassifier, HybridAnonymizer, HybridMode
clf = PIIClassifier.from_pretrained("models/...")
hybrid = HybridAnonymizer(
Anonymizer(mode=ProcessingMode.BALANCED), clf,
mode=HybridMode.REVIEW_FLAG, # 결합 방식
classifier_threshold=0.5, # 민감도(비율) 조절
)
pip install ko-pii[classifier] # torch + transformers + scikit-learn
python -m ko_pii.classifier.train ... # 모델은 직접 학습
사전학습 가중치는 배포하지 않습니다(학습 데이터 라이선스) — 코드·학습 레시피 제공. 튜닝 NER 모델은 추후 공개 예정(라이선스·일반화 평가 후).
하이브리드 평가(룰단독 vs 하이브리드, base vs tuned 매트릭스):
docs/HYBRID_NER.md— 룰 대비 +0.14~0.19(klue), 단 제대로 튜닝된 한국어 NER이라야 일관 효과(base zero-shot 은 역효과). 실험 전 과정(학습·평가 코드, 실행 로그, 원시 결과)은 (내부 NER 실험 아카이브 — 비공개) 아카이브 참조.
Q1. ML 없이 룰만으로 정말 잘 되나요? 한국 핵심 구조적 PII는 형식·체크섬으로 결정론 검증할 수 있어 ML과 상보적이다. PERSON 같은 맥락 의존적 PII는 ML이 더 나을 수 있고, 생성 행정·서식형 평가셋 540문서의 전체 F1 0.790은 고객 운영 보장이 아니라 도메인 qualification의 참고값이다 (docs/BENCHMARK.md §3b).
Q2. 오탐이 많으면?
common_words.py 에 도메인 사전 주입, exclude={"PERSON"} 으로 특정 카테고리 끄기, 모드 변경 (STRICT → BALANCED).
Q3. Vault 분실하면?
복원 불가 (보안 설계). [security] extras 로 암호화 보관 또는 strategy="redact" (카테고리명 치환, Vault 불필요) 사용.
Q4. HWP 표·머리말 다 잡히나요?
네. [file] extras 설치 시 본문 + 표 + 머리말 + 꼬리말 + 메타데이터 모두 추출.
Q5. 다른 도구 (Presidio / openai) 와 뭐가 다르나요?
Q6. 이름 단독 검출이 오탐이 많아요. 이름+전화번호 같이 있을 때만 차단할 수 있나요?
PERMISSIVE 모드 (CRITICAL만 차단) + combined_risk 조건부 재처리:
result = Anonymizer(mode=ProcessingMode.PERMISSIVE).process(text)
if result.combined_risk.combined_risk >= RiskLevel.HIGH:
result = Anonymizer(mode=ProcessingMode.STRICT).process(text)
git clone https://github.com/Marker-Inc-Korea/ko-pii
cd ko-pii
pip install -e ".[dev]"
pytest # 전체 테스트 통과
상세 문서: docs/ 디렉토리 참조.
MIT License
108 commits
Python
100.0%
한국어 PII 검출 + 가명화 Python 라이브러리
88
stars
108
commits
Python
primary language
Sep 9, 2026
updated
English | 한국어
한국어 문서의 개인정보를 검출하고 가역적으로 가명화하는 Python 라이브러리. 외부 ML 의존성 없이 룰 + 사전 + 체크섬만으로 동작. 공공 문서에서 특히 강하며, 어떤 ML 파이프라인의 전처리 레이어로도 활용 가능.
공개 측정값은 제품 자격증명이 아니라 분포별 참고 근거입니다. KDPII 대화체 4,891건에서 F1 0.66, 행정·서식형 생성 평가셋 540건에서 F1 0.79를 기록했습니다. 데이터셋, 라벨 정의와 채점 조건이 달라지면 결과도 달라지므로 보편적 순위나 고객 환경의 정확도로 해석하지 않습니다. 상세 조건은 벤치마크 문서를 따릅니다.
| 항목 | 운영 정의 |
|---|---|
| 주 고객 | 공공·규제 산업의 문서 RAG, 데이터 전처리, 개인정보보호 팀 |
| 고객 과업 | 외부 전송·인덱싱 전에 한국형 구조적 PII를 가명화하고 복원 이력을 통제 |
| 보장 범위 | 형식·사전·체크섬 기반의 재현 가능한 검출, 가명화와 Vault 감사 |
| 비보장 범위 | 일반 NER, 자유 대화·SNS 전반, 법적 익명성 판정, 모든 도메인의 동일 정확도 |
| 운영 조건 | 고객 문서의 라벨 분포와 confusion count를 별도 측정하고 PERSON·ADDRESS 처리 정책 승인 |
| 상태 해석 | Software: Stable은 패키지 안정성이다. 고객 환경은 도메인 qualification 전까지 미승격 |
결정적 식별자와 PERSON·ADDRESS 같은 문맥형 라벨을 하나의 전체 F1으로 합쳐 운영 결정을 내리지 않습니다. 고객별 승격에서는 critical identifier, 문맥형 라벨, Vault 운영을 따로 검증해야 합니다.
from ko_pii import Anonymizer, ProcessingMode
result = Anonymizer(mode=ProcessingMode.STRICT, strategy="tokenize").process(
"신청인 홍길동 (880101-1234568) 연락처 010-1234-5678"
)
print(result.text)
# 신청인 <PERSON_1> (<RRN_1>) 연락처 <PHONE_1>
print(result.vault.reveal("<RRN_1>")) # 880101-1234568 (권한자만 복원)
print(result.combined_risk.combined_risk.name) # CRITICAL
조각 단위 입력은 각 조각을 따로 검사하지 말고, 최종 판정 전까지 보관하십시오.
from ko_pii import PreForwardAnonymizer
stream = PreForwardAnonymizer(max_chars=100_000)
stream.feed("주민번호 880101-")
stream.feed("1234568 입니다")
safe_result = stream.finalize() # 이 시점 전에는 원문을 downstream으로 보내지 않음
이 API는 토큰별 저지연 출력을 가장하지 않습니다. 식별자가 임의의 chunk 경계에서 나뉠 수 있으므로, 개인정보 경계에서는 완성된 메시지를 가명화한 뒤 전달하는 것이 기본 계약입니다.
원본:
신청인 홍길동 (880101-1234568) 연락처 010-1234-5678
주소: 서울특별시 강남구 테헤란로 152
tokenize (토큰 치환 + Vault 복원 가능):
신청인 <PERSON_1> (<RRN_1>) 연락처 <PHONE_1>
주소: <ADDRESS_1>
partial (일부만 가림 — 실무 양식):
신청인 홍OO (880101-1******) 연락처 010-****-5678
주소: 서울특별시 강남구 ***
redact (카테고리명 치환):
신청인 [성명] ([주민등록번호]) 연락처 [전화번호]
주소: [주소]
처음이시면:
mode=ProcessingMode.STRICT+strategy="tokenize"추천. 가장 안전한 기본 설정 (MEDIUM 위험도 이상 차단 + Vault 복원 가능).
ko-pii report.hwp --strategy tokenize (표·머리말·꼬리말·메타데이터 모두 처리)RAG·LLM 파이프라인은 미정제 비정형 데이터를 그대로 인덱싱·검색해 모델에 넣습니다 — PII 가 벡터 DB 와 응답 양쪽에 노출됩니다.
ko-pii 는 RAG 의 인제스트(벡터 DB 진입 전)와 검색(LLM 전달 전) 양단에서 PII 를 차단합니다. 같은 인물은 같은 토큰으로 치환해 문맥을 보존하고(LlamaIndex·LangChain 연동 제공), Vault 로 권한 기반 복원과 감사 추적을 지원합니다.
010)·제로폭 문자 삽입 등 유니코드 우회를 정규화로 무력화 (검출 offset 은 원본 보존)DetectionResult (label/start/end/text/confidence) 표준 객체 출력. ML 파이프라인 앞단에 끼워넣기 편함[file] extras)| 도메인 | 권장 설정 | 비고 |
|---|---|---|
| 공공 문서 (공문서·민원·인사) | STRICT + tokenize | 기본값. 가장 잘 맞는 도메인 |
| LLM 학습 데이터 전처리 | PARANOID + tokenize 또는 redact | 누수 차단 우선 |
| 의약품·바이오 | STRICT + exclude={"AGE","HEIGHT","WEIGHT"} | "체중 1kg당" 같은 용법 오탐 방지 |
| 금융·보험 | STRICT + tokenize | RRN·카드·계좌 결정적 검출 |
| 일반 사무 (사내 문서) | BALANCED + partial | 읽기 편한 부분 마스킹 |
# 의약품 도메인 — PERSON FP 방지 + 신체속성 오탐 방지
anon = Anonymizer(
mode=ProcessingMode.STRICT,
strategy="tokenize",
exclude={"AGE", "HEIGHT", "WEIGHT"}, # "체중 1kg당" 오탐 방지
)
# PERSON FP 가 많다면 — 도메인 사전 주입
# src/ko_pii/dictionaries/common_words.py 에 의약품 성분명·제조사명 추가
# 예: "이부프로펜", "한미약품", "메트포르민" → PERSON 에서 자동 제외
pip install ko-pii # 현재 PyPI stable
# main의 1.16 API를 태그 배포 전에 검증하려면
python3 -m pip install \
"ko-pii @ git+https://github.com/Marker-Inc-Korea/ko-pii.git@main"
extras 옵션 (필요 시):
pip install "ko-pii[file]" # HWP/HWPX/DOCX/PDF
pip install "ko-pii[security]" # Vault AES-256-GCM
Python 3.10 이상. 코어는 표준 라이브러리만 사용.
from pathlib import Path
from ko_pii import Anonymizer, ProcessingMode
anon = Anonymizer(mode=ProcessingMode.PARANOID, strategy="tokenize")
for path in Path("./공문서/").glob("*.hwp"):
result = anon.process(path.read_text(encoding="utf-8"))
Path(f"./가명화/{path.name}").write_text(result.text, encoding="utf-8")
# vault.json 분리 보관 (권한 있는 사용자만 복원 가능)
result.vault.save(f"./vault/{path.stem}.json")
pip install "ko-pii[file]"from ko_pii import Anonymizer, ProcessingMode, RiskLevel
anon = Anonymizer(mode=ProcessingMode.AUDIT) # 차단 X, 검출만 보고
result = anon.process(incoming_petition_text)
# 결합 위험도가 CRITICAL 이면 담당자에게 알림
if result.combined_risk.combined_risk >= RiskLevel.CRITICAL:
notify_admin(
identifiers=result.combined_risk.distinct_identifiers, # ["RRN"]
quasi=result.combined_risk.distinct_quasi, # ["ADDRESS", "PERSON", "PHONE"]
)
# 응대 직원에게는 가명화 버전 제공
masked = Anonymizer(mode=ProcessingMode.STRICT, strategy="partial").process(
incoming_petition_text
).text
partial 전략으로 일부만 마스킹 (880101-1******)# 코드 어디서든 logger.info("...") 호출 시 자동 가명화
import logging
from ko_pii import Anonymizer, ProcessingMode
_anon = Anonymizer(mode=ProcessingMode.STRICT, strategy="redact")
class PIIFilter(logging.Filter):
def filter(self, record):
record.msg = _anon.process(str(record.msg)).text
return True
logging.getLogger().addFilter(PIIFilter())
logging.info("신청인 홍길동 (880101-1234568) 처리 완료")
# → "신청인 [성명] ([주민등록번호]) 처리 완료"
| 평가 도메인 | 문서 수 | ko-pii | openai/PF | Presidio |
|---|---|---|---|---|
| 생성 평가셋 (행정/서식체, 검증 gold) | 540 | 0.790 | 0.451 | 0.483 |
| KDPII (한국어 일상 대화) | 4,891 | 0.660 | 0.264 | 0.273 |
| KLUE-NER (신문기사 풀네임) | 5,000 | 0.419 | 0.155 | 0.000 |
아래 결과는 명시한 데이터셋과 단일 채점 조건의 측정값입니다. 보편적 정확도 순위나 고객 환경의 운영 적합성을 자동으로 의미하지 않습니다:
| 시스템 | 지연/문서 | 처리량 | 100만 문서 비용 | 특성 |
|---|---|---|---|---|
| ko-pii | 0.19 ms | ~5,350/s | ~$0 (CPU 1코어 ~3분) | 결정적 · 체크섬 검증 · 온프레미스 |
| Presidio | 4.2 ms | ~238/s | ~$0 | 한국 카테고리 다수 미지원 |
| openai/privacy-filter | 481 ms | ~2/s | ~$0 | 한국 카테고리 7종만 |
→ 0.19 ms/문서(~5,350 문서/s)로 Presidio(4.2ms)보다 22배 빠르고, 결정적이라 같은 입력에 항상 같은 결과 · 검출마다 근거 조항/evidence 부착. 속도·비용·정성 전체 비교: 종합 비교표(한글).
match_forms_overlap, person_min_length=3)로 전체 4,891문서 재측정 — openai/privacy-filter (660M ML) · Microsoft Presidio 등 룰·NER 도구 대비 우위.data/generated_eval.jsonl)했다. 자기 주입 데이터는
아니지만 생성 데이터의 분포 편향과 평가 불확실성은 남으므로 고객 문서 성능을 대체하지
않는다 (상세 BENCHMARK §3b).docs/BENCHMARK.md · docs/EVALUATION_REPORT.md. (KLUE 는 이전 방법론 수치)운영 전 권장: 사용하시는 도메인의 실제 문서 30~100건을 직접 테스트해보세요. 도메인마다 성능 차이가 있습니다.
common_words.py 도메인 사전 주입, exclude={"PERSON"}, 또는 하이브리드 분류기(dev/classifier)를 권한다. (재현: python -c "from ko_pii.eval.klue_ner import load_klue_ner, evaluate_person; print(evaluate_person(load_klue_ner('data/klue_ner/klue-ner-v1.1_dev.tsv')).format())")1588-2024)는 제품·연식 번호와, 무구분자 13~19자리는 바코드·IMEI와 형식이 같다. 길이-브랜드 일관성·단어경계 문맥으로 상당수 거르지만, recall 우선이라 형식만으로 완전 구분은 불가 — 최종 정밀 판단은 주변 문맥(검토 큐) 또는 하이브리드 분류기(dev/classifier)의 몫상세 평가: docs/EVALUATION_REPORT.md.
# 기본
ko-pii input.txt --mode STRICT --strategy tokenize \
--vault vault.json -o output.txt --report report.html
# 배치 (디렉토리 일괄, 병렬)
ko-pii ./incoming/ --batch --workers 4 --output-dir ./anonymized/
# Vault 암호화 + 감사 로그
KPII_VAULT_PASSWORD=secret ko-pii doc.hwp \
--vault vault.kvault --audit-log audit.jsonl
from ko_pii import Anonymizer, ProcessingMode
anon = Anonymizer(mode=ProcessingMode.STRICT, strategy="tokenize")
result = anon.process(text)
print(result.text) # 가명화된 텍스트
print(result.vault.reveal("<RRN_1>")) # 원본 복원 (권한자만)
print(result.summary["by_label"]) # {"RRN": 1, "PHONE": 1, "PERSON": 1}
# 검출 결과의 결합 위험도 자동 평가
print(result.combined_risk.combined_risk.name) # CRITICAL
print(result.combined_risk.distinct_identifiers) # ["RRN"]
print(result.combined_risk.distinct_quasi) # ["PERSON", "PHONE"]
# k-익명성 평가 (집단 데이터)
from ko_pii.analytics import k_anonymity
report = k_anonymity(records, quasi_keys=["age", "city", "job"], threshold=5)
print(report.k) # 최소 그룹 크기
print(report.satisfies_threshold) # True/False
print(report.rationale) # ["준식별자 ['age', 'city', 'job'] 기준 N개 그룹", ...]
from ko_pii.tabular import anonymize_records
import csv
rows = list(csv.DictReader(open("employees.csv")))
# 헤더 "성명/주민번호/연락처/주소" → 자동으로 PERSON/RRN/PHONE/ADDRESS 매핑
anon_rows, vault = anonymize_records(rows, strategy="tokenize")
print(anon_rows[0])
# {'성명': '<PERSON_1>', '주민번호': '<RRN_1>', '연락처': '<PHONE_1>', '주소': '<ADDRESS_1>'}
SQL 스키마처럼 오탐이 곧 쿼리 차단으로 이어지는 경로에서는 부분 문자열 매핑 대신
근거와 신뢰도를 반환하는 strict API를 사용합니다. product_name은 name으로 분류되지
않고, 단독 name은 자동 차단이 아니라 낮은 신뢰도의 검토 대상으로 표시됩니다.
from ko_pii.tabular import classify_schema_columns
evidence = classify_schema_columns(["성명", "주민등록번호", "product_name", "name"])
print(evidence["성명"]) # PERSON, confidence=1.0
print(evidence["name"].confidence) # 0.60 (ambiguous)
assert "product_name" not in evidence
confidence 낮은 검출 → 검토 큐에 저장 → 사용자가 FP/OK/FN 마킹 → 누적 마킹에서 사전 추천 패치 자동 생성 (자동 반영 X, 사람 검토 후 반영).
result = anon.process(text)
# 1. confidence 낮아 REVIEW 분류된 검출 (모드별 자동 분류)
for record in result.review_items():
d = record.detection
print(d.text, d.confidence, d.evidence)
# 2. 별도 JSONL 큐에 저장 → 사용자가 verdict 마킹
from ko_pii.review.queue import ReviewQueue
q = ReviewQueue("review.jsonl")
q.enqueue_review_records(result.review_items(), document=text)
# 3. 누적 마킹 → 패치 파일 생성 (common_words 후보 / 이름 후보)
from ko_pii.review.feedback import apply_feedback
apply_feedback(
queue_path="review.jsonl",
output_dir="feedback_patches/",
min_repeat=2, # 같은 토큰이 2회 이상 FP → 후보 (사전 오염 방지)
)
# → feedback_patches/common_words_additions.txt (PERSON FP 후보)
# → feedback_patches/names_to_add.txt (FN 표시 이름)
# → feedback_patches/summary.json
from ko_pii.patterns.rrn import detect
for r in detect("신청인 880101-1234568"):
print(r.label, r.text, r.confidence, r.legal_basis)
# RRN 880101-1234568 1.0 개인정보보호법 제24조의2
전체 라벨 키는
ko-pii --labels(CLI) 또는from ko_pii.labels import ALL_LABELS, LABEL_INFO(Python) 로 조회할 수 있습니다.
| 카테고리 | 검증 | 위험도 |
|---|---|---|
| RRN (주민등록번호) | 13자리 + 날짜 + 한국 체크섬 | CRITICAL |
| FRN (외국인등록번호) | gender 5-8 + 체크섬 | CRITICAL |
| 사업자등록번호 | 국세청 가중합 체크섬 | HIGH |
| 법인등록번호 | 법인 체크섬 (RRN 우선) | MEDIUM |
| 운전면허번호 | 지방청 코드 11-28 화이트리스트 | HIGH |
| 여권번호 | prefix (M/S/PP/PD 등) + 8자리 | CRITICAL |
| 신용카드 | BIN 화이트리스트 + Luhn | CRITICAL |
| 필지고유번호 (PNU) | 19자리 + 시·도 코드 | LOW |
| 카테고리 | 키워드 |
|---|---|
| 건강보험증 | 건강보험 / 의료보험 / 보험증 |
| 처방번호 | 처방번호 / Rx / 교부번호 |
| 의약품 코드 | 약품코드 / KD코드 + 한국 GS1 |
| 팩스번호 | 팩스 / FAX |
| 계좌번호 | 계좌 / 은행명 60+ (3-way anchor) |
| 사번 | 사번 / 공무원번호 / 직원번호 / 임용번호 |
| 민원번호 | 민원 / 청구 / 정보공개 / 행정심판 |
| 사건번호 | 사건유형 (가합/고합/구합/헌가 등) |
| 카테고리 | 검증 |
|---|---|
| 전화번호 | 모바일 010-019 / 서울 02 / 지방 031-064 / VoIP 070 / 대표 15xx-18xx / +82 국제 |
| 이메일 | RFC 5322 |
| IP | IPv4 옥텟 + IPv6 RFC 4291 |
| URL | http(s) / ftp |
| 우편번호 | 시·도 첫자리 매핑 |
| 차량번호 | 신형 NN[가-힣]NNNN + 용도 한글 화이트리스트 |
| 공문서번호 | 부처명 + 형식 |
| 카테고리 | 사전 규모 |
|---|---|
| 인명 (PERSON) | 성씨 286 + 직책 인접 + 17 거부 룰 |
| 주소 (ADDRESS) | 광역 17 + 기초 226 + 빈출 동 240 + 법정동 10K (anchor 조건부) + 건물 접미사 38 + 동/호/층 bridge 확장 |
| 국적 (NATIONALITY) | 국가명 70+ (대한민국·미국·일본 등) |
| 학력 (EDUCATION) | 대학 ~330 + 약칭 |
| 전공 (MAJOR) | 학과 ~400 (KEDI 분류) |
| 직책 (POSITION) | titles 250+ (정부·경찰·소방·군·검사·법관·민간) |
| 카테고리 | 검증 | 위험도 |
|---|---|---|
| 생년월일 | 날짜 + 키워드/풀네임/년생 marker | HIGH |
| 나이 | "32세 / 32살 / 환갑 / 12개월 아기 / 30대" | INFO |
| 신장 | "175cm / 1.75m" 50–250 | INFO |
| 체중 | "70kg / 70킬로" 1–300 | INFO |
준식별자 (Quasi-Identifier) 단독으로는 식별 불가지만 다른 정보와 결합 시 재식별 위험.
analytics/combined_risk가 자동 평가.
각 PII 검출은 단순 정규식 매칭이 아닌 multi-gate: 접두어 라벨 / 키워드 anchor / 문맥 사전 / 형식 검증 의 조합.
라벨 직후 1~4글자 한글 → 강한 PERSON 후보.
| 도메인 | 라벨 |
|---|---|
| 기본 | 성명 이름 성함 이 름 |
| 민원·행정 | 신청인 신청자 민원인 청구인 보호자 대리인 당사자 |
| 결재 | 기안자 결재자 검토자 보고자 수신자 발신자 참조 |
| 사법 | 원고 피고 고소인 피고소인 증인 감정인 |
| 경찰·소방 | 피의자 피해자 용의자 참고인 신고자 수사관 출동대장 |
| 인사 | 평가자 피평가자 면담자 추천인 |
| 의료 | 환자 |
라벨 variant 7종 인식: 성명: 홍길동 / [성명] 홍길동 / (성명) 홍길동 / <성명> 홍길동 등.
김부장 김포시 이화여대 → 거부~은데 ~는데 ~라서 ~까지 끝 → 거부보건복지부 행정안전부 → 거부박씨 김모씨 ○○○ 시민 → 거부✓ 성명: 김도윤 (필드 라벨)
✓ 박지훈 과장님께 (직책 인접)
✓ 홍길동(洪吉童) (한자 병기)
✓ 880101-1234568 (RRN — 체크섬)
✓ 120-81-47521 (사업자 — 국세청 체크섬)
✓ 4242-4242-4242-4242 (카드 — Luhn)
✓ M12345678 (여권)
✗ 김부장이 협조 안 함 (호칭 = 거부)
✗ 보건복지부는 검토 후 (부처명)
✗ 시행일자: 2026-05-20 (비-생일 거부)
✗ 881301-1000004 (RRN — 13월 무효)
✗ A12345678 (여권 — A prefix 거부)
| 모드 | 차단 기준 | 용도 |
|---|---|---|
PARANOID | LOW 이상 모두 차단 | 외부 공개·LLM 전송 전 |
STRICT | MEDIUM 이상 차단 | 실무 표준 (기본값) |
BALANCED | HIGH 이상 차단 | 내부 협업 |
PERMISSIVE | CRITICAL 만 차단 | 분석가 작업 |
AUDIT | 차단 없음, 검출만 보고 | 감사·통계 |
| 전략 | 880101-1234568 → | 가역 | 설명 |
|---|---|---|---|
tokenize | <RRN_1> | ✓ | 토큰 치환, Vault 에 원본 보관 |
redact | [주민등록번호] | ✗ | 카테고리명으로 치환 |
partial | 880101-1****** | ✗ | 일부만 가림 (실무 표준) |
asterisk | ************** | ✗ | 별표 마스킹 |
hashed | <RRN:abc123> | ✗ | 해시 (같은 값 → 같은 토큰) |
fpe | 771202-2345671 | ✗ | 형식 유지 암호화 (FPE) |
| 기능 | 설명 | 설치 |
|---|---|---|
| HWP/HWPX/DOCX/PDF 파서 | 한컴오피스·MS Word·PDF 자동 파싱 (본문 + 표 + 머리말 + 메타데이터). 아래 파서 상세 참고 | [file] |
| Vault 암호화 | AES-256-GCM + PBKDF2 480k 반복 | [security] |
| 감사 로그 (JSONL) | 모든 reveal() 호출 기록 (개인정보보호법 제29조) | 코어 |
| 배치 처리 | 디렉토리 일괄 + 병렬 워커 | 코어 |
| 검토 큐 | confidence 낮은 검출 → 사람 검토 → 오탐 어휘 자동 학습 | 코어 |
| HTML 리포트 | 정탐 초록 / 오탐 빨강 / 미탐 노랑 시각화 | 코어 |
| 한자/로마자 변형 | 洪吉童 → 홍길동, Hong Gildong → 홍길동 | 코어 |
| RAG 연동 | LlamaIndex·LangChain 검색 결과 PII 마스킹 (검색→마스킹→LLM) | [llamaindex] / [langchain] |
| 룰+ML 하이브리드 | 룰+ML 결합 모드 4종 + 임계값으로 검출 민감도 조절 (opt-in) | [classifier] |
| 포맷 | 사용 라이브러리 | 비고 |
|---|---|---|
| HWP 5.x | olefile | OLE 바이너리 레코드 직접 파싱, 본문 텍스트 추출 |
| HWPX | stdlib (zipfile + xml) | ZIP+XML 구조, 외부 의존성 없음 |
| DOCX | stdlib (zipfile + xml) | ZIP+XML 구조, 외부 의존성 없음 |
| XLSX | stdlib (zipfile + xml) | sharedStrings + sheet XML |
| pdfplumber (우선) / pypdf (fallback) | 텍스트 레이어만 추출 (스캔 PDF는 OCR 필요) |
PDF 참고: PDF는 글자 좌표 기반이라 칸별 공백·줄바꿈 삽입이 흔합니다. ko-pii는 내장 정규화 엔진(
text_normalizer)으로 PII 패턴 중간의 불필요 공백/줄바꿈을 자동 보정합니다. pdfplumber가 pypdf보다 레이아웃 분석이 우수하므로 pdfplumber 설치를 권장합니다.
신뢰하지 않는 RAG 문서를 처리할 때는 일반 read_text() 대신 파일 크기, ZIP 멤버 수,
개별·전체 압축 해제량, 압축률, 경로 탈출, XML DTD·엔티티, 추출 문자 수를 먼저 검사하는
bounded API를 사용합니다.
from ko_pii.io_ import FileReadPolicy, read_text_bounded
document = read_text_bounded(
"notice.hwpx",
policy=FileReadPolicy(max_file_bytes=8 * 1024 * 1024),
)
print(document.sha256, document.archive_members)
text = document.text
기본 bounded allowlist는 TXT·Markdown·CSV·TSV·HWPX·DOCX·XLSX입니다. PDF와 구형 HWP는
파서 자체가 결정론적 페이지·압축 해제 budget을 제공하지 않으므로, 별도 메모리·시간 제한
worker에서 실행하는 배포만 allowed_extensions로 명시적으로 활성화해야 합니다.
검색된 문서를 LLM 에 넣기 전에 PII 를 마스킹합니다. 한 번의 검색 결과 안에서 같은 인물은 같은 토큰(<PERSON_1>)으로 치환돼 문맥이 보존되고, vault 를 넘기면 답변 생성 후 vault.reveal() 로 복원할 수 있습니다.
# LlamaIndex — node postprocessor (검색 → 마스킹 → LLM)
from ko_pii.integrations.llamaindex import KoPiiNodePostprocessor
qe = index.as_query_engine(
node_postprocessors=[KoPiiNodePostprocessor(mode="STRICT")]
)
# LangChain — Runnable 체인에 그대로 삽입
from ko_pii.integrations.langchain import KoPiiRedactor
chain = retriever | KoPiiRedactor(mode="STRICT") | prompt | llm
코어는 ML 없이 동작하되, 정확도가 더 필요하면 ML 을 얹을 수 있습니다. 하이브리드는 두 종류이며 서로 다른 기능입니다:
| ① 토큰 NER 하이브리드 (span 교체) | ② 문서 분류기 하이브리드 (confidence 결합) | |
|---|---|---|
| 무엇 | 룰=결정적 ID(체크섬), ML=퍼지(이름·주소 등) 검출 자체를 교체 | 문서 수준 "PII 있음/없음" 분류기로 룰 결과를 보강 (span 추가 없음) |
| 사용 | Anonymizer(secondary_detector=..., merge_mode="role_split") | ko_pii.classifier.HybridAnonymizer |
| 성능 | 외부 검증 F1 0.97 (docs/HYBRID_NER.md) | 검토 트리거·민감도 조절용 |
① 토큰 NER 하이브리드 — docs/HYBRID_NER.md 레시피로 직접 학습한 NER 모델을 꽂으면 됩니다:
from ko_pii import Anonymizer
from ko_pii.integrations.hf_token_ner import HFTokenNERAdapter
ml = HFTokenNERAdapter("out/ner_fuzzy/final") # 직접 학습한 모델 (레시피 참조)
anon = Anonymizer(secondary_detector=ml, merge_mode="role_split")
result = anon.process(text) # 모든 전략(tokenize/partial/redact)·Vault 그대로 동작
② 문서 분류기 하이브리드 — [classifier] extra 로 결합 방식과 민감도를 직접 설정:
| 결합 모드 | 동작 | 용도 |
|---|---|---|
SCORE | 룰 검출 + 분류기 확신도 보강 | 기본 |
GATED | 분류기 점수 < 임계값이면 룰 skip | 속도 우선 |
REVIEW_FLAG | 룰 0건인데 분류기 높으면 '검토 권고' | 사람 검토 트리거 |
UNION_BLOCK | 한쪽이라도 PII로 보면 차단 | 보수적 마스킹 |
classifier_threshold · gate_threshold 로 룰 ↔ ML 결합 비율(민감도)을 세밀하게 조절합니다.
from ko_pii import Anonymizer, ProcessingMode
from ko_pii.classifier import PIIClassifier, HybridAnonymizer, HybridMode
clf = PIIClassifier.from_pretrained("models/...")
hybrid = HybridAnonymizer(
Anonymizer(mode=ProcessingMode.BALANCED), clf,
mode=HybridMode.REVIEW_FLAG, # 결합 방식
classifier_threshold=0.5, # 민감도(비율) 조절
)
pip install ko-pii[classifier] # torch + transformers + scikit-learn
python -m ko_pii.classifier.train ... # 모델은 직접 학습
사전학습 가중치는 배포하지 않습니다(학습 데이터 라이선스) — 코드·학습 레시피 제공. 튜닝 NER 모델은 추후 공개 예정(라이선스·일반화 평가 후).
하이브리드 평가(룰단독 vs 하이브리드, base vs tuned 매트릭스):
docs/HYBRID_NER.md— 룰 대비 +0.14~0.19(klue), 단 제대로 튜닝된 한국어 NER이라야 일관 효과(base zero-shot 은 역효과). 실험 전 과정(학습·평가 코드, 실행 로그, 원시 결과)은 (내부 NER 실험 아카이브 — 비공개) 아카이브 참조.
Q1. ML 없이 룰만으로 정말 잘 되나요? 한국 핵심 구조적 PII는 형식·체크섬으로 결정론 검증할 수 있어 ML과 상보적이다. PERSON 같은 맥락 의존적 PII는 ML이 더 나을 수 있고, 생성 행정·서식형 평가셋 540문서의 전체 F1 0.790은 고객 운영 보장이 아니라 도메인 qualification의 참고값이다 (docs/BENCHMARK.md §3b).
Q2. 오탐이 많으면?
common_words.py 에 도메인 사전 주입, exclude={"PERSON"} 으로 특정 카테고리 끄기, 모드 변경 (STRICT → BALANCED).
Q3. Vault 분실하면?
복원 불가 (보안 설계). [security] extras 로 암호화 보관 또는 strategy="redact" (카테고리명 치환, Vault 불필요) 사용.
Q4. HWP 표·머리말 다 잡히나요?
네. [file] extras 설치 시 본문 + 표 + 머리말 + 꼬리말 + 메타데이터 모두 추출.
Q5. 다른 도구 (Presidio / openai) 와 뭐가 다르나요?
Q6. 이름 단독 검출이 오탐이 많아요. 이름+전화번호 같이 있을 때만 차단할 수 있나요?
PERMISSIVE 모드 (CRITICAL만 차단) + combined_risk 조건부 재처리:
result = Anonymizer(mode=ProcessingMode.PERMISSIVE).process(text)
if result.combined_risk.combined_risk >= RiskLevel.HIGH:
result = Anonymizer(mode=ProcessingMode.STRICT).process(text)
git clone https://github.com/Marker-Inc-Korea/ko-pii
cd ko-pii
pip install -e ".[dev]"
pytest # 전체 테스트 통과
상세 문서: docs/ 디렉토리 참조.
MIT License
108 commits
Python
100.0%