본문 바로가기

사이드 프로젝트

콴다 입문 선생님들의 가이드라인 제시 (with Python)

TermProject_2020105721_이시온

콴다 어플에서 상위권 선생님들의 주요과목 및 리뷰 분석

20살, 대학생이 되면서, 상위권 대학에 입학한 몇몇 학생들은 자신의 공부법과 노하우를 수험생들에게 알리고 싶어합니다. 그래서 이러한 학생들은 보통 '콴다'라는 문제풀이 어플에 '선생님'이라는 권한으로서 활동합니다.

콴다 어플에 대해 간단히 설명하자면, 콴다는 종합적인 과목에 대한 문제 풀이 어플로, 중,고등학생이 주요 소비자입니다. 콴다의 특징은 앱 내에 문제인식 기능이 있어, 카메라로 문제를 촬영하면 과거에 그것과 동일한 문제의 질문이 들어왔던 적이 있었는지 확인하며 있다면 그 문제에 해당하는 풀이를 그대로 보여주고, 없다면 '유사문제가 검색되지 않았어요.' 라고 뜨면서 그 문제를 콴다 선생님들에게 직접 질문할 수 있도록 합니다. 물론 콴다 선생님에게 물어보는 과정에서 콴다 선생님께 지불해야하는 비용이 발생하는데, 그 비용은 질문자의 학년에 따라 질문자가 일정한 범위 안에서 정할 수 있으며, 대체로 문제 당 500원에서 1500원 사이입니다. 질문자가 문제를 업로드하면 콴다 선생님들이 이를 확인할 수 있고, 선생님들 중 가장 먼저 그 질문을 클릭하고 '풀이 시작'버튼을 누르는 사람이 해당 질문에 답변할 기회를 얻습니다. 그리고 질문자가 문제를 업로드할 때, 불특정한 선생님께 문제를 질문할 수도 있지만, 그 질문자가 과거에 만족스러운 답변을 얻었던 특정한 선생님을 '찜'할 수 있고, 이 찜한 선생님들께 Pick 질문을 할 수도 있습니다. 그리고, 이 Pick 질문은 일반질문에 비해 더 높은 금액을 지불해야 합니다. 그래서 선생님들은 일반질문들에 답변할 때 정성스럽게 답변을 하여 자신을 '찜'하게 만들고, 자신을 '찜'한 질문자들에게 Pick 질문을 받음으로써 더 많은 금액을 받을 수 있도록 노력합니다. 또한, 콴다 선생님들은 과목 필터를 설정할 수 있는데, 이 기능을 통해 질문자들이 올린 일반 질문들 중 콴다 선생님이 원하는 과목만을 보여주어 그 과목의 질문들만을 확인할 수 있습니다.

저 또한 이 어플에서 선생님으로서 활동하고 있는데, 가끔 선생님들의 목록을 볼 때 질문자들의 높은 만족도와 찜목록이 매우 많은 선생님들을 볼 수 있었습니다. 이를 상위권 선생님이라고 부르겠습니다. 그래서 저는 몇몇 상위권 선생님들을 보며, 저 상위권 선생님들은 답변을 얼마나 많이 했는지, 주요 답변 과목은 무엇인지에 대해 궁금했고, 질문자들의 리뷰를 통해 상위권 선생님들의 답변 방식을 간접적으로 알아내어 저도 답변을 할 때 이를 적용시키고 싶었습니다.

따라서 이 프로젝트의 궁극적인 목표는 '콴다 입문 선생님들을 위한 가이드라인 제시' 입니다. 구체적으로 설명하자면 다음과 같습니다,

첫 번째 :

콴다 앱에서 다수의 상위권 선생님들에 대한 정보를 크롤링하여, 답변 수, 찜목록 질문자 수에 대한 상관관계를 분석할 것이고,

두 번째 :

상위권 선생님들의 주요 담당과목들을 정리해 어떤 과목에 대한 질문이 많이 들어오는지 분석할 것이며,

세 번째 :

질문자의 리뷰 중 부정적인 리뷰를 크롤링하고 이를 Word Cloud로 시각화하여, 질문자가 어떤 부분에서 상위권 선생님에게 불만을 가졌는지 알아냄으로써 입문 선생님들이 주의해야할 사항을 제시할 것입니다.

이 분석된 데이터를 통해 보다 더 수월히 상위권 선생님이 될 수 있을 것입니다.

데이터 획득 및 가공

저는 상위권 선생님을 S+,S등급인 선생님들로 정하였습니다. S+등급과 S등급의 공통점은 답변 수 200개 이상이어야 하는 것이고, 차이점으로는 평균 만족도와 신고율 입니다. 이 두 가지 요소가 어떠한 기준치를 넘어선다면 S+선생님으로 분류됩니다.

데이터 획득 계획은 이러합니다. 먼저 콴다 어플에 접속한 후, 콴다의 등급 시스템에 의거해 등급이 S+, S등급인 선생님들에 대한 데이터를 크롤링 합니다. 여기서 데이터라 함은, 답변 수와 주요 과목, 찜 목록 질문자 수를 말합니다. 크롤링한 데이터 중 필요한 데이터만을 가공해 상위권 선생님들 각각의 답변 수, 주요 과목, 찜 목록을 list로 나타냅니다. 그리고 이 선생님들 각각의 리뷰 또한 크롤링하여 필요한 형태로 가공합니다.

구체적인 데이터 획득, 가공 과정은 이렇습니다. 콴다는 반드시 어플로만 사용 가능하고, 웹으로는 사용이 불가능하였기에, 앱 내의 데이터를 크롤링할 수 있는 Fiddler 프로그램의 이용방법에 대해 간단히 습득한 후, 노트북과 휴대폰을 공유기를 매개로 연결하고 Fiddler 프로그램을 통해 이를 다루며, 프로그램이 나타내주는 상위권 선생님들의 여러 데이터를 확인하였습니다.

그리고 Fiddler 내의 모든 데이터를 JSON 형태로 이용하고자 하였습니다. 필요한 데이터는 JSON 형태로 저장되어있었습니다.

위와 같은 JSON 형태의 데이터들은 json 모듈의 loads 매서드를 통해 python 의 dictionary 형태로 저장이 가능했습니다. 밑의 코드를 통해 먼저 선생님들의 고유 id, 이름 정보를 파이썬으로 불러와 필요한 데이터들만을 list형태로 저장해 놓고자 하였습니다.

In [1]:
import requests
import json
import csv
headers = {
'Host': 'qanda.co.kr',
'x-app-id':'Mathpresso.QandaStudent',
'Authorization': 'Token 9b023936248a20b0cb59604f6d14462d7e6a9fba',
'Accept':'*/*',
'X-Service-Locale': 'ko_KR',
'Accept-Encoding': 'gzip;q=1.0, compress;q=0.5',
'Accept-Language':'ko',
'X-IOS-Version': '29003',
'X-AP-MAC':'',
'Content-Type':'application/json',
'X-IOS-Device-ID': '6b6708913e7b4e069eea4e58786769e3',
'X-Jarvis-Config': 'prod',
'User-Agent': 'QandaStudent/2.90.3 (Mathpresso.QandaStudent; build:100; iOS 13.4.1) Alamofire/4.9.1',
'Connection': 'keep-alive'
} #Fiddler를 이용해 앱의 데이터를 크롤링하기 위해, Fiddler에서 지정해주는 headers를 반드시 사용해야 함.

url1 = 'https://qanda.co.kr/api/v3/feed/teacher/my_teacher/?ordering=recommend_score&page=' 

'''콴다 내 선생님들을 나타내주는 페이지의 기본값으로, page= 뒤에 양의 정수값을 대입해 
해당 페이지에 위치하는 선생님들의 데이터를 볼 수 있음. 페이지 하나 당 10명의 선생님들이 위치.'''
 
Splus_users_id_lists=[] # S+ 선생님들의 고유 id값을 저장하는 리스트
Splus_users_name_lists=[] # S+ 선생님들의 닉네임을 저장하는 리스트(프로그램이 잘 작동하는지 확인할 때 쓰기 위함)
S_users_id_lists=[] # S 선생님들의 고유 id값을 저장하는 리스트
S_users_name_lists=[] # S 선생님들의 닉네임을 저장하는 리스트

for i in range(1,31): # 직접 확인해 본 결과, 13페이지 이후로는 S,S+등급의 선생님들이 거의 보이지 않아 넉넉히 30페이지까지 설정.
    url2 = str(i) 
    url = url1 + url2
    res = requests.get(url, headers=headers) # 해당 페이지를 requests하여 get하고, res변수에 저장
    data = json.loads(res.text) # JSON 형식의 데이터를 python의 dictionary 형태로 data 변수에 저장
    for k in range(10):
        user_rank = data['results'][k]['rank'] # 해당 유저의 랭크
        user_id=data['results'][k]['user'] # 해당 유저의 고유 id
        user_name = data['results'][k]['nickname'] # 해당 유저의 닉네임
        if user_rank == 'S+':
            Splus_users_id_lists.append(str(user_id))
            Splus_users_name_lists.append(user_name) # S+등급 선생님들의 id, 이름을 각각 리스트에 저장
        elif user_rank == 'S':
            S_users_id_lists.append(str(user_id))
            S_users_name_lists.append(user_name)  # S등급 선생님들의 id, 이름을 각각 리스트에 저장


print(Splus_users_id_lists)
print(Splus_users_name_lists)
print(S_users_id_lists)
print(S_users_name_lists)

# 정상적으로 저장되었는지 확인
['1186408', '10536560', '1007506', '607273', '585550', '5426817', '10053228', '3699831', '690329', '1237', '1498568', '87061', '4158411', '56592', '1032626', '10869119', '4100007', '4285153', '10643899', '7960663', '3450328', '627364', '6211990', '491824', '10338630', '77059', '4014829', '487773', '10581910', '10670925', '2389417', '10883986', '2855750', '10833352', '11157456', '11196678', '7143740']
['안녕', '우냉', '1일다이어터', '도토리수학', '대구동그리수학1대1', 'rfiamnf', 'stella__', '주희', 'TRIVIAL', '파워업', '이수아', '꼬부기부기', '퓅', '꾹꾹', 'coooool', '성대은행', '모도린', '울산공대쌤', '아로아로00', '창조건도시', '김정은', '연세대', '콴다초쌤', '홍시', '댕댕이', '푸트남', '세발낙지', '8235', '딸기우융', '밤돌누나', '윤혁준', 'tjrwns13', '둘리', '알쌤', 'minion', '니시', 'lynK']
['10725953', '3789921', '8525288', '10551842', '5788292', '5331831', '1358417', '10595506', '109855', '137139', '695811', '1017222', '4225288', '4868396', '3351129', '1653650', '3445196', '842119', '109247', '1101314', '4237926', '3061622', '6790101', '977', '12092074', '173531']
['과학고수학', 'YR', '정석', '호두마루', '블랙보리', '채택꼭해주세용', '장전동살쾡이', 'vivid', '너또왔니', 'forest', '만점자', '송', '봉봉이', 'Hgwan', '수백이', '뛰뛰빵', '허걱쓰', '유니쌤', 'SNU_sylvia', '스누피', 'leeu', '양념치퀸', '문제푸는 패티', '디제', 'Mm', '레몬워터']

그리고, 선생님들의 주요과목과 누적 답변수, 찜 목록자 에 대한 데이터를 불러오고자 하였습니다. 주요과목은 Fiddler의 json 형태의 데이터에 'specialty_subject' 라는 이름으로, 과목들의 고유 번호로 저장되어있었습니다. 그래서 그 고유번호를 과목이름으로 바꾸어 주는 함수 subject_name 을 만들어 리스트에 추가하고자 하였습니다. 나머지 누적 답변수, 찜 목록자에 대한 데이터는 json형태의 데이터에 'accumulated_answer_count', 'like_count' 라는 일므으로 그대로 저장되어있었기에, 밑의 코드를 통해 리스트 형태로 만들었습니다.

In [2]:
Splus_users_subject_lists = []
S_users_subject_lists = []
Splus_users_accumulated_count_lists = []
S_users_accumulated_count_lists = []
Splus_users_like_count_lists = []
S_users_like_count_lists = []

url_specific1 = 'https://qanda.co.kr/api/v3/statistics/user/' #위의 url1과 비슷한 맥락. 위 url을 통해 선생님들의 세부적인 정보를 알 수 있음.

def subject_name(num):
    if num == 1:
        subject = '수학'
        return subject
    
    elif num == 2:
        subject = '과학'
        return subject
    
    elif num == 3:
        subject = '사회'
        return subject
    
    elif num == 4:
        subject = '국어'
        return subject
    
    elif num == 5:
        subject = '영어'
        return subject
    
    else:
        return None
    


for i in range(len(Splus_users_id_lists)):
    url_specific2 = Splus_users_id_lists[i]+'/' 
    '''https://qanda.co.kr/api/v3/statistics/user/ 뒤에 선생님들의 고유 id가 붙음으로서 해당 선생님에 대한 구체적인 정보를 
    얻을 수 있음'''  
    url = url_specific1 + url_specific2
    specific_res = requests.get(url, headers=headers)
    specific_data = json.loads(specific_res.text)
    subject_id = specific_data['teacher_profile']['specialty_subject']['subject_id']
    accumulated_answer_count = specific_data['teacher_profile']['accumulated_answer_count']
    like_count = specific_data['teacher_profile']['like_count']
    
    results = subject_name(subject_id)
    Splus_users_subject_lists.append(results)
    Splus_users_accumulated_count_lists.append(accumulated_answer_count)
    Splus_users_like_count_lists.append(like_count)
    
    
for i in range(len(S_users_id_lists)):
    url_specific2 = S_users_id_lists[i]+'/' 
    url = url_specific1 + url_specific2
    specific_res = requests.get(url, headers=headers)
    specific_data = json.loads(specific_res.text)
    subject_id = specific_data['teacher_profile']['specialty_subject']['subject_id']
    accumulated_answer_count = specific_data['teacher_profile']['accumulated_answer_count']
    like_count = specific_data['teacher_profile']['like_count']
    
    results = subject_name(subject_id)
    S_users_subject_lists.append(results)
    S_users_accumulated_count_lists.append(accumulated_answer_count)
    S_users_like_count_lists.append(like_count)
    

    
print(Splus_users_subject_lists)
print(S_users_subject_lists)
print(Splus_users_accumulated_count_lists)
print(S_users_accumulated_count_lists)
print(Splus_users_like_count_lists)
print(S_users_like_count_lists)

# 정상적으로 저장되었는지 확인
['수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '영어', '수학', '과학', '수학', '수학', '영어', '영어', '영어', '수학', '영어', '수학', '수학', '수학', '수학', '수학', '수학', '영어', '수학', '수학', '수학', '수학', '수학', '영어', '수학']
['수학', '영어', '영어', '영어', '수학', '수학', '수학', '영어', '수학', '영어', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '영어', '영어', '수학', '수학', '수학', '수학', '수학']
[31214, 1550, 38060, 5307, 2953, 1033, 2565, 6204, 1634, 8334, 20288, 5815, 6511, 22827, 5323, 910, 9025, 4921, 1410, 1231, 2419, 9451, 1803, 14985, 508, 8942, 776, 5888, 532, 1003, 2596, 651, 6190, 972, 469, 439, 4353]
[4771, 987, 1562, 1413, 3508, 3665, 18470, 1815, 17167, 1203, 23469, 38644, 7165, 2909, 4144, 2030, 8716, 1668, 4432, 3954, 2747, 2547, 5250, 3654, 361, 4138]
[7355, 315, 18948, 786, 1143, 514, 735, 1942, 177, 2323, 3388, 5277, 1315, 7297, 799, 82, 1404, 1317, 327, 892, 511, 1841, 1339, 2212, 84, 623, 253, 1773, 79, 333, 314, 87, 1485, 154, 86, 89, 1668]
[542, 92, 143, 168, 1747, 604, 3761, 198, 2004, 116, 5975, 8156, 3272, 621, 731, 494, 2076, 194, 1543, 392, 416, 629, 1574, 1399, 44, 983]

이제 위에서 크롤링한 S+,S 선생님들의 데이터들을 '분석하고자 하는 대상'으로 하나로 묶기 위해 final_users로 이름을 정해 final_users에 S+,S 선생님들의 모든 데이터, 즉 고유 id, 닉네임, 주요과목, 누적답변수, 찜목록자 수를 통합시키고자 하였습니다.

In [3]:
final_users_id_lists=[]
final_users_id_lists.extend(Splus_users_id_lists)
final_users_id_lists.extend(S_users_id_lists)
print(final_users_id_lists)

final_users_name_lists=[]
final_users_name_lists.extend(Splus_users_name_lists)
final_users_name_lists.extend(S_users_name_lists)
print(final_users_name_lists)

final_users_subject_lists=[]
final_users_subject_lists.extend(Splus_users_subject_lists)
final_users_subject_lists.extend(S_users_subject_lists)
print(final_users_subject_lists)

final_users_accumulated_count_lists=[]
final_users_accumulated_count_lists.extend(Splus_users_accumulated_count_lists)
final_users_accumulated_count_lists.extend(S_users_accumulated_count_lists)
print(final_users_accumulated_count_lists)

final_users_like_count_lists = []
final_users_like_count_lists.extend(Splus_users_like_count_lists)
final_users_like_count_lists.extend(S_users_like_count_lists)
print(final_users_like_count_lists)

# 정상적으로 저장되었는지 확인
['1186408', '10536560', '1007506', '607273', '585550', '5426817', '10053228', '3699831', '690329', '1237', '1498568', '87061', '4158411', '56592', '1032626', '10869119', '4100007', '4285153', '10643899', '7960663', '3450328', '627364', '6211990', '491824', '10338630', '77059', '4014829', '487773', '10581910', '10670925', '2389417', '10883986', '2855750', '10833352', '11157456', '11196678', '7143740', '10725953', '3789921', '8525288', '10551842', '5788292', '5331831', '1358417', '10595506', '109855', '137139', '695811', '1017222', '4225288', '4868396', '3351129', '1653650', '3445196', '842119', '109247', '1101314', '4237926', '3061622', '6790101', '977', '12092074', '173531']
['안녕', '우냉', '1일다이어터', '도토리수학', '대구동그리수학1대1', 'rfiamnf', 'stella__', '주희', 'TRIVIAL', '파워업', '이수아', '꼬부기부기', '퓅', '꾹꾹', 'coooool', '성대은행', '모도린', '울산공대쌤', '아로아로00', '창조건도시', '김정은', '연세대', '콴다초쌤', '홍시', '댕댕이', '푸트남', '세발낙지', '8235', '딸기우융', '밤돌누나', '윤혁준', 'tjrwns13', '둘리', '알쌤', 'minion', '니시', 'lynK', '과학고수학', 'YR', '정석', '호두마루', '블랙보리', '채택꼭해주세용', '장전동살쾡이', 'vivid', '너또왔니', 'forest', '만점자', '송', '봉봉이', 'Hgwan', '수백이', '뛰뛰빵', '허걱쓰', '유니쌤', 'SNU_sylvia', '스누피', 'leeu', '양념치퀸', '문제푸는 패티', '디제', 'Mm', '레몬워터']
['수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '영어', '수학', '과학', '수학', '수학', '영어', '영어', '영어', '수학', '영어', '수학', '수학', '수학', '수학', '수학', '수학', '영어', '수학', '수학', '수학', '수학', '수학', '영어', '수학', '수학', '영어', '영어', '영어', '수학', '수학', '수학', '영어', '수학', '영어', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '영어', '영어', '수학', '수학', '수학', '수학', '수학']
[31214, 1550, 38060, 5307, 2953, 1033, 2565, 6204, 1634, 8334, 20288, 5815, 6511, 22827, 5323, 910, 9025, 4921, 1410, 1231, 2419, 9451, 1803, 14985, 508, 8942, 776, 5888, 532, 1003, 2596, 651, 6190, 972, 469, 439, 4353, 4771, 987, 1562, 1413, 3508, 3665, 18470, 1815, 17167, 1203, 23469, 38644, 7165, 2909, 4144, 2030, 8716, 1668, 4432, 3954, 2747, 2547, 5250, 3654, 361, 4138]
[7355, 315, 18948, 786, 1143, 514, 735, 1942, 177, 2323, 3388, 5277, 1315, 7297, 799, 82, 1404, 1317, 327, 892, 511, 1841, 1339, 2212, 84, 623, 253, 1773, 79, 333, 314, 87, 1485, 154, 86, 89, 1668, 542, 92, 143, 168, 1747, 604, 3761, 198, 2004, 116, 5975, 8156, 3272, 621, 731, 494, 2076, 194, 1543, 392, 416, 629, 1574, 1399, 44, 983]

이렇게 필요한 데이터들을 얻었지만, 주피터노트북을 종료시킨 후 다시 실행시키면 위 리스트들이 저장이 되지 않고 맨위의 코드부터 차례대로 실행시켜야 한다는 불편함이 있었습니다. 이는 시간도 오래걸리기에, 위 리스트들을 csv파일형태로 저장하여 쓰고 싶을 때 마다 이 csv파일을 불러 쓰고자 하였습니다.

In [4]:
with open('final_teacher_data.csv','w',newline='',encoding='utf-8') as datafile:
    csv_writer = csv.writer(datafile)
    for i in range(len(final_users_id_lists)):
        csv_writer.writerow([final_users_id_lists[i],final_users_name_lists[i],final_users_subject_lists[i],final_users_accumulated_count_lists[i],final_users_like_count_lists[i]])

이제, 상위권 선생님들의 리뷰를 저장해야합니다. 리뷰를 불러들일 때 사용되는 기본 url은 ''https://qanda.co.kr/api/v3/user/teacher/' 이고, 이 뒤에 각 선생님의 고유 id를 붙여 선생님들에 대한 리뷰를 볼 수 있었습니다. 따라서 위에 csv파일로 저장해놓은 final_teacher_data.csv 파일을 불러와 각 선생님들의 id리스트만을 불러왔습니다.

In [5]:
fileMatrix = []
final_teacher_id_list=[]
with open('final_teacher_data.csv','r',encoding='utf-8') as file:
    for lineContent in file:
        fileMatrix.append(lineContent.strip('\n').split(','))

for j in range(len(fileMatrix)):
    final_teacher_id_list.append(fileMatrix[j][0])
print(final_teacher_id_list)
['1186408', '10536560', '1007506', '607273', '585550', '5426817', '10053228', '3699831', '690329', '1237', '1498568', '87061', '4158411', '56592', '1032626', '10869119', '4100007', '4285153', '10643899', '7960663', '3450328', '627364', '6211990', '491824', '10338630', '77059', '4014829', '487773', '10581910', '10670925', '2389417', '10883986', '2855750', '10833352', '11157456', '11196678', '7143740', '10725953', '3789921', '8525288', '10551842', '5788292', '5331831', '1358417', '10595506', '109855', '137139', '695811', '1017222', '4225288', '4868396', '3351129', '1653650', '3445196', '842119', '109247', '1101314', '4237926', '3061622', '6790101', '977', '12092074', '173531']

이제, 리뷰들을 불러올 수 있습니다. headers에 대한 정보는 맨 위에서 저장해두었던 headers와 약간 차이가 있어, 다시 headers에 대한 데이터를 저장하였습니다.

In [6]:
headers = {
'Host': 'qanda.co.kr',
'x-app-id':'Mathpresso.QandaStudent',
'Authorization': 'Token 9b023936248a20b0cb59604f6d14462d7e6a9fba',
'Accept':'*/*',
'X-Service-Locale': 'ko_KR',
'Accept-Encoding': 'gzip;q=1.0, compress;q=0.5',
'Accept-Language':'ko',
'X-IOS-Version': '29100',
'X-AP-MAC':'',
'Content-Type':'application/json',
'X-IOS-Device-ID': '6b6708913e7b4e069eea4e58786769e3',
'X-Jarvis-Config': 'prod',
'User-Agent': 'QandaStudent/2.91.0 (Mathpresso.QandaStudent; build:100; iOS 13.4.1) Alamofire/4.9.1',
'Connection': 'keep-alive'
}

review_list=[]
review_base_url_1='https://qanda.co.kr/api/v3/user/teacher/'
review_base_url_3='/review_history/?cursor='

그리고 위와같이 모든 리뷰들을 저장할 review_list를 만들었습니다. 그리고 위의 review_base_url_1과 review_base_url_3 사이에 선생님 고유 id가 들어가면 url이 완성됩니다. 하지만, 그렇게 완성된 url을 통해 각 선생님의 리뷰를 볼 수는 있었으나, 한 선생님 당 여러 페이지가 존재하는 리뷰들 중 1페이지의 리뷰만을 볼 수 있었습니다. 다음 페이지의 리뷰를 보기 위해서는 완성된 url다음, 즉 cursor= 다음 값에 base64 형태의 값이 붙어야 했습니다.

그래서 Fiddler에 직접 들어가 다음 페이지에 대한 base64 데이터를 디코딩한 결과, 그 전페이지의 created_at, 즉 전페이지의 마지막 리뷰가 쓰여진 날짜를 기준으로 base64 형태의 데이터가 만들어짐을 알았습니다. 그리고 전페이지의 마지막 리뷰에 있는 created_at에 대한 데이터를 알맞은 형태로 바꾼 후, base64로 인코딩해본 결과, 그 결과를 cursor= 값에 대입시키면 다음 페이지의 리뷰가 출력됨을 확인할 수 있었습니다. 구체적인 과정은 다음과 같습니다.

1. cursor= 뒤의 값이 없는 페이지의 마지막 리뷰의 created_at 복사

(예시) created_at=2020-06-11T20:40:54.332700+07:00

2. URL Encode

(예시) created_at%3D2020-06-11T20%3A40%3A54.332700%2B07%3A00

3. 'created_at%3D'->'p=' ,'T'->'+'

(예시)p=2020-06-11+20%3A40%3A54.332700%2B07%3A00

4. Base64 Encode

(예시)cD0yMDIwLTA2LTExKzIwJTNBNDAlM0E1NC4zMzI3MDAlMkIwNyUzQTAw

이 과정을 base64, urllib.parse 모듈을 이용해 다음과 같이 함수로 구현하였습니다.

In [7]:
import base64
import urllib.parse

def cursor_encoding(par):
    a = urllib.parse.quote(par)
    a = a.replace('created_at%3D','p=')
    a = a.replace('T','+')
    a = base64.b64encode(a.encode('utf-8'))
    
    # 밑은 a를 사용할 수 있는 문자열 형태로 바꾸는 과정
    a = str(a) 
    a = a.replace('b','')
    a = a.replace('\'','',2) 
    return a

위 함수는 1페이지에서 2페이지로 넘어갈때만 사용하는 것이 아니라, n페이지에서 n+1페이지로 넘어갈 때도 사용할 수 있는 함수입니다. 그래서 저는 위의 headers 와 함수를 이용해, final_teacher들의 리뷰들 중, 4페이지까지 리뷰의 평가점수가 5점 만점에 2점이하로 나온 리뷰들을 모두 review_list에 추가시키고자 하였습니다. 이제 그 review_list가 불만족스러운 리뷰의 집합이 되는 것입니다.

In [8]:
for i in range(len(final_teacher_id_list)):


    review_base_url_2 = final_teacher_id_list[i]
    n=0
    while True:
        n=n+1
        if n==1:
            review_url = review_base_url_1+review_base_url_2+review_base_url_3
            res = requests.get(review_url, headers=headers) # 해당 페이지를 requests하여 get하고, res변수에 저장
            data = json.loads(res.text)

            for i in range(len(data)):
                if float(data[i]['accepted_answer']['rating']) <= 2:
                    review = data[i]['accepted_answer']['review_message']
                    review_list.append(review)
                else:
                    continue
        
        elif 2<=n<=4:
            cursor_before = 'created_at='+data[len(data)-1]['created_at']
            cursor_after = cursor_encoding(cursor_before)
            review_url = review_base_url_1+review_base_url_2+review_base_url_3+cursor_after
            res = requests.get(review_url, headers=headers) # 해당 페이지를 requests하여 get하고, res변수에 저장
            data = json.loads(res.text)

            for i in range(len(data)):

                if float(data[i]['accepted_answer']['rating']) <= 2:
                    review = data[i]['accepted_answer']['review_message']
                    review_list.append(review)
                else:
                     continue

        else:
            break

            
print(review_list)
['답안지는 있었고, 단지 풀이을 알고 싶었는데 인상도 별로고 \n이해가 안됌니다.번창하세요..', '벼로에요', '이해안감', 'ê·¼ë�° ㅜㅜ ì\xa0œê°€ 질문한것ì—�대해 ì›�리는 안알ë\xa0¤ì£¼ì‹œê³\xa0 다루지 않는 다ê³\xa0만 하셔서 좀 아쉬웡요', '정말 식도 대충써주시고 답변도 안하내요 정말최악입니다', '계산 ìƒ�ë�µ 너무ë§�ì�´ë�˜ì„œ 보기 ì–´ë\xa0¤ì›€', '다 틀렸어요', '다풀었는데 알려주노', 'í’€ì�´ í•´ 달ë�¬ëŠ”ë�° 답안지를 그대로 빼껴 오시면...', '공부를 질문했ë�”니 답ì�€ ì•Œë\xa0¤ì£¼ì§€ë§Œ  ë§�ì�„기분나ì�˜ê²Œ 하셔서 기분ì�´ 나빴ê³\xa0설명ì�´ 없네요', 'ì\xa0œêº¼ëŠ” ì–¸ì\xa0œ 마무리가 ë�˜ì£\xa0?..', '들ë\xa0¸ì–´ìš”...', '답ì�¥ì�„ 빨리보내주시지 ì•Šì•„ì„œ 답답했ê³\xa0 ì�´í•´ê°€ 안ë�˜ì„œ ì�¬ì§ˆë¬¸ 했는ë�°ë�„ 불구하ê³\xa0 답ì�¥ ì—†ì�Œ', '답ì�´ 계ì†� 틀리는ë�°ìš”?', 'ㄱ개 ㅂㄹ', '틀리심..^^', 'ì�´í•´ ì\xa0„혀 안ë�¼ìš”', '모르는거 물어보라고 했는데 답을 안해 주시네..^^', 'ì\xa0•ë§� ê°�사합니다', '개불친ì\xa0ˆí•¨', '별루ì—�ìš”.í’€ì�´ë¥¼ 안가르ì³�주세요. 모르ê²\xa0는ë�°ã…\xa0ã…\xa0\n넘 ì†�ìƒ�하네요. ì½”ì�¸ë�„ ë‚\xa0리ê³\xa0 ã…\xa0ã…\xa0 \nì\xa0•ë§�루', '답이 정확하지않은데 답변도 없고~~', '겁나 부정확함', '답 틀림.', '별로에요.답지설명을거의다써놓다시피하셨네요ㅜ', '이해가 잘 안갔어요...', '답ì�´ í‹€ë\xa0¸ì–´ìš”..', '답 틀리셨어요 ~^;;;;;;', '완전 우!!!(아니 불친절)', '답변보고 전혀 모르겠어요', '좀 어렵게 풀이를 쓰셧다 근데 내가 못해서 못알아보는걸수도', 'ì�´í•´ ë��ì–´ìš” ê°�사합니다!.!', '걍', '설명이 부족하고 풀이가 명확하지 않아요....', '설명ì�„ 해줘야지 답만 ì•Œë\xa0¤ì£¼ì‹œë©´ ì\xa0œê°€ ì°�ì–´ë�„ ë§�ê²\xa0네여;;;', 'ì�Œ.....', '설명 별로ì—�ìš”', '답변ì�„ ì�´ìƒ�하게함', '답지를 찾아 올리신 것 같은데 그건 그렇다쳐도 질문에 답을 안해주시는건 너무합니다..', 'ì�´ìƒ�í•´', '감사합니당', '설명ì�´ 너무 간단해요;', '왜 그런 결과가 나왔는지 설명해주셨으면 좋ê²\xa0습니다세부 설명ë�„ 해주셨으면 좋ê²\xa0ì–´ìš”', '답변이 질문과 다름', '??', '답ì�´.ì•„ë‹Œë�°ìš”', '.', '답이 틀림', '답장이 느려요', '대충해주셨다', '별로예요ㅠㅠ', '싸가지 없어요', '별로였다', '너무 답ì�´ ëŠ�림', '답ì�´ ì�´ìƒ�함', '답ì�¥ì�„ 안해줌', 'ì�´í•´ê°€ 안ë�¼ì„œ 물어보았는ë�° 답변ì�´ 없으세요...ã…œ', '.', '풀이를 좀 식만 써주시고 재질문하면 답을 안해주신다.', '이해가 안되서 다시 질문해서 알려주시긴했지만 이해가 안되네요 그냥 포기할께요 ..^^', '풀의는 잘하는데 좀... 예의가 없어여', '답이 틀렸어요..', '답지 베끼네ㅋㅋㅋ 돈아까워요', '답변에 대해 궁금한게 있는데 다시 답변을 안 해주셔요..', '다시 물어보면 쳐 말안해주는 새끼', '풀이를 안 알려줌 답젼이 느림', '답만 알려주고 풀이는 안알려주시네요....', '되게 답변 빠르신데 풀이는 안 주시네요.', '3.1인데 3.14로 잘못계산하셨어요\nㅠㅠ', '틀렸습니다ㅠㅠ', '답ì�„ 틀리셨네요', '답ì�´ 틀리셨너요...\nì–´ì©Œì£\xa0?', '대답ë�„ 조금 ëŠ�리시ê³\xa0 그림ì—� 글쓰시는게 조지네요;; 다ì�Œë¶€í„´ 종ì�´ì—� 좀해주셨으면 합니다', '화면ì—� 대ê³\xa0 글씨 ì�¨ì£¼ì…”ì„œ 보는게 좀 불í�¸í–ˆë„¤ìš”;;\nì•�으ë¡\xa0 종ì�´ì—�다가 ì�¨ì£¼ì‹œë©´ 좋ê²\xa0습니다.', '답 절대 안해주시네요', '와', '대충 ì•Œë\xa0¤ì£¼ì‹œê³\xa0 질문했는ë�° 답ë�„ 안하네요 ã…‹', '중간ì—� 안ì\xa0�어줬네요ㅜㅜ', '별로', '답ì�´ 명쾌하지ë�„ ì•Šê³\xa0 í��지부지 하네요 ..', '처ì�Œ 질문ì—� 대한 답ì�„ 해주시지 않으셨습니다. 지문ì�˜ ë°˜ì�¸ë�°ë�„ 30분ë�™ì•ˆ 답ì�´ 없으셨습니다', '너무 말투가 짜증남.', 'ㅂㄹ', '완전 안조음', '풀이가 자세하지 않음.별로', '별로네?', '조금ë�” 세세한 í’€ì�´ë¥¼ 해주시면 ë�”ìš± 좋ì�„것 같습니다', '정확히 알려 주시지 않았습니다', '그림으로해주셔서 좋아요', '지금까지 질문한 답들이 다 틀림', 'ê°�사합니다 ?', '답ì�„ 너무 안하시ê³\xa0...ì´ˆ5문ì\xa0œë¥¼ 중등 수학으로 풀어주시네요...', '추가질문 ì��체를 안받는건지 못보시는건지 답변ì�„ 안해주시는 ì„\xa0ìƒ�님들ì�´ 콴다ì—� 너무 ë§�네요.', '굿', '정확하게 답을 말씀안해주시네요^^', '너무 느려요', 'ㅗ', '풀이가 자세하지 않고요.... 질문에 대해 답을 안해주시네요... 차단하겠습니다..']

그리고 위 코드가 실행되기까지의 시간도 굉장히 오래걸렸기에, 역시 csv형태의 파일로 저장하였습니다.

In [9]:
with open('final_review_below_2.csv','w',newline='',encoding='utf-8') as datafile:
    csv_writer = csv.writer(datafile)
    for i in range(len(review_list)):
        csv_writer.writerow([review_list[i]]) 

이제, 필요한 데이터들을 모두 얻고 가공까지 끝냈습니다. 이제 위의 데이터를 이용해 본격적으로 제가 구현하고자하는 소프트웨어를 만들어보도록 하겠습니다.

- 첫 번째

상위권 선생님들의 답변 수와 찜목록 질문자 수에 대한 상관관계를 분석해 어느 정도 질문을 해야 찜목록 질문자 수가 얼만큼 생기는지를 추측합니다. 처음에는 linear regression을 활용하려 했으나, 파이썬만의 도구보다는 이론적으로도 이 문제에 접근하고 싶었기 때문에, 저는 최소제곱법이라는 이론을 이용하여 그 상관관계를 분석하고자 했습니다. 최소제곱법에 대해 간단히 소개하자면, 여러 좌표에 대한 점이 찍혀있을 때, 이 점들의 경향을 가장 잘 나타낸, 즉 오차가 최소인 직선을 구하는 방법 중 하나입니다. 즉 (x1,y1),(x2,y2),....(xn,yn)인 점들이 있고, 이 점들의 경항을 가장 잘 나타낸, 즉 최소제곱법에 의해 정의된 직선을 y=ax+b라 할 때, a,b는 다음이 성립합니다. 밑식에서 bar x, bar y는 x값, y값의 평균입니다.

1

2

위 식을 파이썬으로 구현하기 전, 먼저 final_teacher의 accumulated_answer과 like_count에 대한 리스트를 각각 불러옵니다.

In [10]:
fileMatrix = []
answer_count_list=[]
like_count_list=[]
with open('final_teacher_data.csv','r',encoding='utf-8') as file:
    for lineContent in file:
        fileMatrix.append(lineContent.strip('\n').split(','))

for j in range(len(fileMatrix)):
    answer_count_list.append(int(fileMatrix[j][3]))


for j in range(len(fileMatrix)):
    like_count_list.append(int(fileMatrix[j][4]))


print(answer_count_list)
print(like_count_list)
[31214, 1550, 38060, 5307, 2953, 1033, 2565, 6204, 1634, 8334, 20288, 5815, 6511, 22827, 5323, 910, 9025, 4921, 1410, 1231, 2419, 9451, 1803, 14985, 508, 8942, 776, 5888, 532, 1003, 2596, 651, 6190, 972, 469, 439, 4353, 4771, 987, 1562, 1413, 3508, 3665, 18470, 1815, 17167, 1203, 23469, 38644, 7165, 2909, 4144, 2030, 8716, 1668, 4432, 3954, 2747, 2547, 5250, 3654, 361, 4138]
[7355, 315, 18948, 786, 1143, 514, 735, 1942, 177, 2323, 3388, 5277, 1315, 7297, 799, 82, 1404, 1317, 327, 892, 511, 1841, 1339, 2212, 84, 623, 253, 1773, 79, 333, 314, 87, 1485, 154, 86, 89, 1668, 542, 92, 143, 168, 1747, 604, 3761, 198, 2004, 116, 5975, 8156, 3272, 621, 731, 494, 2076, 194, 1543, 392, 416, 629, 1574, 1399, 44, 983]

이제, x축을 누적답변수, y축을 찜목록자 수로 설정한 후 각각을 좌표평면 위의 점으로 나타냅니다. 그리고 최소제곱법을 구현해 최소제곱직선까지 그래프로 시각화 합니다. 이를 위해서 matplotlib.pyplot, numpy모듈을 import 합니다.

In [11]:
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline


answer_count_list_average = np.average(answer_count_list) # bar x
like_count_list_average = np.average(like_count_list) # bar y

def least_squares(x_list,y_list): # 최소제곱법을 구현할 함수 정의. 반환값은 [기울기, y절편] 인 list
    x_list_average = np.average(x_list)
    y_list_average = np.average(y_list)
    
    u=0
    d=0

    for i in range(len(x_list)):
        u = u + (x_list[i]-x_list_average)*(y_list[i]-y_list_average)
        d = d + (x_list[i]-x_list_average)**2

    inclination = u/d
    
    y_intercept = y_list_average - inclination*x_list_average
    
    results = [inclination,y_intercept]
    return results

results = least_squares(answer_count_list,like_count_list)
print(results)



x=np.array(answer_count_list)
y=np.array(like_count_list)

# 값들의 시각화
plt.figure()
plt.scatter(x,y)
y= results[0]*x +results[1]
plt.plot(x,y)
plt.show()
[0.29098304009577014, -190.6512101818421]

이렇게, 최소제곱직선까지도 구했습니다. 그리고 이제 답변 당 얻을 수 있는 코인의 기댓값을 구해야하는데, Fiddler에는 해당 선생님의 누적 코인갯수에 대한 데이터는 제공하지 않았습니다. 그래서, 저는 콴다 학생용 앱을 이용하여 S+선생님 중 한분을 찜한 후, 그분께 양해를 구하고 이때까지 환전한 금액과 현재 남아있는 코인의 갯수에 대해 여쭈어보았습니다. 그리고 fiddler를 통해 그분의 누적 답변수와 찜목록자수에 대한 데이터를 얻은 후, 위 그래프에서의 기울기 값, 즉 찜목록자수/누적답변수 을 이용해 비례식을 세워 한문제당 얻을 수 있는 코인의 기댓값을 구하였습니다. 구체적인 과정은 밑과 같습니다.

In [12]:
Incline = results[0]
# S+ 선생님 중 '모도린' 선생님의 데이터 : 누적답변수 = 7292, 찜목록 = 1107, 누적코인 = 25,700,000(근삿값)
# 위 선생님이 받은 답변 하나당 코인의 평균값은 약 3524코인. 

# 답변의 갯수에 대해 찜목록자수가 많다면 답변 하나당 코인의 평균값도 높아질 것이므로, 다음의 비례식을 이용하는것이 합리적이라고 판단

# ((모도린 선생님의 찜목록자수)/(모도린 선생님의 누적답변수)) : 3524 = 최고제곱직선의 기울기 : x (x가 받은 답변 하나 당 코인의 평균값)

# 이때 ((모도린 선생님의 찜목록자수)/(모도린 선생님의 누적답변수))은 이 선생님의 기울기값이라고 볼 수 있음.

sample_Incline = 1107/7292 # 모도린 선생님의 기울기값

expect_coin_per_answer = 3524*Incline*(1/sample_Incline)  # 비례식을 x, 즉 expect_coin_per_answer에 대해 정리

위 식을 통해 답변수에 대한 찜 목록자수를 평균적으로 가지고 있을 때, 답변 하나 당 얻을 수 있는 코인의 평균값을 얻었습니다. 따라서 밑의 식을 통해, 콴다선생님으로서 활동하면서 얼마를 벌고싶은지를 입력하여 평균적으로 몇번 답변해야하는지 입문 선생님들께 알려줄 수 있습니다.

In [13]:
hope_money = int(input("콴다 선생님을 통해 얻고 싶은 수익을 입력하세요 : "))

hope_coin = 5*hope_money # 콴다의 코인에 5를 나누어주면 '원' 단위가 됨   ex) 5000코인 = 1000원

requested_answer = hope_coin/expect_coin_per_answer

print("평균적으로",int(round(requested_answer)),"번 답변해야 합니다.") # 반올림 한 후 출력
콴다 선생님을 통해 얻고 싶은 수익을 입력하세요 : 100000
평균적으로 74 번 답변해야 합니다.

따라서 위 프로그램을 통해, 입문 선생님들은 자신이 벌고싶은 수익에 대해 평균적으로 몇번 답변해야하는지를 쉽게 알 수 있을뿐만 아니라, 입문 선생님들이 여러번 답변을 한 후 위에서 구한 최소제곱직선에 자신의 답변값을 대입하여 얻은 찜목록자수와 현재 자신의 찜목록자수를 비교하며, 평균보다 뒤쳐지는지에 대해 알 수 있고, 뒤쳐진다면 자신의 문제점에 대해 생각하며 자신의 답변들에 대한 문제점을 찾고 개선하고자 노력할 것입니다.

- 두 번째

상위권 선생님들의 주요 과목(수학,과학,영어,국어,사회,한국사,논술)을 종류별로 나열하여, matplotlib 모듈을 이용해 막대그래프로 나타냅니다. 이를 통해 입문 선생님이 어떤 과목을 필터링해야 더 많은 일반질문과 Pick 질문을 받을 수 있는지 알 수 있을 것입니다.

첫 번째 프로그램과 마찬가지로, 먼저 final_teacher의 subject에 대한 리스트를 각각 불러옵니다. 그리고 과목의 종류들을 보여주는 리스트도 print 합니다.

In [14]:
fileMatrix = []
subject_list=[]
with open('final_teacher_data.csv','r',encoding='utf-8') as file:
    for lineContent in file:
        fileMatrix.append(lineContent.strip('\n').split(','))

for j in range(len(fileMatrix)):
    subject_list.append(fileMatrix[j][2])



print(subject_list)
print(list(set(subject_list))) # 과목의 종류만을 프린트
['수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '영어', '수학', '과학', '수학', '수학', '영어', '영어', '영어', '수학', '영어', '수학', '수학', '수학', '수학', '수학', '수학', '영어', '수학', '수학', '수학', '수학', '수학', '영어', '수학', '수학', '영어', '영어', '영어', '수학', '수학', '수학', '영어', '수학', '영어', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '수학', '영어', '영어', '수학', '수학', '수학', '수학', '수학']
['영어', '과학', '수학']

일단 위를 통해, 상위권 선생님들의 주요과목 중 '사회'가 하나도 포함되지 않은 것을 알 수 있습니다. 따라서 보다 빠른 답변의 순환을 위해, 입문 선생님들이 사회 과목에 대한 질문만을 받을 수 있도록 필터링하는 행동은 효율적이지 못할 것입니다. 그리고 이제 사회를 제외한 수학, 국어, 과학, 영어에 대한 비율을 원그래프로 나타낼 것입니다. 이를 위해 matplotlib 의 pyplot을 import합니다.

In [15]:
from matplotlib import pyplot as plt




math=0
science=0
English=0
Korean=0

for i in range(len(subject_list)):
    if subject_list[i] == '수학':
        math = math + 1
        
    elif subject_list[i] == '과학':
        science = science + 1
        
    elif subject_list[i] == '영어':
        English = English + 1
        
    else:
        Korean = Korean + 1

subject_count_list = [math,science,English,Korean] # 수학, 과학, 영어, 국어 순서대로 counting 갯수 리스트 
print(subject_count_list) 

from matplotlib import pyplot as plt
categories = ['Math','Science','Engilsh','Korean']
plt.pie(subject_count_list, labels=categories,autopct='%0.1f%%')
plt.show()
# 원그래프로 표현
[48, 1, 14, 0]

위 원그래프를 보아, 수학이 전체비율 중 74.1%로 압도적인 비율을 차지하고 있으며, 그 다음이 영어이고, 그 다음은 과학, 국어 순서대로 비율을 차지하고 있는 것을 확인할 수 있습니다. 따라서 대학생들이 콴다에 입문할 때, 적어도 수학만큼은 중,고등학교 과정에 대해 복습을 하고 콴다활동을 하는 것이 효율적일 것입니다.

- 세 번째

이번에는 데이터를 가공할 때 저장해놓은 final_review_below_2.csv 파일을 아래와 같은 형식으로 불러옵니다.

In [16]:
text = open('final_review_below_2.csv',encoding='utf-8').read()
print(text)
"답안지는 있었고, 단지 풀이을 알고 싶었는데 인상도 별로고 
이해가 안됌니다.번창하세요.."
벼로에요
이해안감
ê·¼ë�° ㅜㅜ ì œê°€ 질문한것ì—�대해 ì›�리는 ì•ˆì•Œë ¤ì£¼ì‹œê³  다루지 않는 ë‹¤ê³ ë§Œ 하셔서 좀 아쉬웡요
정말 식도 대충써주시고 답변도 안하내요 정말최악입니다
계산 ìƒ�ë�µ 너무ë§�ì�´ë�˜ì„œ 보기 ì–´ë ¤ì›€
다 틀렸어요
다풀었는데 알려주노
풀� 해 달�는� 답안지를 그대로 빼껴 오시면...
공부를 질문했ë�”니 답ì�€ ì•Œë ¤ì£¼ì§€ë§Œ  ë§�ì�„기분나ì�˜ê²Œ 하셔서 기분ì�´ ë‚˜ë¹´ê³ ì„¤ëª…ì�´ 없네요
ì œêº¼ëŠ” ì–¸ì œ 마무리가 ë�˜ì£ ?..
ë“¤ë ¸ì–´ìš”...
답ì�¥ì�„ 빨리보내주시지 ì•Šì•„ì„œ ë‹µë‹µí–ˆê³  ì�´í•´ê°€ 안ë�˜ì„œ ì�¬ì§ˆë¬¸ 했는ë�°ë�„ ë¶ˆêµ¬í•˜ê³  답ì�¥ ì—†ì�Œ
답� 계� 틀리는�요?
ㄱ개 ㅂㄹ
틀리심..^^
ì�´í•´ ì „í˜€ 안ë�¼ìš”
모르는거 물어보라고 했는데 답을 안해 주시네..^^
ì •ë§� ê°�사합니다
ê°œë¶ˆì¹œì ˆí•¨
"별루ì—�ìš”.í’€ì�´ë¥¼ 안가르ì³�주세요. ëª¨ë¥´ê² ëŠ”ë�°ã… ã… 
넘 ì†�ìƒ�하네요. ì½”ì�¸ë�„ ë‚ ë¦¬ê³  ã… ã…  
ì •ë§�루"
답이 정확하지않은데 답변도 없고~~
겁나 부정확함
답 틀림.
별로에요.답지설명을거의다써놓다시피하셨네요ㅜ
이해가 잘 안갔어요...
답ì�´ í‹€ë ¸ì–´ìš”..
답 틀리셨어요 ~^;;;;;;
완전 우!!!(아니 불친절)
답변보고 전혀 모르겠어요
좀 어렵게 풀이를 쓰셧다 근데 내가 못해서 못알아보는걸수도
�해 �어요 �사합니다!.!
걍
설명이 부족하고 풀이가 명확하지 않아요....
설명ì�„ 해줘야지 답만 ì•Œë ¤ì£¼ì‹œë©´ ì œê°€ ì°�ì–´ë�„ ë§�ê² ë„¤ì—¬;;;
�.....
설명 별로�요
답변� ��하게함
답지를 찾아 올리신 것 같은데 그건 그렇다쳐도 질문에 답을 안해주시는건 너무합니다..
��해
감사합니당
설명� 너무 간단해요;
왜 그런 결과가 나왔는지 설명해주셨으면 ì¢‹ê² ìŠµë‹ˆë‹¤ì„¸ë¶€ 설명ë�„ 해주셨으면 ì¢‹ê² ì–´ìš”
답변이 질문과 다름
??
답�.아닌�요
.
답이 틀림
답장이 느려요
대충해주셨다
별로예요ㅠㅠ
싸가지 없어요
별로였다
너무 답� �림
답� ��함
답�� 안해줌
�해가 안�서 물어보았는� 답변� 없으세요...ㅜ
.
풀이를 좀 식만 써주시고 재질문하면 답을 안해주신다.
이해가 안되서 다시 질문해서 알려주시긴했지만 이해가 안되네요 그냥 포기할께요 ..^^
풀의는 잘하는데 좀... 예의가 없어여
답이 틀렸어요..
답지 베끼네ㅋㅋㅋ 돈아까워요
답변에 대해 궁금한게 있는데 다시 답변을 안 해주셔요..
다시 물어보면 쳐 말안해주는 새끼
풀이를 안 알려줌 답젼이 느림
답만 알려주고 풀이는 안알려주시네요....
되게 답변 빠르신데 풀이는 안 주시네요.
"3.1인데 3.14로 잘못계산하셨어요
ㅠㅠ"
틀렸습니다ㅠㅠ
답� 틀리셨네요
"답� 틀리셨너요...
ì–´ì©Œì£ ?"
대답ë�„ 조금 ëŠ�ë¦¬ì‹œê³  그림ì—� 글쓰시는게 조지네요;; 다ì�Œë¶€í„´ 종ì�´ì—� 좀해주셨으면 합니다
"화면ì—� ëŒ€ê³  글씨 ì�¨ì£¼ì…”ì„œ 보는게 좀 불í�¸í–ˆë„¤ìš”;;
ì•�ìœ¼ë¡  종ì�´ì—�다가 ì�¨ì£¼ì‹œë©´ ì¢‹ê² ìŠµë‹ˆë‹¤."
답 절대 안해주시네요
와
대충 ì•Œë ¤ì£¼ì‹œê³  질문했는ë�° 답ë�„ 안하네요 ã…‹
중간ì—� 안ì �어줬네요ㅜㅜ
별로
답ì�´ 명쾌하지ë�„ ì•Šê³  í��지부지 하네요 ..
처� 질문� 대한 답� 해주시지 않으셨습니다. 지문� 반��� 30분�안 답� 없으셨습니다
너무 말투가 짜증남.
ㅂㄹ
완전 안조음
풀이가 자세하지 않음.별로
별로네?
조금� 세세한 풀�를 해주시면 �욱 좋�것 같습니다
정확히 알려 주시지 않았습니다
그림으로해주셔서 좋아요
지금까지 질문한 답들이 다 틀림
�사합니다 ?
답ì�„ 너무 ì•ˆí•˜ì‹œê³ ...ì´ˆ5ë¬¸ì œë¥¼ 중등 수학으로 풀어주시네요...
추가질문 ì��체를 안받는건지 못보시는건지 답변ì�„ 안해주시는 ì„ ìƒ�님들ì�´ 콴다ì—� 너무 ë§�네요.
굿
정확하게 답을 말씀안해주시네요^^
너무 느려요
ㅗ
풀이가 자세하지 않고요.... 질문에 대해 답을 안해주시네요... 차단하겠습니다..

그리고, word cloud를 구현하기 위한 모듈들을 improt 합니다.

In [17]:
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
from wordcloud import WordCloud
%matplotlib inline

워드클라우드의 외형이 콴다앱의 이미지가 되도록, 콴다의 이미지를 저장한 후 word cloud형태로 사용할 수 있게 불러옵니다.

In [18]:
quanda_mask = np.array(Image.open('quanda.png'))
print(quanda_mask)
[[[255 255 255 255]
  [255 255 255 255]
  [255 255 255 255]
  ...
  [255 255 255 255]
  [255 255 255 255]
  [255 255 255 255]]

 [[255 255 255 255]
  [255 255 255 255]
  [255 255 255 255]
  ...
  [255 255 255 255]
  [255 255 255 255]
  [255 255 255 255]]

 [[255 255 255 255]
  [255 255 255 255]
  [255 255 255 255]
  ...
  [255 255 255 255]
  [255 255 255 255]
  [255 255 255 255]]

 ...

 [[255 255 255 255]
  [255 255 255 255]
  [255 255 255 255]
  ...
  [255 255 255 255]
  [255 255 255 255]
  [255 255 255 255]]

 [[255 255 255 255]
  [255 255 255 255]
  [255 255 255 255]
  ...
  [255 255 255 255]
  [255 255 255 255]
  [255 255 255 255]]

 [[255 255 255 255]
  [255 255 255 255]
  [255 255 255 255]
  ...
  [255 255 255 255]
  [255 255 255 255]
  [255 255 255 255]]]

이제 크롤링한 리뷰들을 word cloud로 나타낼 수 있고, 어떠한 단어가 많이 나왔는지를 딕셔너리형태로 볼 수 있도록 밑과 같이 코딩합니다.

In [19]:
wc = WordCloud(font_path='C:\\Users\\Sion\\Desktop\\Font\\NanumBarunGothic.ttf', background_color="white", max_words=2000, mask=quanda_mask) 
# 이때 C:\\Users\\Sion\\Desktop\\Font\\NanumBarunGothic.ttf 는 word cloud로 나타낼때의 폰트를 ttf 확장자로 다운받아 놓은 주소

wc = wc.generate(text)
wc.words_ # 딕셔너리 타입으로 확인
Out[19]:
{'ˆë': 1.0,
 'ìš': 0.8571428571428571,
 '¼ì': 0.6785714285714286,
 'µì': 0.5714285714285714,
 'ϑ': 0.5,
 'ëŠ': 0.35714285714285715,
 'ë³': 0.32142857142857145,
 'ëª': 0.32142857142857145,
 'ê³': 0.25,
 'ëŒ': 0.21428571428571427,
 'ìƒ': 0.21428571428571427,
 'ˆí': 0.21428571428571427,
 'ê²': 0.21428571428571427,
 '답을': 0.17857142857142858,
 '으ë': 0.17857142857142858,
 '습ë': 0.17857142857142858,
 '이해가': 0.14285714285714285,
 'œê³': 0.14285714285714285,
 'ϓ': 0.14285714285714285,
 'µë': 0.14285714285714285,
 'ˆì': 0.10714285714285714,
 '게': 0.10714285714285714,
 '답이': 0.10714285714285714,
 '풀이를': 0.10714285714285714,
 '풀이가': 0.10714285714285714,
 'µë³': 0.10714285714285714,
 '다시': 0.10714285714285714,
 'œëŠ': 0.10714285714285714,
 'ϋ': 0.07142857142857142,
 'ϐ': 0.07142857142857142,
 '답변도': 0.07142857142857142,
 '틀렸어요': 0.07142857142857142,
 'ë¹': 0.07142857142857142,
 'ˆëŠ': 0.07142857142857142,
 'ì½': 0.07142857142857142,
 '완전': 0.07142857142857142,
 '질문에': 0.07142857142857142,
 '틀림': 0.07142857142857142,
 '느려요': 0.07142857142857142,
 '으ì': 0.07142857142857142,
 '대해': 0.07142857142857142,
 '풀이는': 0.07142857142857142,
 '안해주시네요': 0.07142857142857142,
 'ì²': 0.07142857142857142,
 '너무': 0.07142857142857142,
 '자세하지': 0.07142857142857142,
 '답안지는': 0.03571428571428571,
 '있었고': 0.03571428571428571,
 '단지': 0.03571428571428571,
 '풀이을': 0.03571428571428571,
 '알고': 0.03571428571428571,
 '싶었는데': 0.03571428571428571,
 '인상도': 0.03571428571428571,
 '별로고': 0.03571428571428571,
 '안됌니다': 0.03571428571428571,
 '번창하세요': 0.03571428571428571,
 '벼로에요': 0.03571428571428571,
 '이해안감': 0.03571428571428571,
 '¼ë': 0.03571428571428571,
 'œê²ƒì': 0.03571428571428571,
 'ŠëŠ': 0.03571428571428571,
 '정말': 0.03571428571428571,
 '식도': 0.03571428571428571,
 '대충써주시고': 0.03571428571428571,
 '안하내요': 0.03571428571428571,
 '정말최악입니다': 0.03571428571428571,
 '다풀었는데': 0.03571428571428571,
 '알려주노': 0.03571428571428571,
 'ë¹¼ê': 0.03571428571428571,
 'ê³µë': 0.03571428571428571,
 'œêº¼ëŠ': 0.03571428571428571,
 'Šì': 0.03571428571428571,
 'µí': 0.03571428571428571,
 'ˆê³': 0.03571428571428571,
 'ˆêµ': 0.03571428571428571,
 '틀리심': 0.03571428571428571,
 '¼ìš': 0.03571428571428571,
 '모르는거': 0.03571428571428571,
 '물어보라고': 0.03571428571428571,
 '했는데': 0.03571428571428571,
 '안해': 0.03571428571428571,
 '주시네': 0.03571428571428571,
 'ˆì¹œì': 0.03571428571428571,
 'ˆê': 0.03571428571428571,
 'ì³': 0.03571428571428571,
 '정확하지않은데': 0.03571428571428571,
 '없고': 0.03571428571428571,
 '겁나': 0.03571428571428571,
 '부정확함': 0.03571428571428571,
 '별로에요': 0.03571428571428571,
 '답지설명을거의다써놓다시피하셨네요ㅜ': 0.03571428571428571,
 '안갔어요': 0.03571428571428571,
 '틀리셨어요': 0.03571428571428571,
 '아니': 0.03571428571428571,
 '불친절': 0.03571428571428571,
 '답변보고': 0.03571428571428571,
 '전혀': 0.03571428571428571,
 '모르겠어요': 0.03571428571428571,
 '어렵게': 0.03571428571428571,
 '쓰셧다': 0.03571428571428571,
 '근데': 0.03571428571428571,
 '내가': 0.03571428571428571,
 '못해서': 0.03571428571428571,
 '못알아보는걸수도': 0.03571428571428571,
 '설명이': 0.03571428571428571,
 '부족하고': 0.03571428571428571,
 '명확하지': 0.03571428571428571,
 '않아요': 0.03571428571428571,
 '게í': 0.03571428571428571,
 '답지를': 0.03571428571428571,
 '찾아': 0.03571428571428571,
 '올리신': 0.03571428571428571,
 '같은데': 0.03571428571428571,
 '그건': 0.03571428571428571,
 '그렇다쳐도': 0.03571428571428571,
 '안해주시는건': 0.03571428571428571,
 '너무합니다': 0.03571428571428571,
 '감사합니당': 0.03571428571428571,
 'ëŸ': 0.03571428571428571,
 'ê³¼ê': 0.03571428571428571,
 '답변이': 0.03571428571428571,
 '질문과': 0.03571428571428571,
 '다름': 0.03571428571428571,
 '답장이': 0.03571428571428571,
 '대충해주셨다': 0.03571428571428571,
 '별로예요ㅠㅠ': 0.03571428571428571,
 '별로였다': 0.03571428571428571,
 '식만': 0.03571428571428571,
 '써주시고': 0.03571428571428571,
 '재질문하면': 0.03571428571428571,
 '안해주신다': 0.03571428571428571,
 '안되서': 0.03571428571428571,
 '질문해서': 0.03571428571428571,
 '알려주시긴했지만': 0.03571428571428571,
 '안되네요': 0.03571428571428571,
 '그냥': 0.03571428571428571,
 '포기할께요': 0.03571428571428571,
 '풀의는': 0.03571428571428571,
 '잘하는데': 0.03571428571428571,
 '예의가': 0.03571428571428571,
 '없어여': 0.03571428571428571,
 '답지': 0.03571428571428571,
 '베끼네ㅋㅋㅋ': 0.03571428571428571,
 '돈아까워요': 0.03571428571428571,
 '답변에': 0.03571428571428571,
 '궁금한게': 0.03571428571428571,
 '있는데': 0.03571428571428571,
 '답변을': 0.03571428571428571,
 '해주셔요': 0.03571428571428571,
 '물어보면': 0.03571428571428571,
 '말안해주는': 0.03571428571428571,
 '새끼': 0.03571428571428571,
 '알려줌': 0.03571428571428571,
 '답젼이': 0.03571428571428571,
 '느림': 0.03571428571428571,
 '답만': 0.03571428571428571,
 '알려주고': 0.03571428571428571,
 '안알려주시네요': 0.03571428571428571,
 '되게': 0.03571428571428571,
 '답변': 0.03571428571428571,
 '빠르신데': 0.03571428571428571,
 '주시네요': 0.03571428571428571,
 '1인데': 0.03571428571428571,
 '14로': 0.03571428571428571,
 '잘못계산하셨어요': 0.03571428571428571,
 'ㅠㅠ': 0.03571428571428571,
 '틀렸습니다ㅠㅠ': 0.03571428571428571,
 'ˆìš': 0.03571428571428571,
 '절대': 0.03571428571428571,
 '쾌í': 0.03571428571428571,
 'Šê³': 0.03571428571428571,
 'Šìœ¼ì': 0.03571428571428571,
 '30ë': 0.03571428571428571,
 '말투가': 0.03571428571428571,
 '짜증남': 0.03571428571428571,
 'ㅂㄹ': 0.03571428571428571,
 '안조음': 0.03571428571428571,
 '않음': 0.03571428571428571,
 '별로': 0.03571428571428571,
 '것': 0.03571428571428571,
 '정확히': 0.03571428571428571,
 '알려': 0.03571428571428571,
 '주시지': 0.03571428571428571,
 '않았습니다': 0.03571428571428571,
 '그림으로해주셔서': 0.03571428571428571,
 '좋아요': 0.03571428571428571,
 '지금까지': 0.03571428571428571,
 '질문한': 0.03571428571428571,
 '답들이': 0.03571428571428571,
 'ˆ5ë': 0.03571428571428571,
 'ìˆ': 0.03571428571428571,
 '정확하게': 0.03571428571428571,
 '말씀안해주시네요': 0.03571428571428571,
 '않고요': 0.03571428571428571,
 '차단하겠습니다': 0.03571428571428571}

이제, 위 wc 데이터를 가지고 word cloud로 나타냅니다. 코드는 밑과 같습니다.

In [20]:
plt.figure(figsize=(12,12))
plt.imshow(wc,interpolation='bilinear')
plt.axis("off")
plt.show()

비록 위 word cloud에서 가장 크게 나온 단어인 '답이','너무','답변' 같은 경우는 입문 콴다선생님들께 특별한 정보를 주지는 않지만, 이를 제외한 조금 큰 단어들 ('제대로', '틀렸어요', '아깝네요','자세하게,'느려요' 등)을 보았을 때, 콴다선생님들은 답변을 할 때 '정확도'가 최우선이 되어야 하며, 그다음은 답변 속도, 풀이의 상세함이 뒷받쳐줘야 한다는 사실을 알 수 있습니다. 입문 콴다선생님들은 이를 통해 답변을 올바른 방향으로, 즉 질문자의 만족도가 높아지는 방향으로 답변을 작성하게 될 것입니다.

결론

위 3가지의 소프트웨어를 통해, 이 프로젝트의 궁극적인 목표인 '콴다 입문 선생님들을 위한 가이드라인 제시'는 성공적으로 달성하였습니다. 하지만 첫 번째, 세 번째 소프트웨어에 대해 약간의 아쉬움이 남습니다.

첫 번째 소프트웨어에서, 비례식을 활용해 ' 답변수에 대한 찜 목록자수를 평균적으로 가지고 있을 때, 답변 하나 당 얻을 수 있는 코인의 평균값'을 구할 때, S+ 선생님 한분('모도린' 선생님)의 누적 코인갯수만을 대푯값으로 지정한 것에 대해 아쉬움이 남습니다. 비록 이 선생님의 누적 답변수, 찜목록자수의 관계가 최소제곱직선과 유사하긴 하였지만, 더 많은 선생님들의 누적 코인갯수 데이터가 있었다면 더욱 정확한 값을 얻어낼 수 있었을 것입니다.

그리고 세 번째 소프트웨어에서, 도움이 되지 않을 word들('답이','너무','답변' 등)을 삭제시키고 새로운 word cloud를 많들고 싶었지만, 계속 구글링을 해봐도 해당 방법에 대한 정보는 찾을 수 없어, 가독성이 높은 word cloud를 만들지 못한 것이 아쉽습니다.

하지만 나머지 부분에 대한 것들(데이터 크롤링 과정, 그외 소프트웨어 구현 과정)들은 논리적 결함없이 잘 구현되었다고 판단하기에, 위 3가지 소프트웨어는 입문 콴다선생님들께 반드시 도움이 될 것입니다.

그 외(참고문헌, 데이터 원본 링크 등)

참고문헌 :

Fiddler 프로그램, www.base64decode.org(base64 인,디코더),네이버 블로그(최소 자승법), pinkwink.kr(word cloud 참조)

원본 데이터

모든 데이터가 Fiddler에 저장되어 있어 원본데이터를 따로 불러오기 힘듭니다. 하지만 가장 첫번째 코드의 일부를 사용하면 dictionary 형태로 불러올 수는 있지만, 그 양이 굉장히 방대하여 따로 나타내지 못한점 양해부탁드립니다.