소프트웨어와 생활

글에서 낱말 세기

글은 표가 아닙니다
먼저 숫자로 바꾸어야 합니다

학습 목표

  1. 글을 낱말로 잘라 세는 과정을 차례대로 설명할 수 있다.
  2. 한국어를 띄어쓰기로만 자를 때 무엇이 어긋나는지 알고 다룰 수 있다.
  3. 낱말 횟수만으로 말할 수 있는 것과 없는 것을 구분해 말할 수 있다.

무엇을 하나

도서관 대출 기록처럼 이미 표로 되어 있는 데이터는 묶어 세면 됩니다. 이번에는 글입니다. 글은 표가 아니므로 groupby를 쓸 수도, 평균을 낼 수도 없습니다. 먼저 숫자로 바꾸어야 합니다.

답하려는 물음

「우리 반 감상문에는 어떤 말이 자주 나올까?」

자주 나온 말을 알면 학생들이 무엇에 대해 이야기했는지를 짐작할 수 있습니다. 여덟 편을 사람이 다 읽어도 되지만, 수백 편이라면 다 읽기 어렵습니다. 이런 일을 소프트웨어가 맡습니다.

쓸 파일은 3-1-5.감상문.txt입니다. 여덟 편의 짧은 감상문이 들어 있고, 수업용으로 지어낸 글입니다. 사람 이름은 「학생 A」처럼 적어 두었습니다.

글을 표로 바꾸는 세 걸음 글 비정형 자르기 낱말 목록으로 정제 조사 떼고 거르기 세기 표가 된다 여기서부터는 표로 된 데이터처럼 다룰 수 있다

맨 오른쪽에 이르면 「낱말」과 「횟수」 두 칸짜리 표가 됩니다. 그러면 표로 된 데이터를 다루는 방법을 그대로 쓸 수 있습니다. 비정형을 정형으로 옮기는 일이 바로 이것입니다.

스스로 확인

글을 「낱말과 횟수」 표로 바꾸면 무엇이 버려질까요? 말투나 글의 흐름처럼 표에 담기지 않는 것을 떠올려 두 가지만 적어 보세요.

자르기

먼저 파일을 읽고 얼마나 되는 글인지 확인합니다. 표에서 줄 수를 먼저 본 것과 같습니다.

글 = FILE.read_text(encoding="utf-8")
print("글자 수:", len(글))
print("줄 수:", len(글.strip().split("\n")))

띄어쓰기와 줄바꿈까지 세어 1,037글자입니다. 이제 이것을 낱말로 자릅니다. 가장 단순한 방법은 띄어쓰기에서 자르는 것입니다.

글 = re.sub(r"[^가-힣\s]", " ", 글)     # 한글과 띄어쓰기만 남긴다
낱말들 = 글.split()
print("자른 낱말 수:", len(낱말들))
print("서로 다른 낱말 수:", len(set(낱말들)))
자르기 부분의 각 줄이 하는 일
코드 하는 일
re.sub(...) 한글과 띄어쓰기만 남기고 마침표 · 쉼표 · 영문자를 공백으로 바꾼다. 이것을 안 하면 「바다.」와 「바다」가 서로 다른 낱말로 세어진다.
글.split() 띄어쓰기마다 잘라 낱말 목록으로 만든다.
set(낱말들) 같은 것을 하나로 묶어 서로 다른 낱말이 몇 가지인지 센다.

결과는 자른 낱말 252개, 서로 다른 낱말 213가지입니다. 여기서 벌써 이상한 점이 보입니다.

252개 가운데 213가지가 서로 다르다?

거의 모든 낱말이 한 번씩만 나왔다는 뜻입니다. 여덟 편이나 되는 글에서 같은 말이 거의 겹치지 않는다는 것은 이상합니다.

까닭은 조사입니다. 「바다」 · 「바다에」 · 「바다가」 · 「바다에서」가 모두 다른 낱말로 세어진 것입니다. 한국어를 띄어쓰기로만 자르면 이 일이 생기기 쉽습니다. 조사가 낱말에 붙여 적히기 때문입니다.

스스로 확인

영어로 된 글이었다면 이 문제가 덜했을까요, 더했을까요? 그렇게 생각한 까닭을 말해 보세요.

조사 떼기

조사를 떼어 내야 같은 말이 같은 말로 세어집니다. 뒤에 자주 붙는 조사를 목록으로 적어 두고, 낱말 끝이 그 목록에 있으면 잘라 내는 방법을 쓰겠습니다.

def 조사_떼기(낱말):
    """낱말 뒤에 붙은 조사를 하나만 떼어 낸다.

    긴 조사부터 살펴야 한다. 「에서」를 먼저 보지 않으면
    「바다에서」가 「바다에」로만 줄어든다.
    """
    for 꼬리 in JOSA:
        if len(낱말) > len(꼬리) + 1 and 낱말.endswith(꼬리):
            return 낱말[: -len(꼬리)]
    return 낱말


낱말들 = [조사_떼기(w) for w in 낱말들]
낱말들 = [w for w in 낱말들 if len(w) >= 2 and w not in STOPWORDS]
print("걸러 낸 뒤 낱말 수:", len(낱말들))

긴 조사부터 살펴야 하는 까닭

목록의 순서가 중요합니다. 「에서」보다 「에」를 먼저 보면 「바다에서」가 「바다」가 아니라 「바다에」로만 줄어듭니다. 끝에서 한 글자만 떼고 멈추기 때문입니다.

코드의 JOSA 목록을 보면 긴 것이 앞에 있습니다. 이런 순서는 실행해 보기 전에는 잘 눈에 띄지 않는 종류의 실수입니다.

조건에 len(낱말) > len(꼬리) + 1이 붙어 있는 것도 눈여겨보세요. 떼고 나서 한 글자만 남는 경우를 막는 것입니다. 「내가」에서 「가」를 떼면 「내」 한 글자만 남아 무슨 말인지 알 수 없게 됩니다.

그리고 거르는 일도 함께 합니다. 두 글자가 안 되는 것과, 뜻을 크게 담지 않는 말을 뺍니다. 「이」 · 「그」 · 「것」 같은 말은 어느 글에나 많이 나와서 세어 봐야 알 수 있는 것이 없습니다.

정제 전후의 낱말 수 변화
무엇을 했나 낱말 수 무슨 뜻인가
띄어쓰기로 자른 뒤 252 조사가 붙은 채라 같은 말이 흩어져 있다
조사를 떼고 걸러 낸 뒤 214 38개가 빠졌다. 짧은 말과 뜻을 크게 담지 않는 말이 사라진 것이다

이 방법은 완전하지 않다

실행해 보면 「것이」 · 「나서」 · 「처음에」 같은 말이 여전히 위쪽에 남아 있습니다. 「것이」는 두 글자라 조건에 걸려 조사를 떼지 못했고, 「처음에」는 글에 「처음에는」으로 적혀 있어 조사를 하나만 떼는 방법으로는 「는」만 떨어지고 「에」가 남았습니다.

제대로 하려면 한국어의 문법을 아는 도구가 필요합니다. 그런 도구는 「먹었다」에서 「먹다」를 찾아 주고, 「것이」의 「것」이 명사임을 알려 줍니다.

그래도 이 단순한 방법을 먼저 해 보는 데 뜻이 있습니다. 무엇이 어긋나는지 겪어 봐야 왜 그런 도구가 필요한지 알게 됩니다.

스스로 확인

STOPWORDS에 「이야기」를 넣으면 어떻게 될까요? 넣는 것이 좋을까요, 넣지 않는 것이 좋을까요? 이 판단은 무엇에 달려 있나요?

해석

이제 세어 봅니다. Counter는 같은 것이 몇 번 나왔는지를 한 번에 세어 주는 도구이고, most_common(10)은 많이 나온 것부터 열 개를 돌려줍니다.

센것 = Counter(낱말들)
for 낱말, 횟수 in 센것.most_common(10):
    print(f"{낱말:>6}  {횟수}회  {'█' * 횟수}")

'█' * 횟수가 재미있습니다. 글자를 횟수만큼 이어 붙여 막대그래프처럼 보이게 한 것입니다. 그림을 그리는 도구 없이도 모양을 눈으로 볼 수 있습니다.

감상문에서 자주 나온 낱말 열 가지. 수업용으로 지어낸 글에서 나온 결과다.
낱말 횟수 눈여겨볼 점
바다 4 여러 편에 걸쳐 나왔다면 여러 사람이 이야기한 것이다
장면 4 줄거리보다 인상 깊은 장면을 적은 글이 많다는 뜻일 수 있다
도시 4 —
처음에 · 갔다 · 나서 · 것이 각 3 조사를 덜 뗐거나 활용한 꼴 그대로 남은 말이다. 정제가 덜 된 낱말
이야기 · 사람 · 마지막 각 3 —

「바다가 4번 나왔다」에서 무엇을 말할 수 있나

말할 수 없는 것 — 「학생들이 바다를 좋아한다」. 횟수는 좋아하는지 싫어하는지를 알려 주지 않습니다. 「바다가 무서웠다」도 「바다가 좋았다」도 똑같이 한 번으로 세어집니다.

말할 수 없는 것 둘 — 「네 명이 바다 이야기를 했다」. 한 사람이 네 번 썼을 수도 있습니다. 사람 수를 알려면 글마다 나누어 세어야 합니다.

말할 수 있는 것 — 「이 여덟 편을 이 방법으로 세었을 때 바다라는 말이 네 번 나왔다. 장면 · 도시와 함께 가장 많이 나온 말이다.」 여기까지입니다.

그래서 낱말 세기는 끝이 아니라 시작입니다. 자주 나온 말을 찾아 그 말이 나온 문장들을 직접 읽어 보는 것이 다음 걸음입니다. 기계가 어디를 읽어야 할지 좁혀 준 셈입니다.

해 보기 — 네 가지 고쳐 보기

  1. STOPWORDS에 「것이」 · 「나서」를 넣고 다시 실행해 보세요. 열 개의 목록이 어떻게 달라지나요?
  2. most_common(10)을 20으로 바꿔 보세요. 아래쪽은 대부분 몇 번씩 나오나요?
  3. 글을 편마다 나누어 세어 보세요. 「바다」가 몇 편에 나왔는지를 알 수 있습니다.
  4. 우리 반이 실제로 쓴 감상문으로 바꿔 실행해 보세요. 이때 이름은 반드시 지우고 씁니다.

편마다 나누어 세기가 특히 쓸모 있습니다. 「몇 번 나왔나」와 「몇 편에 나왔나」는 다른 물음이고, 여러 사람이 무엇을 말했는지 알고 싶을 때는 「몇 편에 나왔나」가 맞는 물음입니다.

스스로 확인

「좋지 않았다」를 띄어쓰기로 자르면 「좋지」와 「않았다」가 따로 세어집니다. 「좋」으로 시작하는 말만 모아 세면 어떤 오해가 생길까요?

핵심 정리

무엇을 하나

  • 글은 표가 아니므로 먼저 숫자로 바꾸어야 한다.
  • 자르기 → 정제 → 세기를 거치면 두 칸짜리 표가 된다.
  • 표가 된 뒤부터는 표로 된 데이터와 똑같이 다룰 수 있다.

자르기

  • 마침표와 쉼표를 먼저 없앤다. 안 그러면 「바다.」와 「바다」가 다른 낱말이 된다.
  • 252개 가운데 213가지가 서로 다르다는 것은 이상하다는 신호다.
  • 까닭은 조사다. 「바다」 · 「바다에」 · 「바다에서」가 따로 세어진다.

조사 떼기

  • 조사를 뗄 때는 긴 것부터 살펴야 한다.
  • 뜻을 크게 담지 않는 말은 세기 전에 걸러 낸다.
  • 이 방법은 완전하지 않다. 무엇이 어긋나는지 겪어 봐야 왜 문법을 아는 도구가 필요한지 알게 된다.

해석

  • 횟수는 좋아하는지 싫어하는지를 알려 주지 않는다.
  • 「몇 번 나왔나」와 「몇 편에 나왔나」는 다른 물음이다.
  • 낱말 세기는 끝이 아니라 어디를 읽어야 할지 좁혀 준 시작이다.

확인 퀴즈

배운 내용을 확인해 봅시다.

Q1

252개를 잘랐는데 213가지가 서로 다르게 나온 까닭은?

Q2

조사 목록에서 「에서」를 「에」보다 먼저 두는 까닭은?

Q3

「바다가 4번 나왔다」에서 말할 수 있는 것은?

Q4

낱말 세기가 「끝이 아니라 시작」인 까닭은?