Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

짱이 될거야

2022-09-19: Python 네이버 쇼핑몰 후기 웹 크롤링 2탄 본문

Today I Learned

2022-09-19: Python 네이버 쇼핑몰 후기 웹 크롤링 2탄

jeong57 2022. 9. 19. 23:09

저번 크롤링 코드는 구글링해서 찾은 코드를 그대로 따라한 것이었는데, 다른 사이트에서 여러 번 크롤링을 하면서 코드를 많이 수정해야 했다.

2022.09.06 - [Today I Learned] - 2022-09-06: Python 네이버 쇼핑몰 후기 웹 크롤링 1탄

이번 크롤링 2탄은 잘못된 부분을 수정하고 일어날 수 있는 오류들을 모아둔 것을 소개한다.

 

 

첫 번째 문제점. Pagination error

Chrome WebDriver를 사용하면 사용자가 클릭하는 것과 똑같다고 생각하면 된다.

쇼핑몰 사이트마다 pagination 구조가 다른데, 현재 예시 쇼핑몰 사이트의 경우에는 1~10까지의 버튼으로 구성돼 있다.

이 때 주의할 것은 '이전', '이후' 버튼이 숨어져 있다는 것이다.

10페이지까지는 '이전' 버튼은 숨겨져 있고 '이후' 버튼만 보인다. 그러다 11페이지부터는 '이전' 버튼과 '이후' 버튼이 함께 보이면서 XPath 또한 달라진다.

네이버는 10페이지에서 '다음' 버튼을 누르면 11 페이지로 이동하지만, 11페이지에서 '다음' 버튼을 누르면 21페이지로 이동한다. 따라서 1~10까지는 버튼을 차례대로 누르다가 11페이지에 가고 싶을 때는 '다음' 버튼을, 12페이지부터 20페이지까지 차례대로 누르다가 21페이지로 가고 싶을 때는 '다음' 버튼을 누르는 방식으로 이동한다.

따라서 페이지를 전환하는 코드는 아래와 같이 수정되어야 한다.

마지막 페이지까지 가면 try~except 문 덕분에 자동으로 종료된다.

if page<11:#page10
    try: #리뷰의 마지막 페이지에서 error발생
        page +=1
        next_page=d.find_element_by_xpath('/html/body/div/div/div[2]/div[2]/div[2]/div[3]/div[6]/div[3]/a['+str(page)+']').click() 
    except: break #리뷰의 마지막 페이지에서 process 종료

else : 
    try: #page11부터
        page+=1
        if page%10==0:  # 현재 19페이지, 다음 20페이지(=page) 예정일 경우 '이후' 버튼 바로 앞에 있는 버튼을 누른다
            next_page=d.find_element_by_xpath('/html/body/div/div/div[2]/div[2]/div[2]/div[3]/div[6]/div[3]/a[11]').click()
        elif page%10==1:    # 현재 20페이지, 다음 21페이지(=page) 예정일 경우 '이후' 버튼을 누른다
            next_page=d.find_element_by_xpath('/html/body/div/div/div[2]/div[2]/div[2]/div[3]/div[6]/div[3]/a[12]').click()
        else:   # 그 외: 현재 11페이지, 다음 12페이지(=page) 예정일 경우, a[3]을 눌러야 한다.
            next_page=d.find_element_by_xpath('/html/body/div/div/div[2]/div[2]/div[2]/div[3]/div[6]/div[3]/a['+str(page%10+1)+']').click()
    except: break

쇼핑몰마다 Pagination이 달라 헷갈릴 때는 직접 버튼을 하나씩 눌러가면서 규칙을 파악하면 된다.

 

예외적인 케이스로 처음에는 1~6까지 숫자와 '다음' 버튼만 보이고, 6을 누르는 순간 3~12(예를 들어)까지 보이는 경우도 있었다. 이런 경우에는 대부분 1 페이지 버튼만 눌러져 있다면 그 다음부터는 2, 3, ... 페이지 버튼을 누를 필요 없이 '다음' 버튼을 누르면 넘어갔다.

참고로 이때는 XPath 대신 CSS Selector를 활용해 class name 등으로 가져오는 게 가장 확실하다.

# css selector를 활용한 크롤링 예시
review = top_review.find_element_by_css_selector('div.YEtwtZFLDz > span._3QDEeS6NLn').text.strip().replace("\n", " ")

 

두 번째 문제점. 리뷰 제목

기존 코드에서 리뷰를 받아올 때, 아래와 같은 코드를 사용했다.

쇼핑몰마다 다르지만, 현재 사용하는 쇼핑몰의 경우 리뷰가 제목과 내용으로 나뉘어져 있고 제목은 내용에서 일부분을 발췌한 것이다.

따라서 똑같은 내용이 두 번 들어오기 때문에 쇼핑몰 리뷰를 가져올 때 div 단이 아니라 그 밑에 있는 p 태그에서 값을 가져와야 한다.

# 기존 코드: 수정 필요
review=d.find_element_by_xpath('/html/body/div/div/div[2]/div[2]/div[2]/div[3]/div[6]/ul/li['+str(j)+']/div[2]/div[1]').text
# 새로운 코드: 완성본
review=d.find_element_by_xpath('/html/body/div/div/div[2]/div[2]/div[2]/div[3]/div[6]/ul/li['+str(j)+']/div[2]/div[1]/p').text

 

 

추가적으로 알아두면 좋은 것

1. 후기에 '재구매' 혹은 'BEST' 등 태그가 달려있는 경우

간혹 쇼핑몰 리뷰를 보면 아래와 같이 '재구매', 'BEST' 혹은 '한달 사용후기'와 같은 태그가 달려있는 것을 볼 수 있다.

이런 경우는 아주 까다로운데, '재구매' 태그가 있을 경우와 없을 경우 후기 내용의 XPath가 달라지기 때문이다.

그것들을 일일이 고려하기는 힘들고, 따라서 이럴 때는 CSS Selector를 활용하면 좋다.

그 중에서도 내가 찾은 방법은 다음과 같다.

1. 일단 제일 먼저 각 리뷰를 포괄하는 태그를 CSS Selector로 가져온다. 거의 99%의 확률로 후기 리스트 구조는 ul > li로 되어 있고 여기서 li 태그를 가져오면 된다.

top_review = d.find_element_by_css_selector('ul.TsOLil1PRz > li:nth-child('+str(j)+')')

2. 태그를 제외한 리뷰의 주소를 CSS Selector를 활용해 가져온다. (굳이 top_review를 설정해서 가져오는 이유는, 같은 class name을 가진 다른 태그가 있을 수도 있기 때문에 경로를 확실하게 알려주기 위해서이다.)

review = top_review.find_element_by_css_selector('div.YEtwtZFLDz > span._3QDEeS6NLn')\
.text.strip().replace("\n", " ")

 

2. 크롤링이 도중에 멈추는 경우

크롤링 코드에 문제가 없는데 어떨 때는 4페이지까지 하고 멈추고, 어떨 때는 10페이지까지 문제없이 크롤링 될 수 있다. (혹은 나는 안되는데 다른 사람은 되는 경우)

그럴 때는 WebDriver 화면에서 페이지 버튼이 어딘가에 가려져 있는지 확인하면 된다.

앞서 말했듯이 크롤링은 사용자 대신 클릭하는 것이기 때문에 스크롤이 지나치게 돼서 버튼이 가려져 있다면 페이지를 이동하지 못할 것이고, 당연히 멈추게 된다.

이 경우 해결 방법은 세 가지가 있다.

첫 번째는 가장 단순하지만 귀찮은 방법인데, 계속해서 화면을 지켜보다가 버튼이 가려졌을 때 스크롤을 직접 조정해주는 것이다. (이렇게 할 거면 굳이 크롤링을 할 필요가 없다)

두 번째스크롤 하는 방식을 바꾸는 것이다. 이 부분에 대해서는 크롤링 1탄 마지막 부분에도 짧게 언급되어 있다. 하지만 두 번째 방식을 권하지는 않는데, 그 이유는 스크롤이 끝까지 내려가서 높은 확률로 페이지 버튼이 가려지거나 후기 페이지 버튼이 아니라 Q&A 버튼 등 다른 것이 눌릴 수도 있기 때문이다.

# 첫 번째 방식
ELEMENT = d.find_element_by_xpath('/html/body/div/div/div[2]/div[2]/div[2]/div[3]/div[6]/ul/li['+str(j)+']/div[2]/div[1]')
d.execute_script("arguments[0].scrollIntoView(true);", ELEMENT)     

# 두 번째 방식
d.execute_script("window.scrollTo(0, document.body.scrollHeight)")

세 번째 방법은 스크롤 시에 보여지는 태그를 바꿔보는 것이다. (가장 추천하는 방법)

이 방법을 사용하면 최종 스크롤이 멈추는 위치가 항상 동일하도록 맞출 수 있기 때문에 가장 쉽고 안전하다.

1탄에서 따라했던 코드는 ELEMENT를 리뷰 내용으로 설정하고, 다음 스크롤을 내릴 때 그만큼만 내렸다. 이 코드는 후기에 '재구매', 'BEST' 등의 태그가 없을 때 사용하는 것을 권장한다.

2탄에서 새롭게 구성한 코드는 ELEMENT를 각 리뷰를 포괄하는 li 태그로 설정한다. 이 코드는 후기에 '재구매', 'BEST' 등 태그가 있을 때 사용하는 것을 권장한다.

# 1탄 코드
ELEMENT = d.find_element_by_xpath('/html/body/div/div/div[2]/div[2]/div[2]/div[3]/div[6]/ul/li['+str(j)+']/div[2]/div[1]')
d.execute_script("arguments[0].scrollIntoView(true);", ELEMENT)

# 2탄 코드
ELEMENT = d.find_element_by_css_selector('ul.TsOLil1PRz > li:nth-child('+str(j)+')')
d.execute_script("arguments[0].scrollIntoView(true);", ELEMENT)

 

3. 크롤링 개수 제한

네이버 쇼핑몰 후기의 경우, 크롤링할 수 있는 개수에 제한이 있다.

1. https://smartstore.naver.com/... : 이런 형식의 사이트는 최대 2만개까지 후기 크롤링이 가능하다. 그 이상 넘어가게 되면 다음 페이지로 넘어가는 것이 아니라 QnA 사이트, 혹은 블로그 등 다른 url로 이동한다.

2. https://search.shopping.naver.com/... : 예시로 활용했던 LG전자 쇼핑몰 사이트처럼, 여러 판매처를 모아둔 형식의 사이트는 최대 2천개까지 후기 크롤링이 가능하다. 2,000건을 초과하면 사이트에서 후기를 더 이상 보여주지 않는다. 이는 크롤링을 막는 것이 아니라 아예 열람 자체를 불가능하게 해둔 것이다. 따라서 직접 페이지를 변경해도 100 페이지까지만 클릭이 가능하다.

후기 2,000건을 초과해서 보고자 하면 다음과 같은 alert 문구가 뜬다.

 

4. 크롤링 도중 멈추면 저장 안됨

크롤링을 하다가 도중에 ctrl+c를 눌러서 중지시키는 경우가 있을 수 있다.

만약 csv 파일에 크롤링 결과를 저장하고 있었다면 크롤링 하던 값들이 하나도 저장되지 않는다.

이것 때문에 내가 크롤링을 잘못한 것인지 한참 찾아봤는데 알았다면 시간을 낭비하지 않을 수 있었을 것이다.

 


최종 변경 크롤링 코드

from selenium import webdriver
from bs4 import BeautifulSoup
from time import sleep
import requests
import pandas as pd
import warnings
warnings.filterwarnings('ignore')

name=['LG 에어로타워']
category=['별점']


#LG 에어로타워 후기
ns_address="https://search.shopping.naver.com/catalog/30128278618?cat_id=50002543&frm=NVSCPRO&query=%EC%97%90%EC%96%B4%EB%A1%9C%ED%83%80%EC%9B%8C&NaPm=ct%3Dl0ksn0vc%7Cci%3D5bbd25c0299ce5dbcb72ff2b1d41488ebd6d52ce%7Ctr%3Dsls%7Csn%3D95694%7Chk%3D87194ce8ced4cb2b52968022b8eb9db67602d12e"
#xpath
shoppingmall_review="/html/body/div/div/div[2]/div[2]/div[2]/div[3]/div[6]/ul"


header = {'User-Agent': ''}
d = webdriver.Chrome('chromedriver.exe') # webdriver = chrome
d.implicitly_wait(3)
d.get(ns_address)
req = requests.get(ns_address,verify=False)
html = req.text 
soup = BeautifulSoup(html, "html.parser")
sleep(2)

#쇼핑몰 리뷰 보기
d.find_element_by_xpath(shoppingmall_review).click()
sleep(2)

element=d.find_element_by_xpath(shoppingmall_review)
d.execute_script("arguments[0].click();", element)
sleep(2)

def add_dataframe(name,category,reviews,stars,cnt):  #데이터 프레임에 저장
    #데이터 프레임생성
    df1=pd.DataFrame(columns=['type','category','review','star'])
    n=1
    if (cnt>0):
        for i in range(0,cnt-1):
            df1.loc[n]=[name,category,reviews[i],stars[i]] #해당 행에 저장
            i+=1
            n+=1
    else:
        df1.loc[n]=[name,category,'null','null']
        n+=1    
    return df1

# 리뷰 가져오기
d.find_element_by_xpath(shoppingmall_review).click() #스크롤 건드리면 안됨
name_=name[0]
category_=category[0]
reviews=[]
stars=[]
cnt=1   #리뷰index
page=1


# 1번 코드(스크롤 시 보이는 화면 제한)
while True:
    j=1
    print ("페이지", page ,"\n") 
    sleep(2)
    while True: #한페이지에 20개의 리뷰, 마지막 리뷰에서 error발생
        try:
            star=d.find_element_by_xpath('/html/body/div/div/div[2]/div[2]/div[2]/div[3]/div[6]/ul/li['+str(j)+']/div[1]/span[1]').text
            review=d.find_element_by_xpath('/html/body/div/div/div[2]/div[2]/div[2]/div[3]/div[6]/ul/li['+str(j)+']/div[2]/div[1]/p').text

            stars.append(star)
            reviews.append(review)

            if j%2==0: #화면에 2개씩 보이도록 스크롤
                ELEMENT = d.find_element_by_xpath('/html/body/div/div/div[2]/div[2]/div[2]/div[3]/div[6]/ul/li['+str(j)+']/div[2]/div[1]')
                d.execute_script("arguments[0].scrollIntoView(true);", ELEMENT)       
            j+=1
            print(cnt, review ,star, "\n")
            cnt+=1 
        except: break
            
    sleep(2)
    
    if page<11:#page10
        try: #리뷰의 마지막 페이지에서 error발생
            page +=1
            next_page=d.find_element_by_xpath('/html/body/div/div/div[2]/div[2]/div[2]/div[3]/div[6]/div[3]/a['+str(page)+']').click() 
        except: break #리뷰의 마지막 페이지에서 process 종료
        
    else : 
        try: #page11부터
            page+=1
            if page%10==0:
                next_page=d.find_element_by_xpath('/html/body/div/div/div[2]/div[2]/div[2]/div[3]/div[6]/div[3]/a[11]').click()
            elif page%10==1:
                next_page=d.find_element_by_xpath('/html/body/div/div/div[2]/div[2]/div[2]/div[3]/div[6]/div[3]/a[12]').click()
            else:
                next_page=d.find_element_by_xpath('/html/body/div/div/div[2]/div[2]/div[2]/div[3]/div[6]/div[3]/a['+str(page%10+1)+']').click()
        except: break

df4=add_dataframe(name_,category_,reviews,stars,cnt)

 

배운 것들

  • 역시 다른 사람이 짠 코드를 따라하는 것보다 내가 직접 코드를 바꾸고 그 과정에서 여러 문제를 해결하면서 익히는 것이 그것을 배우는 가장 빠르고 최고인 방법이다.
  • 여러 사이트들을 크롤링 해보면서 각 사이트마다의 pagination 방식과 태그 주소 규칙들을 빠르게 파악할 수 있었다.
  • 대략적인 틀에서 크게 벗어나지 않아서 address 주소나 태그의 인덱싱(예를 들면 div[2])과 같은 것들만 수정하면 거의 모든 (네이버) 쇼핑몰 사이트의 후기를 크롤링할 수 있을 것이라 자신한다.
  • 여러 판매처를 모아둔 쇼핑몰 사이트에서는 후기가 2천개까지밖에 볼 수 없다는 것과, 일반 쇼핑몰에서는 후기를 2만개까지만 모을 수 있다는 것을 알게 되기까지 정말 많은 시도를 했다. 사소해보여도 이걸 직접 발견한 사람이 많이 없을 것이라는 추측에 기분이 좋다. (실제로 2천개까지 보여지는 것에 당황해서 구글링해봤지만 아무도 알려주지 않았다..)
Comments