플레이 스토어 리뷰 크롤링 - 3


본 포스트와 관련된 코드는 현 시점에서 작동되지 않는다.(리뷰 전체 불러오기 불가)

대신 앱 리뷰 크롤링을 수행할 수 있는 더 빠르고 편한 라이브러리에 대한 소개를 해 두었으니 해당 포스트를 참고하시면 된다.


2개월만의 업데이트

분명 2번째 게시물에서 앱 링크를 하나씩 불러와 여는 기능을 올린다 했는데…그로부터 2개월이 지났다.

물론 2개월이란 시간동안 아무런 진전이 없었던 것은 아니지만 그 진전을 공유할 여유가 없었음을 양해 부탁드린다.

일단 직전 포스트의 내용을 이어 설명하자면, 파일로 저장한 각 앱 링크를 하나씩 여는 코드를 만들었다. 이후 개별 앱 링크를 받아 웹페이지를 열고 크롤링을 진행하는 함수, 그리고 크롤링 함수에서 만들어진 데이터를 엑셀로 저장하는 함수, 마지막으로 위 핵심 함수들을 하나로 묶어 실행시키는 함수까지 완성하였다.





#폴더 내 어플 링크 txt파일 읽어오는 함수
def altxtread() :
    path = "./" #현재 디렉토리
    file_list = os.listdir(path)
    al_list_txt = [file for file in file_list if file.endswith("어플링크.txt")] #어플링크.txt로 끝나는 파일 찾기
    return al_list_txt



앱 링크 txt파일을 불러와 링크를 순차적으로 여는 함수





#실질적인 크롤링을 하는 함수. 인수로 어플 링크를 받음. #txt 파일 이름도 받음
def crawler(x, keywordname) :
    webdriver_options = webdriver.ChromeOptions()
    webdriver_options .add_argument('headless')
    driver = webdriver.Chrome('./chromedriver.exe', options=webdriver_options)
    driver.get(x)
    
    name = driver.find_element_by_xpath("//h1[@class='AHFaub']").text #어플 이름 찾기
    name = re.sub('[-=+,#/\?:^$.@*\"※~&%ㆍ!』\\‘|\(\)\[\]\<\>`\'…》]', '', name) #특수문자 포함된 이름으로 엑셀 시트 이름 설정 불가.
    
    print(name, " crawling is in operation", datetime.today().strftime("%Y/%m/%d %H:%M:%S")) #크롤링중인 앱 이름 확인
 
    dev = driver.find_element_by_xpath("//a[@class='hrTbp R8zArc']").text #개발사 이름 찾기
 
    cat = driver.find_element_by_xpath("//a[@itemprop='genre']").text #개발사 이름 찾기
 
    detail = driver.find_element_by_xpath("//span[@jsslot]").text #어플 설명 찾기
    
    Udate = driver.find_element_by_xpath("//div/div[1]/span/div/span[@class='htlgb']").text #마지막 업데이트 날짜
    
    Inum = driver.find_element_by_xpath("//div/div[3]/span/div/span[@class='htlgb']").text #설치 수
 
    RrateAll = driver.find_element_by_xpath("//div[@class='BHMmbe']").text #평균 평점 찾기
 
    Rppl = driver.find_element_by_xpath("//span[@class='EymY4b']").text #평점 개수 찾기
 
####리뷰 크롤링
 
    driver.get(x + "&showAllReviews=true") 
#주소에 &showAllReviews=true를 통해 전체 리뷰 화면으로 넘어감
    time.sleep(2)
    
    print('review crawling is in operation now.', datetime.today().strftime("%Y/%m/%d %H:%M:%S")) #리뷰 크롤링 시작 알림
 
    Scroll_pause_time = 3.5 #전체 리뷰를 불러오기 위해 자동으로 스크롤 다운 및 더보기를 눌러주는 기능
    last_height = driver.execute_script("return document.body.scrollHeight")
    while True:
        for i in range(5) :
            driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
            time.sleep(Scroll_pause_time)
            
        try: #게시글이 표시될때까지 대기. 최대 20초. 게시글이 표시 될 경우 즉시 이하의 코드를 실행
            element = WebDriverWait(driver, 20).until(
                                                    EC.presence_of_element_located((By.CLASS_NAME, "CwaK9"))
            )
            print('더보기가 표시되었습니다.', datetime.today().strftime("%Y/%m/%d %H:%M:%S"))
        except: 
            print('게시글이 로드되지 않았습니다. 스크롤을 2회 다시 시도합니다.', datetime.today().strftime("%Y/%m/%d %H:%M:%S")) 
            for i in range(2) :
                driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
                time.sleep(Scroll_pause_time)
        try : 
            viewmore = driver.find_element_by_xpath("//span[@class='RveJvd snByac']")
            driver.execute_script("arguments[0].click();", viewmore)
        except NoSuchElementException: 
            pass
        #최하단에는 더보기 버튼이 생기지 않아 오류 발생. 해당 오류를 무시하도록 수정
        new_height = driver.execute_script("return document.body.scrollHeight")
        
        if new_height == last_height:
            break
        last_height = new_height
    
    viewallreviews = driver.find_elements_by_xpath("//button[@class='LkLjZd ScJHi OzU4dc  ']")
    viewallreviews
    
    print(str(name) + "'s review expanding is now in operation.", datetime.today().strftime("%Y/%m/%d %H:%M:%S"))
 
    for i in range(len(viewallreviews)) : #전체보기 버튼이 있는 리뷰 처리
        
        isTrue = viewallreviews[i].is_displayed()
        print("Element is visible?" + str(isTrue))
        if isTrue: 
            viewallreviews[i].send_keys(Keys.ENTER) #click 명령어로는 작동 하지 않음. 기존 코드 수정
            print(str(i)+"th more button is clicked and wait 0.5 secs...")
            time.sleep(0.8)
        
    ####스크롤 기능 및 전체보기 버튼 클릭 종료  
 
    reviews = driver.find_elements_by_xpath("//span[@jsname='bN97Pc']") #기본리뷰
    long_reviews = driver.find_elements_by_xpath("//span[@jsname='fbQN7e']") #전체보기버튼 눌러 확인하는 리뷰
    
    try :
        developer_review_answers = driver.find_elements_by_xpath("//div[@class='LVQB0b']") #개발자 리뷰 답변
        developer_review_answers_date = driver.find_elements_by_xpath("//div[2]/div[3]/div[2]/span[2][@class='p2TkOb']") #개발자리뷰답변날짜
    except NoSuchElementException :
        pass
    print("merging reviews...", datetime.today().strftime("%Y/%m/%d %H:%M:%S"))
 
    #전체보기 리뷰는 기본 리뷰에 포함되지 않음. 두 리뷰를 합쳐주어야 함. 
    merged_review = [t.text if t.text!='' else long_reviews[i].text for i,t in enumerate(reviews)] ##전체 리뷰 LIST방식
    
    print("reviews are merged", datetime.today().strftime("%Y/%m/%d %H:%M:%S"))
    
    #### 개별 리뷰 별점등 크롤링
    
    Rdate = driver.find_elements_by_xpath("//span[@class='p2TkOb']") #개별 리뷰 등록 날짜 LIST 방식
    Rlike = driver.find_elements_by_xpath("//div[@aria-label='이 리뷰가 유용하다는 평가를 받은 횟수입니다.']") #좋아요 갯수 LIST방식
    Rstar = driver.find_elements_by_xpath("//span[@class='nt2C1d']/div[@class='pf5lIe']/div[@role='img']") # 평점 LIST방식 .get_attribute('aria_label') 사용할 것
    
    print("data is being saved in excel", datetime.today().strftime("%Y/%m/%d %H:%M:%S"))
    
    save_in_excel(keywordname, name, dev, cat, detail, Udate, Inum, RrateAll, Rppl, merged_review, Rdate, Rlike, Rstar, developer_review_answers, developer_review_answers_date) #엑셀로 저장하는 함수
    
    driver.quit() #웹드라이버 종료 및 크롬 창 종료. 메모리 관리 목적.



실질적 크롤링을 하는 함수, 앱 이름, 개발사 이름 등 기본적인 정보부터 전체 리뷰를 보는 기능까지 구현되어있다.





#엑셀로 저장하는 함수
def save_in_excel(keywordname, name, dev, cat, detail, Udate, Inum, RrateAll, Rppl, merged_review, Rdate, Rlike, Rstar, developer_review_answers, developer_review_answers_date) :
    crawlreview = [] #리뷰 딕셔너리
    appdata = [] #이름, 개발사 등 저장 딕셔너리
    developer_review = [] #개발사 리뷰 답변 딕셔너리
    Rlike_int = 0
    
    print('review dictionary is being made', datetime.today().strftime("%Y/%m/%d %H:%M:%S"))
    for i in range(len(merged_review)) : ##리뷰 딕셔너리 for i in range(len(merged_review))
        if Rlike[i].text == '' :
            Rlike_int = '0'
        else :
            Rlike_int = Rlike[i].text
        crawlreview.append({
            'DATE' : Rdate[i].text,
            'STAR' : int(Rstar[i].get_attribute('aria-label')[10:11]),
            'LIKE' : int(Rlike_int),
            'REVIEW' : merged_review[i]
        })
        
    print('appdata dictionary is being made', datetime.today().strftime("%Y/%m/%d %H:%M:%S"))
    appdata.append({  #이름, 개발사 등 저장 딕셔너리
        'NAME' : name,
        'DEVELOPER' : dev,
        'CATEGORY' : cat,
        'DESCRIPTION' : detail,
        'LATEST_UPDATE' : Udate,
        'INSTALLED' : Inum,
        'AVERAGE_RATE' : float(RrateAll),
        'RATE_COUNT' : Rppl,
        'ACQUIRED_DATE' : datetime.today().strftime("%Y/%m/%d %H:%M:%S") #크롤링된 날짜 및 시간
    })
    
    print('developer_review_answer dictionary is being made', datetime.today().strftime("%Y/%m/%d %H:%M:%S")) #개발사 리뷰 답변 딕셔너리
    for i in range(len(developer_review_answers)) :
        developer_review.append({
            'DEVELOPER_answer_date' : developer_review_answers_date[i].text,
            'DEVELOPER_answer' : developer_review_answers[i].text[len(dev)+len(developer_review_answers_date[i].text)+1:] #개발사, 날짜가 포함된 스트링 슬라이스. +1은 날짜 직후에 줄바꿈 해결하기 위함
        })
        
        
    print("data are being transform into dataframe...", datetime.today().strftime("%Y/%m/%d %H:%M:%S"))
    appdata_df = pd.DataFrame(appdata) #데이터프레임 화
    crawlreview_df = pd.DataFrame(crawlreview)
    developer_review_df = pd.DataFrame(developer_review)
    
    print("data are being saved in excel now.", datetime.today().strftime("%Y/%m/%d %H:%M:%S"))

    total_df = pd.concat([appdata_df, crawlreview_df, developer_review_df], axis = 1) #세 딕셔너리 합치기
    
    if not os.path.exists(keywordname[:len(keywordname)-6]+".xlsx") : #엑셀 파일 이름은 어플링크 파일에서 링크 부분 삭제한 이름
        with pd.ExcelWriter(keywordname[:len(keywordname)-6]+".xlsx", mode='w', engine='openpyxl') as writer : #엑셀 파일 없으면 생성
            total_df.to_excel(writer, sheet_name = name)
    else: 
        with pd.ExcelWriter(keywordname[:len(keywordname)-6]+".xlsx", mode='a', engine='openpyxl') as writer : #엑셀 파일 있으면 해당 파일 내 시트 추가
            total_df.to_excel(writer, sheet_name = name) #시트 이름은 어플 이름
    print("DONE", datetime.today().strftime("%Y/%m/%d %H:%M:%S"))



위 크롤링 함수의 데이터를 받아 엑셀에 저장하는 함수





#allist함수에 crawler함수 조함. 
def web_crawler() :
    for i in altxtread() :
        keywordname = i #링크 txt파일 이름
        f = open(i, "r")
        applists = f.readlines()
        f.close()
        for x in applists :
            crawler(x, keywordname) #크롤러로 txt파일 이름 보내주기
            time.sleep(5)



크롤링에 필요한 모든 함수를 총집합시킨 함수

참고로 위 사진의 코드들은 2개월동안 이런저런 수정이 이루어진 코드들이다.


위 코드를 작성 및 실행하며 느낀점은 크게 4가지이다.

  1. 크롤링 자체는 컴퓨터 자원을 크게 소모하지 않는다.
    • 셀레니움 사용으로 멀티프로세싱 사용이 불가능해 유의미한 차이를 얻을 수 없었다. 만약 셀레니움에 멀티프로세싱 기능을 사용할 수 있는 방법을 아신다면 꼭 연락 부탁드린다.
  2. 데이터를 순차적으로 정리 및 재구성 하는 것에는 큰 시간이 걸린다.
  3. 웹페이지를 실제로 띄우는 것보다 셀레니움의 headless 옵션을 사용하는 것이 훨씬 컴퓨터 자원을 적게 사용한다.
  4. 각 단계별로 print를 사용해 시간을 나타내는 것이 이후 디버깅에 큰 도움이 된다.

다만 약간의 보완 해야 할 점이 있었다.

먼저, 본인의 컴퓨터는 그리 고사양이 아니다.

현재 i5-6500, 16GB RAM, GTX 1060 3GB로 구성된 시스템을 사용중인데, 크롤링을 하는 과정에서 작업관리자상의 성늠탭에서 평소와 큰 차이를 보여주지 않았고 동시에 전체적인 성능저하가 없었기에 크롤링 자체는 컴퓨터 자원을 크게 소모하지 않는다고 결론내렸다.

하지만 크롤링된 데이터를 순차적으로 정리 및 재구성(짧은 리뷰, 긴 리뷰를 순서에 맞게 정리 하는 것)하는 작업과 여러 데이터를 하나로 묶는 작업은 큰 시간이 걸렸다. 일례로 케이크 라는 어플의 리뷰 약 19000개의 크롤링은 약 한시간이 걸렸지만, 엑셀에 데이터를 저장할때까지 9시간정도 걸렸다. 아무래도 작업이 순차적으로 이루어져서 그렇다고 생각된다.

크롤링과정에서 웹페이지가 제대로 보여지지 않아 리뷰 크롤링이 중간에 중단되고 다음 단계(데이터 저장 단계)로 넘어가는 경우가 많았다. 이는 한 웹페이지에 끊임없는 스크롤 다운이 지속되고 표시할 요소가 많아짐에 따라 느려짐이 발생해 일어나는 일이라 생각되었고 이를 해결하기 위해 Webdriver에 headless옵션을 적용하였다. 이후 이전과 동일한 문제는 크게 발생하지 않았다.

마지막으로 한 작업에서 다른 작업으로 넘어갈때 print에 datetime을 이용해 시간을 표기하는 것이 디버깅에 큰 도움이 되었다. 이 경우 어느 부분이 느린지, 어느 부분에서 오류가 발생했는지 등 많은 정보를 유추 할 수 있게 해주었고 완벽히 해결된 부분이 아니라면 꼬리표를 달듯 에러 트래킹이 가능하도록 해주었다. 아주 유용한 부분이었다.

위 코드 작업을 통해 15만개가 넘는 리뷰를 확보 할 수 있었다.

이제 확보한 데이터를 가지고 놀 일만 남았다.