이전 포스팅 (https://kibua20.tistory.com/192)에서는 Python Selenium과 BeatifulSoup을 사용해서 Google Play Store에서 App에 대한 사용자, 작성일, 좋아요, 리뷰 내용의 데이터를 크롤링하는 방법에 대해서 설명했습니다. Apple App Store에서는 사용자 리뷰 정보를 웹 페이지와 RSS Feed로 제공하고 있으며, 이를 수집하는 방법을 설명합니다.
Apple App 앱 스트에서 사용자 리뷰는 아래 그림과 같이 사용자 리뷰 내용을 제공하고 있습니다. 아래 사이트를 Selenium을 사용해서 크롤링도 가능하지만, RSS Feed가 보다 정형적인 데이터를 제공하고 구현이 쉽게 때문에 RSS Feed를 사용합니다. 예를 들어 Youtube의 RSS Feed와 사용자 리뷰 사이트는 아래와 같습니다.
- RSS 사이트: https://itunes.apple.com/kr/rss/customerreviews/page=1/id=544007664/sortby=mostrecent/xml
- 사용자 리뷰 사이트: https://apps.apple.com/us/app/youtube-watch-listen-stream/id544007664#see-all/reviews
애플이 제공하는 RSS Feed를 활용해서 사용자 리뷰 정보를 얻어오는 방법은 다음과 같이 3단계가 필요하고, 각 단계별로 상세하게 설명하도록 하겠습니다.
- Apple Appostore에서 App id 확인하기
- AppID의 RSS Feed 사이트에서 사용자 리뷰 내용을 xlm 파일로 받기
- XML 파일을 Parsing 해서 사용자, 입력 날짜, 평점, 좋아요 데이터를 얻어 CSV 파일로 저장
App ID 확인: Apple 앱 스토어 사이트에서 검색하여 URL 확인
애플의 App ID를 확인하는 방법은 Apple site에서 앱 이름으로 검색하면 상단 URL에서 App ID를 알 수 있습니다. 예를 들어 Youtube을 App site에서 검색하고 App Store URL을 확인하면 URL의 마지막 부분인 ID 544007644가 Youtube의 App ID입니다.
▣ Apple Appstore의 Youtube ID: https://apps.apple.com/us/app/youtube-watch-listen-stream/id544007664
AppID의 RSS Feed 사이트에서 사용자 리뷰 내용을 xlm 파일로 받기
RSS Feed는 JSON과 XML 데이터 형식으로 제공합니다. JSON과 XML의 내용은 동일하지만, JSON에서는 리뷰 작성일이 빠져 있어 XML 데이터를 활용하는 것을 추천드립니다. 특정 App의 RSS의 URL은 아래와 같이 언어 코드와 App ID 구성되어 있어 App ID를 먼저 확인해야 합니다. (참고 링크: https://stackoverflow.com/questions/1090282/api-to-monitor-iphone-app-store-reviews)
▣ RSS Feed URL: http://itunes.apple.com/[언어]/rss/customerreviews/id=[you app id]/xml
e.g) https://itunes.apple.com/kr/rss/customerreviews/id=544007664/xml
Apple RSS 사이트에서 사용자 리뷰를 Python requests를 사용해서 다운로드 받습니다. Apple의 RSS Feed는 사용자 리뷰를 50개씩 나눠서 최대 10개의 URL Page에 나눠서 다운로드할 수 있습니다.
1st Page Feed: https://itunes.apple.com/kr/rss/customerreviews/page=1/id=544007664/sortby=mostrecent/xml
2nd Page Feed: https://itunes.apple.com/kr/rss/customerreviews/page=2/id=544007664/sortby=mostrecent/xml
...
n-th Page Feed: URL: https://itunes.apple.com/kr/rss/customerreviews/page=n/id=544007664/sortby=mostrecent/xml
애플 RSS Feed사이트에서 XML형식으로 다운로드한 내용은 아래와 같습니다. XML 내용에는 사용자 리뷰의 시작 URL과 마지막 URL이 <link> tag에 명시되어 있고, <entry> 하위에 사용자 리뷰 내용이 정리되어 있습니다. <updated> tag에 사용자가 업데이트한 날짜를 가지고 있고, 제목은 <title> tag, 사용자는 <author> tag에 저장되어 있고, 제일 중요한 사용자 리뷰 내용은 <content>에서 가져올 수 있습니다.
XML 파일을 Parsing 해서 사용자, 입력 날짜, 평점, 좋아요 데이터를 얻어 CSV 파일로 저장
Python에서는 requests 모듈을 사용해서 ①XML을 다운로드하고, ② response를 xlmtodict 모듈로 dictionary형태로 변환 후 ③Pandas Dataframe으로 저장합니다. ④ 최종적으로는 아래와 같이 CSV 파일로 저장합니다. 세부적인 내용은 Python code와 함께 Comment로 추가하였습니다.
Python code로 옮기면 get_url_index()와 appstore_crawler()로 정리할 수 있고 코드에 대한 설명은 아래와 같습니다.
# 리뷰 내용에서 마지막 page의 Index 가져 오는 함수
def get_url_index(url):
# requests에 response에 대해서 한글 깨짐을 방지하기 위해서 UTF8로 decoding
response = requests.get(url).content.decode('utf8')
#xml 을 dict로 변환 - https://kibua20.tistory.com/146 에서 xmltodict 설명을 참고하세요.
xml = xmltodict.parse(response)
# 사용자 리뷰 Page의 last Index 가져오기
last_url = [l['@href'] for l in xml['feed']['link'] if (l['@rel'] == 'last')][0]
last_index = [int(s.replace('page=', '')) for s in last_url.split('/') if ('page=' in s)][0]
return last_index
# AppStore에서 Review 전체를 가져오는 함수
def appstore_crawler(appid, outfile='./appstore_reviews.csv'):
url = 'https://itunes.apple.com/kr/rss/customerreviews/page=1/id=%i/sortby=mostrecent/xml' % appid
# 예외 처리 1: Review가 전혀 없는 경우
try:
last_index = get_url_index(url)
except Exception as e:
print ('\tNo Reviews: appid %i' %appid)
print ('\tException:', e)
return
# Apple RSS Feed URL을 Page 1 ~ last index page까지 Iteration
result = list()
for idx in range(1, last_index+1):
url = "https://itunes.apple.com/kr/rss/customerreviews/page=%i/id=%i/sortby=mostrecent/xml?urlDesc=/customerreviews/id=%i/sortBy=mostRecent/xml" % (idx, appid, appid)
# 사용자 리뷰 xlm을 다운로드 (한글 깨짐 방지를 위해서 utf8로 인코딩)
response = requests.get(url).content.decode('utf8')
# xml을 dict로 변환, Apple Site에서 xml이 깨져 있는 경우가 있어 예외처리 추가
try:
xml = xmltodict.parse(response)
except Exception as e:
print ('\tXml Parse Error %s\n\tSkip %s :' %(e, url))
continue
# 사용자 Review가 존재하는지 확인, 리뷰가 없으면 이후 처리를 하지 않음
try:
num_reivews= len(xml['feed']['entry'])
except Exception as e:
print ('\tNo Entry', e)
continue
# 사용자 리뷰가 단 1개인 경우 XML 처리 시 에러 발생 방지
try:
xml['feed']['entry'][0]['author']['name']
single_reviews = False
except:
#print ('\tOnly 1 review!!!')
single_reviews = True
pass
# 사용자 리뷰를 list에 저장
if single_reviews:
result.append({
'USER': xml['feed']['entry']['author']['name'],
'DATE': xml['feed']['entry']['updated'],
'STAR': int(xml['feed']['entry']['im:rating']),
'LIKE': int(xml['feed']['entry']['im:voteSum']),
'TITLE': xml['feed']['entry']['title'],
'REVIEW': xml['feed']['entry']['content'][0]['#text'],
})
else:
for i in range(len(xml['feed']['entry'])):
result.append({
'USER': xml['feed']['entry'][i]['author']['name'],
'DATE': xml['feed']['entry'][i]['updated'],
'STAR': int(xml['feed']['entry'][i]['im:rating']),
'LIKE': int(xml['feed']['entry'][i]['im:voteSum']),
'TITLE': xml['feed']['entry'][i]['title'],
'REVIEW': xml['feed']['entry'][i]['content'][0]['#text'],
})
# 사용자 리뷰 결과 list를 DataFrame을 생성
res_df = pd.DataFrame(result)
# DATE Column은 String에서 Dataframe의 Date로 변환
res_df['DATE'] = pd.to_datetime(res_df['DATE'], format="%Y-%m-%dT%H:%M:%S")
# CSV 파일로 저장, 한글 깨짐을 방지하고 위해서 'utf-8-sig'로 저장, Index column은 저장하지 않음
res_df.to_csv(outfile, encoding='utf-8-sig', index=False)
GitHub Sample Code
Apple Appstore에서 사용자 댓글을 얻어오는 코드의 전체 코드는 아래와 같고 Git Hub에서 반영했습니다.
▣ 소스 코드: https://github.com/kibua20/devDocs/tree/master/appstore_crawler
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import pandas as pd
import xmltodict
import requests
import os
def get_url_index(url):
response = requests.get(url).content.decode('utf8')
xml = xmltodict.parse(response)
last_url = [l['@href'] for l in xml['feed']['link'] if (l['@rel'] == 'last')][0]
last_index = [int(s.replace('page=', '')) for s in last_url.split('/') if ('page=' in s)][0]
return last_index
# https://stackoverflow.com/questions/1090282/api-to-monitor-iphone-app-store-reviews
def appstore_crawler(appid, outfile='./appstore_reviews.csv'):
url = 'https://itunes.apple.com/kr/rss/customerreviews/page=1/id=%i/sortby=mostrecent/xml' % appid
try:
last_index = get_url_index(url)
except Exception as e:
print (url)
print ('\tNo Reviews: appid %i' %appid)
print ('\tException:', e)
return
result = list()
for idx in range(1, last_index+1):
url = "https://itunes.apple.com/kr/rss/customerreviews/page=%i/id=%i/sortby=mostrecent/xml?urlDesc=/customerreviews/id=%i/sortBy=mostRecent/xml" % (idx, appid, appid)
print(url)
response = requests.get(url).content.decode('utf8')
try:
xml = xmltodict.parse(response)
except Exception as e:
print ('\tXml Parse Error %s\n\tSkip %s :' %(e, url))
continue
try:
num_reivews= len(xml['feed']['entry'])
except Exception as e:
print ('\tNo Entry', e)
continue
try:
xml['feed']['entry'][0]['author']['name']
single_reviews = False
except:
#print ('\tOnly 1 review!!!')
single_reviews = True
pass
if single_reviews:
result.append({
'USER': xml['feed']['entry']['author']['name'],
'DATE': xml['feed']['entry']['updated'],
'STAR': int(xml['feed']['entry']['im:rating']),
'LIKE': int(xml['feed']['entry']['im:voteSum']),
'TITLE': xml['feed']['entry']['title'],
'REVIEW': xml['feed']['entry']['content'][0]['#text'],
})
else:
for i in range(len(xml['feed']['entry'])):
result.append({
'USER': xml['feed']['entry'][i]['author']['name'],
'DATE': xml['feed']['entry'][i]['updated'],
'STAR': int(xml['feed']['entry'][i]['im:rating']),
'LIKE': int(xml['feed']['entry'][i]['im:voteSum']),
'TITLE': xml['feed']['entry'][i]['title'],
'REVIEW': xml['feed']['entry'][i]['content'][0]['#text'],
})
res_df = pd.DataFrame(result)
res_df['DATE'] = pd.to_datetime(res_df['DATE'], format="%Y-%m-%dT%H:%M:%S")
res_df.to_csv(outfile, encoding='utf-8-sig', index=False)
print ('Save reviews to file: %s \n' %(outfile))
if __name__ == '__main__':
# https://apps.apple.com/us/app/youtube-watch-listen-stream/id544007664
app_id = 544007664
outfile = os.path.join('appstore_' + str(app_id)+'.csv')
appstore_crawler(app_id, outfile=outfile)
관련 글:
[SW 개발/Python] - Python: JSON 개념과 json 모듈 사용법
[SW 개발/Data 분석 (RDB, NoSQL, Dataframe)] - Panda Dataframe 날짜 기준으로 데이터 조회 및 처리하기
[SW 개발/Data 분석 (RDB, NoSQL, Dataframe)] - Pandas Dataframe 여러 열과 행에 apply() 함수 적용 (Sample code 포함)
[SW 개발/Data 분석 (RDB, NoSQL, Dataframe)] - CSV 파일에서 MariaDB(또는 MySQL)로 데이터 가져오는 방법
[SW 개발/Android] - Python BeautifulSoup으로 만든 Apk download 자동화 (Sample code)
[SW 개발/Python] - Python: xmltodict를 활용하여 XML를 JSON으로 변환하는 방법
[SW 개발/Python] - Python 명령어 처리: Argparse 모듈 (ArgumentParser 사용 예제)
[SW 개발/Python] - Python 표준 입출력(stdin/stdout) 활용 - 리눅스 프로그램과 연동
[SW 개발/Data 분석 (RDB, NoSQL, Dataframe)] - Jupyter Notebook의 업그레이드: Jupyter Lab 설치 및 extension 사용법
댓글