ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python/Crawling] 네이버 플레이스(네이버 지도) 리뷰 크롤링
    Deep Learning/NLP 2021. 9. 15. 23:30

    사실 selenium이 편하니까 머릿속으로는 웬만하면 뷰숲써야지 하면서도 (selenium은 라이브러리 자체가 굉장히 무겁기도 하고 잘 막힌다는 단점이 있다.) 굳이 막히지만 않으면 동적으로 크롤링하는 버릇(?)이 있었다. 하지만 내가 적은 코드 명령들을 전혀 먹어주지 않는 네이버와 근 몇 달 간 함께하면서 뷰숲과 많이 친해졌다.

    종강하고 좀만 여유로워지면 올릴게욥.. 찬찬히 뜯어보겠음..

    크롤링하게 된 이유

    빅데이터 동아리 ADV 프로젝트로 "식당 추천 시스템"을 하게 되면서, 팀원들이 네이버/카카오/구글맵 리뷰 데이터를 각자 분배하여 크롤링하기로 했다. 사다리 탔는데 네이버가 걸렸다. 네이버는 크롤링이 자주 막힌다.. F12를 누르면 훤히 보이는 소스들을 파이썬에다가 순순히 보내주지 않는다.

    작업 환경

    Jupyter Notebook

    본격적인 크롤링에 들어가기에 앞서 주의할 점

    • 네이버 플레이스에 "XX역 XXX(식당이름)"라고 검색했을 때, 하나의 결과가 나오는 플레이스만 크롤링이 가능하다.
      • 이유 : 검색했을 때, 여러 개의 플레이스 결과가 나오면, 그 중 하나의 플레이스를 선택해서 클릭시키는 동작을 수행시켜야 해당 플레이스의 url에 접근이 가능한데, 온갖 시도를 다 해보았지만 click이 먹히지 않았다.
      • 이런 이유로, 위와 같이 검색했을 때 여러 개의 결과가 나오는 프랜차이즈 식당의 리뷰는 크롤링이 불가했다.
    • 키를 찾을 수 없다며 모든 동작을 할 수 없었기 때문에, 플레이스 명을 검색해서 해당 플레이스의 url을 뽑는 코드와 그 url에 접속해서 리뷰 데이터를 가져오는 코드를 작성했다.

    완성된 csv 및 json으로 두 단계를 살펴보면 아래와 같다.

    1단계 : 플레이스 url 뽑아오기
    2단계 : 플레이스 별 리뷰 뽑아오기

     

    크롤링 순서

    A. 플레이스 URL 크롤링
    A-1. 검색 및 검색한 플레이스에 대한 URL 추출

    B. 각 플레이스 리뷰 크롤링
    B-1. A에서 추출한 URL 접속
    B-2. 더보기 버튼 누르기
    B-3. 파싱 및 리뷰 가져오기



    A. 플레이스 URL 크롤링

    A-1. 검색 및 검색한 플레이스에 대한 URL 추출

    크롤링의 상징 BeautifulSoup과 selenium을 모두 사용할 것이다.

    import pandas as pd
    from selenium import webdriver
    from selenium.webdriver.common.keys import Keys 
    from selenium.common.exceptions import NoSuchElementException 
    
    import time
    import re
    from bs4 import BeautifulSoup 
    from tqdm import tqdm 
    
    df = pd.read_csv('원하는 플레이스 정보가 담긴 파일.csv') 
    df['naver_map_url'] = '' # 미리 url을 담을 column을 만들어줌 
    
    driver = webdriver.Chrome(executable_path=r'C:\Users\chromedriver.exe') # 웹드라이버가 설치된 경로를 지정해주시면 됩니다.

    자 이렇게 모든 준비는 끝났다. #가보자고

    for i, keyword in enumerate(df['검색어'].tolist()): 
        
        print("이번에 찾을 키워드 :", i, f"/ {df.shape[0]} 행", keyword) 
        
        try: 
            naver_map_search_url = f'https://map.naver.com/v5/search/{keyword}/place' # 검색 url 만들기 
            driver.get(naver_map_search_url) # 검색 url 접속 = 검색하기 
            time.sleep(4) # 중요
    
            cu = driver.current_url # 검색이 성공된 플레이스에 대한 개별 페이지 
            res_code = re.findall(r"place/(\d+)", cu)
            final_url = 'https://pcmap.place.naver.com/restaurant/'+res_code[0]+'/review/visitor#' 
            
            print(final_url)
            df['naver_map_url'][i]=final_url 
            
        except IndexError: 
            df['naver_map_url'][i]= ''
            print('none') 
        
        df.to_csv('url_completed.csv', encoding = 'utf-8-sig')

    (여기서 print 명령은 모두 내가 크롤링 현황을 주피터에서 확인하기 위함이다. 생략 가능 가능)
    final url에 대해서는 조금 설명이 필요할 것 같아 덧붙인다. 코드를 보면 현재 플레이스에 접속한 상태에서 크롤링을 바로 시작하는 것이 아니라, 현재 url에서 특정 res_code를 뽑아와서 새로운 final_url에 맵핑을 해주는 과정을 거친다. 아까 서두에서 언급했듯이 특정 플레이스에 접속하면 어떤 동작도 먹히지 않는 것은 물론 tag와 path조차 찾지 못한다. 그러나 새로 맵핑한 final_url로 새로 접근하면, 크롤링 명령들이 모두 수행되는 환경이 된다. 어떻게 찾았는지는 진짜.. 계속 해보다가 찾았다.. 이거 또 언제 막힐지 모른다.. 네이버는.. 강력하다..
    그래서 이 새로운 url을 플레이스마다 찾아서 append 해주고, 검색 결과가 없거나 여러 개인 경우는 IndexError로 건너뛰게 처리해주었다.

    B. 각 플레이스 리뷰 크롤링

    A에서 추출한 URL 접속 + 더보기 버튼 누르기 + 파싱 및 리뷰 가져오기

     

    일단 준비운동

    import pandas as pd 
    from selenium import webdriver 
    from selenium.webdriver.common.keys import Keys
    from selenium.common.exceptions import NoSuchElementException
    
    import time
    import re 
    from bs4 import BeautifulSoup 
    from tqdm import tqdm 
    
    # 웹드라이버 접속
    driver = webdriver.Chrome(executable_path=r'C:\Users\chromedriver.exe')
    
    # 전처리 완료한 데이터 불러오기
    # url이 없는 경우는 제거함
    df = pd.read_csv('url_completed.csv') 
    
    # 수집할 정보들 
    rating_list = []    # rating
    user_review_id = {} # user id
    review_json = {} # 리뷰 
    image_json = {} # 리뷰 이미지

    모든 준비는 끝났다.

    for i in range(len(df)): 
        
        print('======================================================') 
        print(str(i)+'번째 식당') 
        
        # 식당 리뷰 개별 url 접속
        driver.get(df['naver_map_url'][i]) 
        thisurl = df['naver_map_url'][i]
        time.sleep(2) 
        
        # 더보기 버튼 다 누를 것
        # 더보기 버튼은 10개 마다 나옴
        while True: 
            try: 
                time.sleep(1) 
                driver.find_element_by_tag_name('body').send_keys(Keys.END) 
                time.sleep(3) 
                
                driver.find_element_by_css_selector('#app-root > div > div.place_detail_wrapper > div:nth-child(5) > div:nth-child(4) > div:nth-child(4) > div._2kAri > a').click() 
                time.sleep(3) 
                driver.find_element_by_tag_name('body').send_keys(Keys.END) 
                time.sleep(1) 
                
            except NoSuchElementException: 
                print('-더보기 버튼 모두 클릭 완료-') 
                break 
        
    
        # 파싱
        html = driver.page_source 
        soup = BeautifulSoup(html, 'lxml') 
        time.sleep(1) 
        
        # 식당 구분 
        restaurant_name = df['검색어'][i]
        print('식당 이름 : '+restaurant_name) 
        
        user_review_id[restaurant_name] = {}
        review_json[restaurant_name] = {} 
        image_json[restaurant_name] = {} 
        
        try: 
            restaurant_classificaton = soup.find_all('span',attrs = {'class':'_3ocDE'})[0].text 
        
        except: 
            restaurant_classificaton = 'none'
        
        print('식당 구분 : '+restaurant_classificaton)
        print('----------------------------------------------')
        
    
        # 특정 식당에 대한 리뷰 수집
        try: 
            one_review = soup.find_all('div', attrs = {'class':'_1Z_GL'})
            review_num = len(one_review) # 특정 식당의 리뷰 총 개수 
            print('리뷰 총 개수 : '+str(review_num)) 
            
            # 모든 리뷰에 대해서 정보 수집
            for i in range(len(one_review)): 
                
                # user url
                user_url = one_review[i].find('div', attrs = {'class':'_23Rml'}).find('a').get('href')
                print('user_url = '+user_url) 
                
                # user url로부터 user code 뽑아내기
                user_code = re.findall(r"my/(\w+)", user_url)[0]
                print('user_code = '+user_code) 
                
                # review 1개에 대한 id 만들기 
                res_code = re.findall(r"restaurant/(\d+)", thisurl)[0] 
                review_id = str(res_code)+"_"+user_code
                print('review_id = '+review_id) 
                
                # rating, 별점 
                rating = one_review[i].find('span', attrs = {'class':'_2tObC'}).text
                print('rating = '+rating) 
                
                ## 주의
                ## 사진 리뷰 유무에 따라 날짜 파싱코드가 다르기 때문에
                ## case 별로 처리해줘야 함
                ## ('span', attrs = {'class':'_3WqoL'}) 
                ## 사진 없는 경우 : 총 6개 중 4번째 
                ## 사진 있는 경우 : 총 5개 중 3번째 
                 
                # date 
                # 사진 리뷰 없음 
                if len(one_review[i].find_all('span', attrs = {'class':'_3WqoL'})) == 5: 
                    date = one_review[i].find_all('span', attrs = {'class':'_3WqoL'})[2].text 
    
                elif len(one_review[i].find_all('span', attrs = {'class':'_3WqoL'})) == 6: 
                    date = one_review[i].find_all('span', attrs = {'class':'_3WqoL'})[3].text
    
                else: 
                    date = ""
                
                print('date = '+date) 
    
                
                # review 내용
                try: 
                    review_content = one_review[i].find('span', attrs = {'class':'WoYOw'}).text
                except: # 리뷰가 없다면
                    review_content = "" 
                print('리뷰 내용 : '+review_content) 
                
    
                # image 내용
                sliced_soup = one_review[i].find('div', attrs = {'class':'_1aFEL _2GO1Q'}) 
                
                if (sliced_soup != None): 
                    sliced_soup = sliced_soup.find('div',attrs={'class':'dRZ2X'}) 
                    
                    try: 
                        img_url = 'https://search.pstatic.net/common/?autoRotate=true&quality=95&type=l&size=800x800&src='+re.findall(r'src=(.*jpeg)', str(sliced_soup))[0] 
                    
                    except : 
                        if (len(re.findall(r'src=(.*jpg)', str(sliced_soup)))!= 0): 
                            img_url = 'https://search.pstatic.net/common/?autoRotate=true&quality=95&type=l&size=800x800&src='+re.findall(r'src=(.*jpg)', str(sliced_soup))[0]
                        elif (len(re.findall(r'src=(.*png)', str(sliced_soup)))!= 0): 
                            img_url = 'https://search.pstatic.net/common/?autoRotate=true&quality=95&type=l&size=800x800&src='+re.findall(r'src=(.*png)', str(sliced_soup))[0]
                        else : 
                            img_url = ""
                
                else: 
                    img_url = "" 
                
                
                print('이미지 url : '+img_url) 
                print('----------------------------------------------') 
                print('\n') 
                
                
                # 리뷰정보 
                # user_review_id
                user_review_id[restaurant_name][user_code] = review_id 
                
                # review_json 
                review_json[restaurant_name][review_id] = review_content 
                
                # image_json 
                image_json[restaurant_name][review_id] = img_url 
                
                # rating_df_list 
                naver_review = user_code, restaurant_name, rating, date
                rating_list.append(naver_review) 
    
        # 리뷰가 없는 경우        
        except NoSuchElementException: 
            none_review = "네이버 리뷰 없음" 
            print(none_review)
            review_num = 0 
            
            # 리뷰정보 = restaurant_name, restaurant_classification, review_num, none_review 
            
            # rating_df_list
            naver_review = user_code, restaurant_name, none_review, none_reivew
            rating_list.append(naver_review)
    
    print('\n')

    여기서 모든 print 명령은 크롤링 현황을 주피터에서 확인하기 위함. 생략 가능.


    위 코드를 수행했을 때 결과창은 아래와 같음.


    csv와 json 저장으로 마무리

    # dataframe 저장 및 csv 저장
    rating_df = pd.DataFrame(rating_list) 
    rating_df.columns = ['UserID','ItemID','Rating','Timestamp'] 
    rating_df.to_csv('rating9.csv', encoding='utf-8-sig') 
    
    # json 저장
    import json 
    
    file_path = "./user_review_id.json"
    with open(file_path,'w') as outfile: 
        json.dump(user_review_id,0 outfile) 
    
    file_path = "./review.json" 
    with open(file_path,'w') as outfile: 
        json.dump(review_json, outfile) 
        
    file_path = "./image.json" 
    with open(file_path,'w') as outfile: 
        json.dump(image_json, outfile)

     

    왜 이렇게 xpath랑 css를 섞어썼냐 물으시면, 일단 저는 xpath를 선호하고 웬만하면 이걸로 해결하려 합니다. 그런데 가끔 안먹힐 때가 있어서 css를 종종 씁니다.


    도움이 되었으면 좋겠습니다

     

    'Deep Learning > NLP' 카테고리의 다른 글

    [Python/Crawling] 네이버 증권 주식 뉴스 크롤링  (0) 2021.10.29
Designed by Tistory.