학습 자료

이름은 정규식으로 못 잡는다 — 안 꺼내는 것이 먼저다


Article

앞 편에서 전화번호와 이메일을 지웠다. 이름이 남았다.

1. 🔴 이름은 정규식으로 못 잡는다

앞 편에서 전화번호는 모양이 있어서 잡혔다. 이름은 모양이 없다.

시도해본다. 한국 이름은 대개 성 한 글자 + 두 글자다.

SURNAMES = "김이박최정강조윤장임한오서신권황안송"

# [가-힣] 은 "한글 글자 하나" 라는 뜻이다 (가 부터 힣 까지).
# f-string 안에서 중괄호를 진짜 중괄호로 쓰려면 두 번 적어야 한다 -> {{2}}
# 합치면 "성 한 글자 + 한글 두 글자" 를 찾는 모양이 된다
name_re = re.compile(f"[{SURNAMES}][가-힣]{{2}}")

# findall 은 걸린 것을 전부 목록으로 준다 (search 는 첫 하나만)
for t in reviews[:200]:
    for m in name_re.findall(t):
        print(m, "|", t[:40])      # t[:40] = 앞 40글자만
터미널 — 이름이라고 잡힌 것들
  • 정돈된 | 재구매입니다. 결이 정돈된 느낌이 든다는 게...
  • 정도는 | 가격 생각하면 이 정도는 하는 것 같아요...
  • 한테는 | 광고를 너무 믿었나 봐요. 향이 저한테는 좀...
  • 장에도 | 천안 일산동 사는데 근처 매장에도 있더라고요.
  • 권보민 | 천안 사는 권보민입니다. 배송 빨랐어요.
  • 이름처럼 잡힌 322건 중 진짜 이름은 일부뿐

그럼 어떻게 하나 — 우리가 가진 것을 쓴다

우리는 고객 이름을 이미 알고 있다. 고객표에 다 있다. 추측하지 말고 대조한다.

15_replace.py
# 중괄호로 감싸면 집합(set)이 된다. 목록과 달리 중복이 자동으로 사라지고,
# "이 안에 있나" 를 확인하는 속도가 훨씬 빠르다
names = {r[0] for r in cur.execute("SELECT name FROM customers").fetchall()}
print(len(names), "개")

# any 는 "하나라도 참이면 참" 이다.
# n in t 는 "이름 n 이 후기 t 안에 글자로 들어 있나" 를 본다
hits = [t for t in reviews if any(n in t for n in names)]
print(len(hits), "건")
터미널
  • 213 개
  • 184 건

오탐이 하나도 없다. 실제로 있는 이름만 지우기 때문이다.

2. 🔴 그래도 60건이 남는다

여기가 오늘의 마지막 함정이다. 후기에는 본인 이름만 나오는 게 아니다.

터미널 — 사전에 없는 이름
  • 안준서 | 재구매입니다. 안준서 언니가 쓰는 거 보고 따라 샀습니다.
  • 황유진 | 두 통째 쓰는 중이에요. 황유진님이 추천해줘서 샀어요.
  • 강우진 | 강우진 언니가 쓰는 거 보고 따라 샀습니다.
  • 남의 이름이 든 후기 96건 · 그중 우리 고객이 아닌 사람 60건

3. 제일 좋은 처방 — 처음부터 안 꺼낸다

앞에서 만든 프롬프트를 다시 본다. 전화번호가 추천에 필요한가? 아니다.

SELECT 를 고치는 것이 먼저다

지우는 방식

꺼낸 뒤에 처리

  • SELECT name, phone, email, ...
  • 정규식으로 지운다
  • 빠뜨리면 그대로 나간다
  • 새 칸이 늘면 또 빠뜨린다

안 꺼내는 방식

SQL 에서 자른다

  • SELECT age, gender, skin_type
  • 애초에 변수에 안 담긴다
  • 빠뜨릴 것이 없다
  • 코드를 보면 뭘 보내는지 한눈에 보인다
이름과 전화번호를 안 꺼내도 추천은 똑같이 만들어진다. 추천에 필요한 건 나이·피부타입·산 것·평가다.
08_safe_prompt.py
# 추천에 필요한 것만 꺼낸다. 이름 · 전화 · 이메일은 SELECT 에 아예 없다
customer = cur.execute("""
    SELECT age, gender, skin_type
    FROM customers WHERE customer_id = ?
""", ("C007",)).fetchone()

history = cur.execute("""
    SELECT products.name, products.category, products.price, purchases.rating, purchases.review
    FROM purchases
    JOIN products ON purchases.product_id = products.product_id
    WHERE purchases.customer_id = ? AND purchases.is_holdout = 0
""", ("C007",)).fetchall()

그래도 후기는 남는다. 후기는 취향을 말해주니 빼면 안 된다. 그러니 후기에만 지우기를 건다.

def mask_text(text):
    text = phone.sub("[연락처]", text)
    text = email.sub("[메일]", text)
    for n in names:
        text = text.replace(n, "[이름]")
    return text
순서를 이렇게 잡는다
  1. 01

    ① 안 꺼낸다

    SELECT 에서 뺀다. 제일 확실하고 제일 싸다. 코드 한 줄로 끝나고 실수할 여지가 없다.

  2. 02

    ② 꺼내야 하는 글은 가린다

    후기처럼 뜻이 필요해서 빼면 안 되는 글만 정규식·대조로 처리한다.

  3. 03

    ③ 남는 게 있다고 적어둔다

    60건은 못 잡았다. 못 잡는다는 걸 아는 것과 모르는 것은 다르다. 문서에 적고, 필요하면 사람이 검수한다.

4. 지우는 것과 바꿔치는 것

한 가지가 더 있다. 지우면 되돌릴 수 없다.

두 방식

마스킹 — 지운다

[이름] 으로 덮는다

  • 간단하다
  • 원래 뭐였는지 못 되찾는다
  • 여러 사람이 나오면 다 똑같이 [이름]
  • 「누가 누구를 추천했나」가 뭉개진다

치환 — 바꿔친다

[고객1] 로 바꾸고 표를 든다

  • 표를 우리 쪽에 남겨둔다
  • 답이 오면 되돌릴 수 있다
  • 사람마다 다른 번호가 붙는다
  • 밖에 나가는 글자는 여전히 안전하다
16_swap.py
def mask(text, table):
    """개인정보를 번호표로 바꾸고, 되돌릴 표를 채운다."""
    # table 은 딕셔너리다 — {원본 글자: 번호표} 로 짝을 지어 담는다.
    # 같은 번호가 두 번 나오면 같은 번호표를 줘야 하므로 여기서 확인한다
    def tag_for(raw, kind):
        if raw not in table:
            # values() 는 표에 담긴 번호표들이다. startswith 로 같은 종류만 세어
            # [연락처1] 다음은 [연락처2] 가 되게 한다
            same_kind = [v for v in table.values() if v.startswith(f"[{kind}")]
            table[raw] = f"[{kind}{len(same_kind) + 1}]"
        return table[raw]

    # sub 에 글자 대신 함수를 주면, 찾을 때마다 그 함수를 불러 결과로 바꾼다.
    # lambda 는 이름 없이 그 자리에서 만드는 짧은 함수다.
    # m 은 "찾은 것", m.group() 은 "찾은 글자 그 자체"다
    text = phone.sub(lambda m: tag_for(m.group(), "연락처"), text)
    text = email.sub(lambda m: tag_for(m.group(), "메일"), text)
    for n in names:
        if n in text:
            text = text.replace(n, tag_for(n, "고객"))
    return text


def unmask(text, table):
    """모델이 준 답에 남아 있는 번호표를 원래대로 돌린다."""
    for raw, tag in table.items():
        text = text.replace(tag, raw)
    return text
터미널
  • 전: 재구매입니다. 천안 사는 권보민입니다. 배송 빨랐어요.
  • 후: 재구매입니다. 천안 사는 [고객1]입니다. 배송 빨랐어요.
  • 표: {'권보민': '[고객1]'}
  • 전: 기대가 컸나 봐요. 재고 문의는 010 8202 9049 로 부탁드려요.
  • 후: 기대가 컸나 봐요. 재고 문의는 [연락처1] 로 부탁드려요.

이번 편에 나온 것

정리
잰 것 · 안 것숫자와 내용
이름 정규식쓰면 안 된다. 「정돈된」 「한테는」을 이름으로 잡는다
이름 대조고객표 213개로 대조 → 184건. 오탐 0건
그래도 남는 것우리 고객이 아닌 사람 이름 60건
제일 좋은 처방SELECT 에서 애초에 안 꺼낸다
마스킹지운다. 되돌릴 수 없다
치환번호표로 바꾸고 표는 우리가 든다. 답이 오면 되돌린다
lambda · m.group()찾을 때마다 함수를 불러 바꾼다
말할 수 있어야 할 것완벽하지 않다는 것을 알고 있다는 것

미션

미션
  1. 01

    [필수] 내가 만든 프롬프트에서 개인정보를 없앤다

    mission_pii_1.py. 앞 편에서 뽑은 내 고객의 프롬프트를 다시 만들되 SELECT 를 고치고 후기에 가리기를 건다. 전과 후를 나란히 찍는다.

  2. 02

    [응용] 카톡 아이디와 사는 곳을 세어본다

    mission_pii_2.py. 후기에 카톡 아이디가 49건, 사는 곳이 51건 들어 있다. 이건 개인정보인가 아닌가. 잡을 수 있는지 시도해보고, 판단과 이유를 한 줄로 적는다.

  3. 03

    [도전] 가린 뒤에 추천이 나빠지는지 잰다

    mission_pii_3.py. 후기를 통째로 빼고 프롬프트를 만들어본다. 취향이 얼마나 사라지나. 내일 채점기가 생기면 이걸 숫자로 확인한다.

[응용] 카톡 아이디는 개인정보인가충분히 고민해본 뒤 꼭 필요한 경우에만 열어보세요

정답이 하나가 아니다. 그래서 이 미션을 냈다.

카톡 아이디는 그것만으로 사람을 찾을 수 있다. 전화번호와 성격이 같다. 그런데 모양이 정해져 있지 않아서 정규식으로는 못 잡는다 — 「dohyun65」와 평범한 영어 단어를 구별할 방법이 없다.

사는 곳은 좀 다르다. 「수원」 하나로는 사람이 안 찾아진다. 그런데 이름·나이와 합쳐지면 찾아진다. 하나씩 보면 괜찮은데 모이면 위험해지는 것이 개인정보의 성질이다.

실무의 답은 이렇다 — 애매하면 안 보낸다. 추천을 만드는 데 카톡 아이디와 사는 곳이 필요한가? 안 필요하다. 그럼 후기에서 그 문장을 통째로 빼도 손해가 거의 없다.

# 잡을 수 있는 것만 잡고, 못 잡는 것은 문장째 뺀다
kakao_re = re.compile(r"[^.]*카톡[^.]*\.")
contact_re = re.compile(r"[^.]*(문자|연락|공구|문의)[^.]*\.")

def drop_sentence(text):
    """연락을 주고받자는 문장은 통째로 버린다. 취향과 무관하다."""
    text = kakao_re.sub("", text)
    text = contact_re.sub("", text)
    return text.strip()


sample = "재구매입니다. 복합성인데 자극 없이 잘 썼어요. 카톡 아이디 dohyun65 입니다."
print(drop_sentence(sample))
# -> 재구매입니다. 복합성인데 자극 없이 잘 썼어요.

낱말을 지우는 것보다 문장을 버리는 쪽이 나을 때가 있다. 「카톡 아이디 [아이디] 입니다」는 남겨봐야 취향에 아무 도움이 안 된다.

무엇을 남길지 정하는 기준은 하나다 — 이 문장이 추천에 도움이 되는가. 도움이 안 되면서 위험하기만 한 것은 버리는 게 맞다.

그리고 이건 비용에도 좋다. 안 보낸 글자는 돈이 안 든다.

정리하면

한 편을 통째로 지우는 법에 썼는데, 결론은 지우는 것으로는 부족하다였다.

첫 정규식이 152건 중 42건만 잡았다. 그걸 알아챈 방법은 정규식을 노려본 게 아니라 원본이 몇 건인지 세어본 것이다. 지운 수와 원래 수를 나란히 안 보면 놓친 것을 못 본다.

이름은 아예 다른 문제였다. 모양이 없어서 정규식으로 잡으려 하면 「정돈된」과 「한테는」이 사람 이름이 된다. 우리가 가진 고객표로 대조하니 오탐 없이 184건을 잡았지만, 우리 회원이 아닌 사람 이름 60건은 여전히 남았다.

그래서 순서를 뒤집었다. 제일 좋은 처방은 지우는 게 아니라 안 꺼내는 것이다. SELECT 에서 이름과 전화번호를 빼면 지울 것 자체가 없고, 빠뜨릴 일도 없다. 꺼내야만 하는 후기에만 지우기를 걸고, 못 잡는 게 남는다는 사실을 적어둔다.

마지막으로 지우기와 바꿔치기를 나눠 봤다. 지우면 끝이지만 번호표로 바꿔두면 답이 왔을 때 되돌릴 수 있다. 금요일에 화면에 이름을 붙여 보여줄 때 이게 필요해진다.

다음 편에서 답의 모양을 정한다. 지금은 모델이 문장으로 대답한다. 그걸 화면에 뿌리려면 상품 번호와 이유가 정해진 자리에 들어 있어야 한다. 말로 부탁하면 안 되고 형식을 고정해야 한다 — 그게 다음 편이다.

Share
  • 파이썬
  • 개인정보
  • 보안
  • 설계