2020/11/21 - [Python/Community Manager를 위한 Python] - 4. 저장된 txt 파일은 어떻게 정제해야 할까? 2편

위 링크인 이전 글에 이어서 이번 정제를 마무리를 짓도록 한다. 일단 전체 소스를 이야기 하기 전에 중요 로직을 확인해본다.

카카오톡 채팅 데이터는 line by line으로 한 메시지에 여러 문장을 작성할 경우 짤리기 때문에 같은 메시지라고 인식하고 이 메시지를 붙여주는 것이 중요하다. 먼저 1편에서 확인한 내용은 아래와 같다.

fig1.txt 파일 읽은 후 리스트로 변환한 결과 

 

이제 아래처럼 바꿔 줘야 한다. 물론 아래는 각 채팅날짜별로 돌기 때문에 for loop를 이용하여 모든 날짜에 적용할 것이다. 그리고 아래에서 작성하진 않았지만 각 리스트로 구성된 메시지를 문자열로 변환 작업도 필요하다. 

fig2. 특정 날짜에 발생한 채팅 메시지 리스트를 문자열로 변환한 결과

fig2 결과를 위해 사용한 함수는 아래와 같다. 채팅 날짜에 해당하는 전체 메시지를 작성자+작성시간에 맞춰 문자열로 변환 후 다시 리스트 객체에 담아준다. 크게 신경써야 하는 부분은 1) 메시지가 시작된 부분인가? 2) 이전 편에서 확인했던 관리자 행동이나 참여자의 인입인가? 이다.

def split_talk_by_user(context):
    whole_txt, merge_txt = [], []
    for index, element in enumerate(context):
        start_str = re.match(r'\[.*?\]\s\[[오전|오후].*?\]', element)
        if start_str:
            whole_txt.append(' '.join(merge_txt))
            merge_txt = []
            merge_txt.append(element)
            if index == len(context)-1:
                whole_txt.append(' '.join(merge_txt))
        else:
            is_contain = False
            for action in ACTIONS:
                if action in element:
                    is_contain = True
            if is_contain:
                if merge_txt:
                    whole_txt.append(' '.join(merge_txt))
                whole_txt.append(element)
                merge_txt = []
            else:
                merge_txt.append(element)
    return [e for e in whole_txt if e.strip()]

 

그래서 전체 소스 코드는 아래를 참고하길 바란다.

github.com/hyunkyungboo/kakaotalk_chat_analysis/blob/master/01_read_txt_and_data_preprocessing.py

 

hyunkyungboo/kakaotalk_chat_analysis

카카오톡 채팅방 분석하기. Contribute to hyunkyungboo/kakaotalk_chat_analysis development by creating an account on GitHub.

github.com

 

다음 편에서는 데이터를 탐색해볼 것이다.

2020/11/14 - [Python/Community Manager를 위한 Python] - 3. 저장된 txt 파일은 어떻게 정제해야 할까? 1편

위 링크인 이전 글에 이어서.. 같은 날에 발생한 채팅들을 어떻게 정제할 것인지 살펴보고 순서에 맞춰서 정제를 해볼 것이다. 그리고 추가적으로 생각해야 하는 것은 관리자의 행동과 참여자의 인입이다. 자세히 말하자면 오픈 채팅창을 이용하다 보면 광고가 들어오고, 그 광고를 방장이 가리기도 하고.. 사람을 내보내기도 한다. 그리고 사람들은 오픈 채팅장에 입장도 하고 나가기도 한다. 아 그리고 때때로 특정 게시글을 공지로 올릴 수도 있게 된다. 따라서 이 부분도 미리 플래그 값을 추가한다면 추후 채팅방 트렌드를 확인할 때 유용하게 사용될 것이다.

위 설명에 맞춘 정제 포인트는 아래와 같다.

  • 기준일 추출
  • 작성자, 작성 시간, 메시지 추출
  • 공지글 확인하기
  • 관리자 행동, 참여자 인입 확인
  • 공지글, 삭제 메시지, 샵 검색, 이모티콘, 사진 확인

 

 

1. 기준일 추출

따로 정규식은 쓰지 않을 정도로 기준일 추출은 아주 쉽다. 기준일 양옆에 붙은 기호(-)만 제거해주면 된다. 

import datetime

def get_date(line):
    date_sep = '---------------'
    full_date = line.replace(date_sep, '').strip()
    talk_date, day_name = full_date[:-4], full_date[-3:]
    talk_date = datetime.datetime.strptime(talk_date, '%Y년 %m월 %d일')
    talk_date = datetime.datetime.strftime(talk_date, '%Y-%m-%d')
    return talk_date, day_name
       
line = "--------------- 2020년 11월 1일 일요일 ---------------"
get_date(line)
('2020-11-01', '일요일')

 

2. 작성자, 작성 시간, 메시지 추출

한 메시지에서 작성자, 작성 시간, 메시지 등 3개의 요소를 추출해야 하므로 다소 어려울 수 있다. 밑에 코드를 한 번 살펴보도록 한다. 

import datetime
import re

# 작성자, 작성 시간, 메시지 추출
def get_writer_and_wrote_at_and_msg(line):
    def convert_time(ko_wrote_at):
        import time

        if '오전' in ko_wrote_at:
            time_str = ko_wrote_at.replace('오전', 'am')
        elif '오후' in ko_wrote_at:
            time_str = ko_wrote_at.replace('오후', 'pm')

        time_str = datetime.datetime.strptime(time_str, '%p %I:%M')
        time_str = datetime.datetime.strftime(time_str, '%H:%M')
        return time_str
    
    split_line = re.split('\]|\[', line)
    split_line = [e for e in split_line if e.strip()]
    writer, wrote_at = split_line[0], convert_time(split_line[1])
    msg = line.split(split_line[1]+']')[1].strip()
    msg = msg if msg else None
    return writer, wrote_at, msg

line = "[포도] [오전 11:16] 안녕하세요!"
get_writer_and_wrote_at_and_msg(line)
('포도', '11:16', '안녕하세요!')

한 줄은 작성자와 작성 시간을 대괄호로 감싸고 있고 나머지는 메시지 부분으로 구성되므로 먼저, 대괄호 ([, ])를 기준으로 split()를 이용해 문자열을 분리해준다. 물론 이때 각 요소에 양쪽 공백도 없애준다. (19번째 행)

그러면 리스트에 [작성자, 작성 시간, 메시지]로 담기는데 이때 작성 시간은 오전 11:16의 형태로 오전/오후가 붙고 시간이 붙는다.  오전/오후 00 ~~ 12 표현을 00 ~ 24 표현으로 바꾸기 위해 convert_time() 함수를 만들어 주었다. 오전, 오후를 각각 am, pm으로 바꾸고 datetime 패키지를 활용하여 변경해주었다. 만약 오후였다면 어떻게 나오는지 아래 결과를 살펴보자.

line = "[BB] [오후 08:16] helloworld!"
get_writer_and_wrote_at_and_msg(line)
('BB', '20:16', 'helloworld!')

 아무래도 결과가 잘 나온것 같다. :D 

 

3. 관리자 행동, 참여자 인입 확인

이 부분은 생각해봐야 할 것이 꽤 있다. 여기서 정의하는 관리자의 행동은 1) 관리자가 메시지를 가리기 할 때 2) 참여자를 내보낼 때이고 참여자의 인입은 1) 참여자가 나갈 때, 2) 참여자가 들어올 때이다. 이때 카카오톡 메시지에는 다음처럼 나온다.

  • 채팅방 관리자가 메시지를 가렸습니다.
  • ~ 님을 내보냈습니다.
  • ~ 님이 나갔습니다.
  • ~ 님이 들어왔습니다.
import re

ACTIONS = ["채팅방 관리자가 메시지를 가렸습니다.", 
           "님을 내보냈습니다.",
           "님이 나갔습니다.",
           "님이 들어왔습니다."]

# 관리자, 참여자 행동 확인
def get_actions(line):
    global action_msg
    writer = '관리자'
    if ACTIONS[0] in line:
        action_msg = '게시글 공지'
    elif ACTIONS[1] in line:
        action_msg = '메시지 가리기'
    elif ACTIONS[2] in line:
        action_msg = '내보내기'
    elif ACTIONS[3] in line:
        writer, action_msg = line.replace(ACTIONS[3], ''), '나가기'
    elif ACTIONS[4] in line:
        writer, action_msg = line.replace(ACTIONS[4], ''), '들어오기'
    return writer, action_msg


msg_list = ["채팅방 관리자가 메시지를 가렸습니다.",
            "방장아니고반장님을 내보냈습니다.",
            "위너님이 들어왔습니다.",
            "위너님이 나갔습니다."]

for msg in msg_list:
    print(get_actions(msg))
('관리자', '메시지 가리기')
('관리자', '내보내기')
('위너', '들어오기')
('위너', '나가기')

 

4. 공지글 확인하기

이번에는 특정 값의 포함관계를 regex를 활용하여 확인해본다. 특정 게시글을 공지하게 되면 자동적으로 톡게시판 '공지': 라는 머리말이 붙고 이 부분이 있는지 re.match() 함수를 이용해 확인하였다,

def check_notice(text):
    start_str = re.match(r"톡게시판 '공지': ", text)
    is_notice_action = False
    if start_str:
        is_notice_action = True
    return is_notice_action
     
text = "톡게시판 '공지': 오늘도 평안하세요!"
check_notice(text)
True

 

5. 1~4 적용한 결과 도출하기

1,2는 작성시간이 존재하고 3은 작성 시간이 존재하지 않으므로 작성 시간 부분을 나눠 함수를 목적에 맞게 적용할 수 있다. 4번은 2번에서 추출된 메시지을 입력값으로 이용한다. 또한, start_regex에서 [ + (오전이나 오후)가 들어가는지 확인한 후 그다음에 각 목적에 맞춰 결과를 적용한다. 이때, is_talking_activity, is_notice_action을 추가하는데 3번에서 action_msg값이 None이 아니라면 is_talking_activity가 False이고 is_notice_action 또한, 4번 반환 값에 따라 결정될 것이다.  

def check_logic(content):
    content = content.strip()
    start_regex = r'\[.*?\]\s\[[오전|오후].*?\]'
    start_str = re.match(start_regex, content)
    is_talking_activity, is_notice_action = True, False
    wrote_at, action_msg = None, None
    if start_str:
        writer, wrote_at, msg = get_writer_and_wrote_at_and_msg(content)
        is_notice_action = check_notice(msg)
    else:
        writer, action_msg = get_actions(content)
        msg,is_talking_activity = content, False

    msg = re.sub(r'\s+', ' ', msg).strip()
    print(f"writer: {writer}, wrote_at: {wrote_at}, msg: {msg}")
    print(f"action_msg: {action_msg}, is_talking_activity: {is_talking_activity}, is_notice_action: {is_notice_action}")
    
content1 = '[hk] [오후 11:22] 심심해서 입사했다고 하던데..'
check_logic(content1)

content2 = 'hk님이 나갔습니다.'
check_logic(content2)
writer: hk, wrote_at: 23:22, msg: 심심해서 입사했다고 하던데..
action_msg: None, is_talking_activity: True, is_notice_action: False
writer: 고참참치님이 나갔습니다., wrote_at: None, msg: 고참참치님이 나갔습니다.
action_msg: 나가기, is_talking_activity: False, is_notice_action: False

 

6. 공지글, 삭제 메시지, 샵 검색, 이모티콘, 사진 확인

이 부분은 사후 처리로 추출된 메시지에서 확인을 한다. 다소 1~5 전개방식과는 다르지만 먼저 예제를 통해 내용을 살펴보자. 사실 1~5번 과정 전에 고려해야 할 부분이 있고, 이 결과를 데이터 프레임에서 람다식의 입력값으로 활용하여 각 목젝에 맞는 플래그 값을 줄 수 있다. 이 대, 데이터 프레임의 행으로 쌓을 것이다. 다음을 같이 확인해보자.

import pandas as pd

df = pd.DataFrame({'msg': ['이모티콘', '사진', '삭제된 메시지입니다.', '샵검색: #']})            
df['is_deleted_msg'] = df['msg'].apply(lambda x: True if x.strip() == '삭제된 메시지입니다.' else False)
df['is_emoji'] = df['msg'].apply(lambda x: True if x.strip() == '이모티콘' else False) 
df['is_picture'] = df['msg'].apply(lambda x: True if x.strip() == '사진' else False)
df['is_deleted_msg'] = df['msg'].apply(lambda x: True if x.strip() == '삭제된 메시지입니다.' else False)
df['is_search'] = df['msg'].apply(lambda x: True if x.startswith('샵검색: #') else False)
	msg	is_deleted_msg	is_emoji	is_picture	is_search
0	이모티콘	False	True	False	False
1	사진	False	False	True	False
2	삭제된 메시지입니다.	True	False	False	False
3	샵검색: #	False	False	False	True

람다식이 아직 어려울 수 있지만 하나만 기억하자. for문보다 간단하게 작성 가능함을!

 

다음 편에서 전체적으로 이용 가능한 소스를 소개하고 정제는 마무리 짓도록 하겠다. :D

먼저 저장된 txt 파일은 아래처럼 구성된다.

내보내기 된 카카오톡 오픈 채팅방.txt 내용 1
내보내기 된 카카오톡 오픈 채팅방.txt 내용 2

위 설명에 맞춘 정제 포인트는 아래와 같다.

  • 기본 헤더 제거
  • 일별 구분
  • 첫 입장 메시지 제거
  • 작성자/기준시간/작성 메시지 분리하기
  • 입장, 퇴장 확인하기

 

1. 정제 전 채팅 내용 데이터(.txt) 읽기

input_file_name = 'kakao_chat.txt'  # 텍스트 데이터 경로/이름 입력
with open(input_file_name, "r", encoding="utf-8-sig") as input_file:
    for line in input_file:
        line = line.strip()
        print(line)
데이터분석 QnA&네트워킹 님과 카카오톡 대화
저장한 날짜 : 2020-11-14 14:56:45

--------------- 2020년 11월 1일 일요일 ---------------
hk님이 들어왔습니다.운영정책을 위반한 메시지로 신고 접수 시 카카오톡 이용에 제한이 있을 수 있습니다.
[팬다 Jr.] [오전 10:27] 안녕하세요, 반갑습니다. 데분방입니다. 데분데분
포도님이 들어왔습니다.
[팬다 Jr.] [오전 11:16] 안녕하세요, 반갑습니다. 데분방입니다. 데분데분
[포도] [오전 11:16] 안녕하세요!
모모님이 들어왔습니다.

위 블록은 코드와 결과 일부 담고 있다. 그다음에 우선적으로 해야 하는 것은 기본 헤더 제거와 일자별 내용을 묶는 것이다.

2. 기본 헤더 제외한 일자별 내용 리스트로 담기

date_sep = '일 ---------------'
context_list, tmp_context_list = [], []
with open(input_file_name, "r", encoding="utf-8-sig") as input_file:
    index = 0
    for line in input_file:
        line = line.strip()
        start_index = 4
        if index == start_index:
            tmp_context_list.append(line)
        else:
            if line.endswith(date_sep):
                context_list.append(tmp_context_list)
                tmp_context_list = []
            tmp_context_list.append(line)
        index += 1
print("주어진 텍스트 파일 {}의 일자별 context는 {}건입니다.".format(input_file_name, len(context_list)))
주어진 텍스트 파일 kakao_chat.txt의 일자별 context는 11건입니다.

context_list의 내용은 아래와 같다. (일부)

[['데이터분석 QnA&네트워킹 님과 카카오톡 대화', '저장한 날짜 : 2020-11-14 14:56:45', ''],
 ['--------------- 2020년 11월 1일 일요일 ---------------',
  'hk님이 들어왔습니다.운영정책을 위반한 메시지로 신고 접수 시 카카오톡 이용에 제한이 있을 수 있습니다.',
  '[팬다 Jr.] [오전 10:27] 안녕하세요, 반갑습니다. 데분방입니다. 데분데분',
  '포도님이 들어왔습니다.',
  '[팬다 Jr.] [오전 11:16] 안녕하세요, 반갑습니다. 데분방입니다. 데분데분',
  '[포도] [오전 11:16] 안녕하세요!', (중략)

1번에 비해 다소 어려워진 것 같지만 때에 따라 어려워졌을 수도 있다... ㄴㅇㄱ

전반적인 코드 아래처럼 구성되어 있다.

  • 전체 내용을 담을 리스트, 일자별 내용을 담을 리스트 (각 context_list, tmp_context_list)
  • 일자별 구분을 위해 사용되는 date_sep과 .endwith()
  • 파일을 한 줄씩 읽는 for문, 그 한 줄에 대한 index

기본 헤더는 index 값이 0 ~ 3에 해당하므로 기준인을 index가 4로 시작된다. 그런데 기준일이 계속 늘어나기 때문에 기준일의 패턴인 [일 ---------------]을 활용하여 기준일 별 발생한 채팅 내용을 묶어줘야 한다. 그런데 첫 입장 메시지가 거슬리니 다음에서 첫 입장 메시지를 제거해본다. (단, 텍스트에 없는 경우가 있을 수 있다. 따라서 이 경우 다음을 넘어가도 된다.)

 3. 첫 입장 메시지 제거하기

if ~ continue를 활용하여 첫 입장 메시지, 즉 본문에 pass_msg에 해당하면 넘어가도록 한다.
(pass와는 달리 continue는 if ~ continue 이후 코드를 실행하지 않게 해 준다.)

 

date_sep = '일 ---------------'
pass_msg = '운영정책을 위반한 메시지로 신고 접수 시 카카오톡 이용에 제한이 있을 수 있습니다'
context_list, tmp_contenxt_list = [], []
with open(input_file_name, "r", encoding="utf-8-sig") as input_file:
    index = 0
    for line in input_file:
        line = line.strip()
        # 새로 추가한 부분 시작
        if pass_msg in line:
          continue 
        # 새로 추가한 부분 끝
        start_index = 4
        if index == start_index:
            tmp_contenxt_list.append(line)
        else:
            if line.endswith(date_sep):
                context_list.append(tmp_contenxt_list)
                tmp_contenxt_list = []
            tmp_contenxt_list.append(line)
        index += 1
print("주어진 텍스트 파일 {}의 일자별 context는 {}건입니다.".format(input_file_name, len(context_list)))
[['데이터분석 QnA&네트워킹 님과 카카오톡 대화', '저장한 날짜 : 2020-11-14 14:56:45', ''],
 ['--------------- 2020년 11월 1일 일요일 ---------------',
  '[팬다 Jr.] [오전 10:27] 안녕하세요, 반갑습니다. 데분방입니다. 데분데분',
  '포도님이 들어왔습니다.',
  '[팬다 Jr.] [오전 11:16] 안녕하세요, 반갑습니다. 데분방입니다. 데분데분',
  '[포도] [오전 11:16] 안녕하세요!', (중략)

 

어느 정도 구조화 가능한 데이터를 확보했으므로 이제 본격적으로 다음 편에서 pandas를 활용해 데이터 정제 과정을 공유하고자 한다.

* 채팅방 우측 상단 더보기 버튼 -> 대화내용 -> 대화 내보내기 클릭 후 파일 저장 

카카오톡 오픈 채팅방을 운영을 하고 있는 분들이라면 카카오톡 채팅 내용에 대해 궁금해 하지 않을까? 라는 생각이 들었고 마침 학교 과제와도 겹치는 부분이 있어 과제 결과 도출까지 과정을 공유하고자 한다. :D

큰 틀의 목차는 아래와 같다.

  1. 카카오톡 대화는 어떻게 이쁘게 만들까?
  2. 추가적으로 도출가능한 데이터는 무엇이지?
  3. 이 데이터로 무엇을 확인해볼까?

다만 글 작성시에는 조금 더 상세한 목차로 진행하려고 한다.

 

 

 

+ Recent posts