이걸 복사해와서 driver.get(url) 안에 넣겠습니다. 아래 코드를 실행하면 다나와 공기청정기 카테고리로 이동합니다.
# 필요한 패키지 가져오기
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from bs4 import BeautifulSoup
import time
import math
import pandas as pd
import pickle
# 다나와 공기청정기 카테고리로 이동
url = "https://prod.danawa.com/list/?cate=10352481&searchOption=/innerSearchKeyword="
driver.get(url)
1️⃣ 공기청정기 인기3사인 LG전자, 삼성전자, 위닉스만 선택해서 제품을 필터링하겠습니다.
경쟁사를 정해놓고 제품정보를 크롤링하는 상황으로 가정하겠습니다.
현재 분석타겟 제조사는 LG전자, 삼성전자, 위닉스 3사입니다. 제조사 필터에서 이 3가지만 선택해야합니다.
어느정도 "지속가능한 코드 만들기"가 목표니까요, 나중에 타겟 제조사를 바꿔서 다시 분석할 경우를 대비해서, 제조사 이름을 입력하면 → 체크박스를 클릭해주는 흐름으로 코드를 구성하고 싶어요.
HTML을 살펴보고 [제조사별] 부분의 XPath를 카피해서 find_elements 메소드로 내용을 가져와봤습니다.
아래와 같이 내용을 가져올 수는 있지만 리스트 길이가 1입니다. 이 경로로 가져오면 리스트로 가져온게 소용이 없고.. 다 합쳐져서 가져와지네요.
HTML을 다시 살펴볼게요. 하위에 li class="sub item" 이 5개 존재하고, 이게 각각 제조사별 체크박스에 해당하네요!
이 부분의 XPATH를 카피하면 끝에 li[숫자]가 있습니다. //*[@id="dlMaker_simple"]/dd/ul[1]/li[1] 이 [숫자] 부분을 지우면 리스트를 제대로 가져올 수 있을 것 같아요.
제조사 리스트 가져오기 성공! 리스트 길이가 5니까 제조사별로 각각 떼어서 가져오는걸 성공했어요~
이 리스트 요소 중에서 'LG전자', '삼성전자', '위닉스'에 해당하는 경로를 각각 찾아서, 체크박스를 클릭할 수 있어요.
이제 이 리스트 요소 중에서 'LG전자', '삼성전자', '위닉스'에 해당하는 각 경로를 가져와서, 체크박스를 클릭할 수 있습니다.
에어컨, 세탁기 등 다른 카테고리 페이지에서도 이부분은 HTML구성이 동일합니다! 같은 코드로 체크박스 클릭이 가능하네요. 나중에 다른 제품 크롤링할때도 같은 코드로 써먹을 수 있을 것 같아요!
위에서 밝혔듯이 제조사 이름을 넣으면 → 체크박스를 클릭하는 함수를 만들어서 크롤링 코드 맨 앞에 넣어줄게요.
이러면 나중에 분석타겟 제조사를 바꿔도, 제조사 이름만 바꿔넣으면 문제없이 크롤링할 수 있겠죠. 또 코딩할 필요 없이요. 혹은 코드가 초면인 사람에게, 코드를 주면서 다나와 링크랑 경쟁사 이름 넣어서 실행하면 돼요~ 라고 전달할 수 있을거에요. 모든건 나중의 나자신을 위한 노가다와 준비입니다..
def manufacturer_checkbox(target_list): # 함수 입력값: 타겟 제조사 리스트
# 제조사별 부분 XPath 리스트로 가져오기
manufacturer_list = driver.find_elements(By.XPATH, '//*[@id="dlMaker_simple"]/dd/ul[1]/li')
# 타겟 제조사 체크박스 하나씩 클릭하기
for target in target_list:
for m in manufacturer_list:
if target in m.text:
m.click()
break
print(str(target_list), '선택 완료')
2️⃣ 이제 제품정보를 크롤링해오려면, 페이지를 하나씩 넘겨야합니다.
총 몇 페이지인지 알아야겠죠. 아래 캡처에서 빨간네모친 총 제품 개수를 가져와서, 총 페이지 수 num_pages 변수를 만들게요.
# 모델 개수
num_models = driver.find_element(By.CLASS_NAME, 'list_num').text.strip().replace('(', '').replace(')', '')
# 페이지 개수 (페이지당 보이는 제품은 30개)
num_pages = math.ceil(int(num_models) / 30))
반복문 돌리면서 지금 페이지 내용 긁어온 다음, 다음 페이지 번호를 눌러주는 흐름으로 가야겠습니다.
지금 몇번째 페이지로 넘겨야하는지 나타내는 page 변수를 두고, num_page 와 같아질때까지 while문을 돌릴게요.
주의할 점! 11, 21, 31..번째 페이지로 넘길 때는 그냥 다음 페이지가 아니라, 다음 "화살표 버튼"을 눌러줘야 합니다. 이 부분이 아래코드에서 if문에 해당합니다. (참고-이상하게 css selector로 가져오지 않으면 에러가 나더라구요..)
하나더 주의! 한 페이지 끝날때마다 time.sleep으로 잠시 "대기"가 필요합니다. 잠깐 로딩을 기다려주지 않으면 요소를 찾을 수 없다는 에러가 계속나왔어요. (참고-driver.implicitly_wait도 안통했음.)
# 페이지 변수 생성, 시작은 1페이지부터
page = 1
# page에 1씩 더하면서, 전체 페이지 수와 같아지기 전까지 반복
while page < num_pages:
page += 1
### 페이지 크롤링 코드는 여기에 추가될 예정 ###
# 11,21,31..번째 페이지인 경우 [다음 페이지] 버튼 클릭
if page%10 == 1:
driver.find_element(By.CSS_SELECTOR, '#productListArea > div.prod_num_nav > div > a').click()
# 나머지 경우는 page에 해당하는 페이지 클릭
else:
# 페이지 넘기는 부분 경로 통째로 가져와놓고
page_nums = driver.find_element(By.XPATH, '//*[@id="productListArea"]/div[4]/div')
# 다음 페이지 링크를 클릭
page_nums.find_element(By.LINK_TEXT, str(page)).click()
# 에러나지 않도록 한페이지마다 잠시 대기
time.sleep(2)
3️⃣ 이제 페이지마다 제품정보를 크롤링해와서, DataFrame에 저장해주겠습니다.
한 페이지마다 데이터는 BeautifulSoup으로 전부 긁어오겠습니다. 체크박스랑 페이지 넘길때는 Selenium이 일하고, 한 페이지 안에서는 BeautifulSoup이 일하는 구성이에요.
3️⃣-(1). 가격정보 크롤링은 준비가 필요합니다!
그 전에 문제가 하나 있어요! 아래 캡처를 보시면.. 가격(최저가) 정보가 문제입니다. 가격 정보가 1개인 제품이 있고, 2개 3개 여러개인 제품이 있습니다. 가격 정보만 먼저 따로 살펴봐야 겠어요.
제품마다 가격정보가 몇개인지 가져와보고, 가장 많은 것 제품을 기준으로 크롤링해오려고 합니다. 예를들어 가장 많은 가격정보를 가진 제품이 가격 3개였다면.. 엑셀에서 가격 칼럼을 3개 만들고 싶다는 거죠. 가격이 1개거나 2개인 제품들은 그냥 셀을 비워두면 되잖아요.
이제 모든 페이지의 모든 제품을 돌려보면서 "가격정보가 가장 많은 제품은 가격정보가 몇개" 인지 알아봐야 합니다. 아래와 같이.. 위에서 작성했던 페이지 넘기는 반복문 중간에, 가격 리스트 가져오는 부분을 추가하고, 최댓값을 돌려받을게요.
그리고 한페이지 마다 페이지에 있는 모든 제품 데이터들을 products 변수에 넣고 이걸 딕셔너리에 저장해줄게요. 사실상 크롤링은 여기서 끝입니다! 이제 딕셔너리에서 데이터를 가져다가 필요한 정보만 뽑아쓰면 됩니다.
# 가격정보 저장할 빈 리스트 생성
numofprices_list = []
# 페이지별 소스 저장할 빈 딕셔너리 생성
soup_dict = {}
page = 1
# page에 1씩 더하면서, 전체 페이지 수와 같아지기 전까지 반복
while page < num_pages:
# 제품 정보들 전부 가져오기
soup = BeautifulSoup(driver.page_source)
products = soup.select('div.prod_main_info')
time.sleep(1)
# 페이지별 소스 딕셔너리에 저장해두기
soup_dict[page] = products
# 제품하나씩 돌리면서
for i in range(len(products)):
try: # 가격정보 개수 가져오기
numofprices_list.append(len(products[i].select('div.prod_pricelist li')))
except:
numofprices_list.append('')
time.sleep(1)
print(page, "페이지 완료!")
page += 1
# 11,21,31..번째 페이지로 넘기는 경우 [화살표] 버튼 클릭
if page%10 == 1:
driver.find_element(By.CSS_SELECTOR, '#productListArea > div.prod_num_nav > div > a').click()
# 나머지 경우는 page에 해당하는 페이지 클릭
else:
# 페이지 넘기는 부분 경로 통째로 가져와놓고
page_nums = driver.find_element(By.XPATH, '//*[@id="productListArea"]/div[4]/div')
# 다음 페이지 링크를 클릭
page_nums.find_element(By.LINK_TEXT, str(page)).click()
# 에러나지 않도록 한페이지마다 잠시 대기
time.sleep(2)
# 마지막 페이지 소스 가져오고 마무리
soup = BeautifulSoup(driver.page_source)
products = soup.select('div.prod_main_info')
soup_dict[page] = products
print(page, "페이지 완료!")
print("<<< 크롤링 완료입니다! >>>")
print("가격정보가 가장 많은 경우는", max(numofprices_list), "개 입니다!")
3️⃣-(2). 저장해둔 페이지 소스에서 쓸모있는 제품정보만 가져와서, DataFrame으로 합칠게요.
위 코드로 저장해둔 딕셔너리에서, 제품정보를 하나씩 뽑아오면서.. 이제 드디어 쓸모있는 결과물을 만들어줄 차례입니다!
이 부분에선 HTML을 하나씩 살펴보면서.. 어디에 어떤정보가 있는지 직접 가져와야합니다.
본격적으로 BeatifulSoup을 사용하게 되는데, select는 여러개를, select_one은 딱 한개만 찾아오는 메소드입니다. 각자 Selenium에서 find_elements, find_element와 비슷하죠?
select_one으로 가져온 결과물은 날것 그자체이니, 마지막에 .text.strip()을 붙여서 텍스트만 남기는것 잊지마세요!
위와 같이, 제품1개마다 모델이름, 출시시기, 헤드메시지, 스펙, 리뷰평점, 리뷰개수, 페이지링크, 가격정보를 가져오겠습니다.
이제 for문을 돌리면서, 위 정보들을 리스트에 하나씩 append하고, 마지막에 DataFrame으로 합쳐줄거에요.
아까 뽑아둔 가격정보 최댓값X2 개의 리스트를 만들어서 가격정보도 같이 붙여줄거에요. 이건 코드랑 결과물 참고해주세요. priceinfo_list_set, price_list_set에 해당합니다.
참고로, 제품에 따라 정보가 없을수도 있으니 try-except를 달아줬습니다. 예를들어 n번째 제품에 헤드메시지가 있으면 리스트에 추가하고, 없으면 공백을 추가할 것입니다.
def crawling_danawa(soup_dict, max_numofprices_list):
# 모델이름, 출시시기, 헤드메시지, 스펙, 리뷰평점, 리뷰개수, 페이지링크 가져올 빈 리스트 생성
name_list, hm_list, spec_list, release_list, star_list, review_list, link_list = [], [], [], [], [], [], []
# 가격정보 리스트셋 (ex. 1위 일반구매, 2위 추가필터 포함..)
priceinfo_list_set = []
# 최저가 리스트셋 (ex. 100,000원, 120,000원..)
price_list_set = []
# 최댓값만큼 빈 리스트 생성 (이중리스트)
for i in range(max_numofprices_list):
priceinfo_list_set.append([])
price_list_set.append([])
# 딕셔너리에 저장해둔 페이지 하나씩 돌리기
for key in range(len(soup_dict)):
products = soup_dict[key+1]
# 제품마다 하나씩 돌리기
for i in range(len(products)):
# 모델 이름
try:
name_list.append(products[i].select_one('p.prod_name a').text.strip())
except:
name_list.append('')
# 모델 출시시기
try:
release_list.append(products[i].select_one('div.prod_sub_info dd').text.strip())
except:
release_list.append('')
# 헤드 메시지
try:
hm_list.append(products[i].select_one('div.prod_intro').text.strip())
except:
hm_list.append('')
# 모델 스펙
try:
spec_list.append(products[i].select_one('div.spec_list').text.strip())
except:
spec_list.append('')
# 리뷰 평점 & 개수
try:
star_list.append(products[i].select_one('div.cnt_star').text.strip().replace('점', ''))
review_list.append(products[i].select_one('div.prod_sub_info a').text.strip())
except:
star_list.append('')
review_list.append('')
# 제품페이지 링크
try:
thumbnail = products[i].select_one('div.thumb_image a')
link_list.append(thumbnail['href'])
except:
link_list.append('')
# 가격정보
pricelist_raw = products[i].select('div.prod_pricelist li')
for p in range(max_numofprices_list):
try:
priceinfo_list_set[p].append(
pricelist_raw[p].select_one('div.over_preview').text.strip())
except:
priceinfo_list_set[p].append('')
for p in range(max_numofprices_list):
try:
price_list_set[p].append(pricelist_raw[p].select_one('p.price_sect a').text.strip())
except:
price_list_set[p].append('')
# DafaFrame 저장
df_result = pd.DataFrame({
'제품모델명' : name_list, '출시시기' : release_list,
'리뷰평점' : star_list, '리뷰개수' : review_list,
'헤드메시지' : hm_list, '스펙' : spec_list, '링크URL' : link_list
})
for p in range(max_numofprices_list):
df_result['가격정보_{}'.format(str(p+1))] = priceinfo_list_set[p]
df_result['최저가_{}'.format(str(p+1))] = price_list_set[p]
return df_result
4️⃣ 문제없이 크롤링 완료됐는지 확인한 다음, 크롤링 함수를 최종 정리해보겠습니다.
다나와에 보이는 제품은 총 375개 였는데, 이상하게도 결과 데이터프레임은 388행이라고 나오네요. 중복 수집된 모델이 있는지 확인해봐도.. 중복은 없습니다. 아무래도 더미데이터가 들어온 것 같으니 엑셀로 내보내서 볼게요.
이상하게 예전부터 다나와 크롤링하면 이런 더미데이터가 딸려들어오더라구요. 중간중간 광고때문인걸까요? 그런것 치곤 가격데이터가 있는 것이 좀 이상합니다. 아무튼 이걸 빼니 375개 딱 맞네요!
살펴보니 출시시기가 비어있으면 더미에 해당합니다. 이걸 기준으로 걸러주는게 좋겠네요.
4️⃣-(1) [함수1] 크롬드라이버 실행부터 페이지 넘기면서 크롤링까지
이제 위에서 작성했던 코드들을 함수 2개로 묶겠습니다.
crawling_danawa_get_source: 페이지 넘기면서 제품정보 데이터들을 긁어오는 함수입니다. - 입력값은 다나와 링크, 타겟 제조사 리스트입니다. - 출력값은 크롤링 페이지 정보 저장된 딕셔너리, 가격정보 최댓값입니다.
# 필요한 패키지 가져오기
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from bs4 import BeautifulSoup
import time
import math
import pandas as pd
import pickle
def crawling_danawa_get_sources(url, target_list):
# 함수 입력값: 다나와 링크, 타겟 제조사 리스트
##### 드라이버 실행 ~ 타겟 제조사 선택까지 #####
# webdirver 설정 및 실행
my_options = webdriver.ChromeOptions()
my_options.add_argument('--window-size=1920,1080')
my_options.add_argument("disable-gpu")
driver = webdriver.Chrome(options=my_options)
driver.implicitly_wait(1)
# 다나와 제품카테고리 페이지 접속
driver.get(url)
driver.implicitly_wait(3)
# 제조사별 부분 XPath 리스트로 가져오기
manufacturer_list = driver.find_elements(By.XPATH, '//*[@id="dlMaker_simple"]/dd/ul[1]/li')
# 타겟 제조사 체크박스 하나씩 클릭하기
for target in target_list:
for m in manufacturer_list:
if target in m.text:
m.click()
break
driver.implicitly_wait(3)
print('타겟 제조사: ', str(target_list), '선택 완료 했습니다!\n')
# 전체 모델 개수
num_models = driver.find_element(
By.CLASS_NAME, 'list_num').text.strip().replace('(', '').replace(')', '')
# 페이지 개수
num_pages = math.ceil(int(num_models) / 30))
print('현재 검색된 모델은 총', str(num_models), '개 입니다.')
print('크롤링할 페이지는 총', str(num_pages), '페이지 입니다.\n')
##### 페이지 넘기면서 제품정보 가져와서 저장하기 ####
# 가격정보 저장할 빈 리스트 생성
numofprices_list = []
# 페이지별 소스 저장할 빈 딕셔너리 생성
soup_dict = {}
page = 1
# page에 1씩 더하면서, 전체 페이지 수와 같아지기 전까지 반복
while page < num_pages:
# 제품 정보들 전부 가져오기
soup = BeautifulSoup(driver.page_source)
products = soup.select('div.prod_main_info')
time.sleep(1)
# 페이지별 소스 딕셔너리에 저장해두기
soup_dict[page] = products
# 제품하나씩 돌리면서
for i in range(len(products)):
try: # 가격정보 개수 가져오기
numofprices_list.append(len(products[i].select('div.prod_pricelist li')))
except:
numofprices_list.append('')
time.sleep(1)
print(page, "페이지 완료!")
page += 1
# 11,21,31..번째 페이지로 넘기는 경우 [화살표] 버튼 클릭
if page%10 == 1:
driver.find_element(
By.CSS_SELECTOR, '#productListArea > div.prod_num_nav > div > a').click()
# 나머지 경우는 page에 해당하는 페이지 클릭
else:
# 페이지 넘기는 부분 경로 통째로 가져와놓고
page_nums = driver.find_element(By.XPATH, '//*[@id="productListArea"]/div[4]/div')
# 다음 페이지 링크를 클릭
page_nums.find_element(By.LINK_TEXT, str(page)).click()
# 에러나지 않도록 한페이지마다 잠시 대기
time.sleep(2)
# 마지막 페이지 소스 가져오고 마무리
soup = BeautifulSoup(driver.page_source)
products = soup.select('div.prod_main_info')
soup_dict[page] = products
print(page, "페이지 완료!\n")
print("<<< 크롤링 완료입니다! >>>\n")
print("가격정보가 가장 많은 경우는", max(numofprices_list), "개 입니다!")
# 함수 출력값: 크롤링된 페이지 정보 저장된 딕셔너리, 가격정보 최댓값
return soup_dict, max(numofprices_list)
4️⃣-(2) [함수2] 수집된 정보를 거르고 필터링해서 분석 가능한 데이터프레임으로!
위에서 더미데이터 거르는 부분 수정도 포함해서 수정한 최종함수입니다.
crawling_danawa_result: 긁어온 데이터를 돌리면서 데이터프레임으로 정리하는 함수입니다. - 입력값은 함수1에서 크롤링 페이지 정보 저장했던 딕셔너리, 가격정보 최댓값, 그리고 저장할 엑셀파일의 이름입니다. - 출력값은 크롤링 결과가 정리된 데이터프레임, 그리고 저장된 엑셀파일입니다.
def crawling_danawa_result(soup_dict, max_numofprices_list, excel_title):
# 함수 입력값: 제품정보 저장된 딕셔너리, 가격정보 최댓값, 결과 저장할 엑셀파일명
### 데이터 저장할 빈 리스트 생성 ###
# 모델이름, 출시시기, 헤드메시지, 스펙, 리뷰평점, 리뷰개수, 페이지링크
name_list, hm_list, spec_list, release_list, star_list, review_list, link_list = [], [], [], [], [], [], []
# 가격정보 리스트셋 (ex. 1위 일반구매, 2위 추가필터 포함..)
priceinfo_list_set = []
# 최저가 리스트셋 (ex. 100,000원, 120,000원..)
price_list_set = []
# 최댓값만큼 빈 리스트 생성 (이중리스트)
for i in range(max_numofprices_list):
priceinfo_list_set.append([])
price_list_set.append([])
### 딕셔너리에 저장해둔 페이지 하나씩 돌리기 ###
for key in range(len(soup_dict)):
products = soup_dict[key+1]
# 제품마다 하나씩 돌리기
for i in range(len(products)):
# 모델 출시시기 (마지막 온점떼고 string으로 저장)
try:
release = str(products[i].select_one('div.prod_sub_info dd').text.strip())
release_list.append(release[:-1])
except:
continue
# 모델 이름 (string으로 저장)
try:
name_list.append(str(products[i].select_one('p.prod_name a').text.strip()))
except:
name_list.append('')
# 헤드 메시지 (string으로 저장)
try:
hm_list.append(str(products[i].select_one('div.prod_intro').text.strip()))
except:
hm_list.append('')
# 모델 스펙 (string으로 저장)
try:
spec_list.append(str(products[i].select_one('div.spec_list').text.strip()))
except:
spec_list.append('')
# 리뷰 개수 (int로 저장, 리뷰없으면 0)
try:
review_list.append(int(
products[i].select_one('div.prod_sub_info a').text.strip().replace(',', '')))
except:
review_list.append(0)
# 리뷰 평점 (float으로 저장, 리뷰없으면 0)
try:
star_list.append(float(
products[i].select_one('div.cnt_star').text.strip().replace('점', '')))
except:
star_list.append(0)
# 제품페이지 링크
try:
thumbnail = products[i].select_one('div.thumb_image a')
link_list.append(thumbnail['href'])
except:
link_list.append('')
# 가격정보 전부 가져와서 하나씩 더하기
pricelist_raw = products[i].select('div.prod_pricelist li')
# 가격정보 (string으로 저장)
for p in range(max_numofprices_list):
try:
priceinfo_list_set[p].append(str(
pricelist_raw[p].select_one('div.over_preview').text.strip()))
except:
priceinfo_list_set[p].append('')
# 가격 최저가 (int로 저장)
for p in range(max_numofprices_list):
try:
price_list_set[p].append(int(
pricelist_raw[p].select_one('p.price_sect a').text.strip().replace('원', '').replace(',', '')))
except:
price_list_set[p].append('')
### 크롤링 결과 저장 ###
# 데이터프레임 생성
df_result = pd.DataFrame({
'제품모델명' : name_list, '출시시기' : release_list, '리뷰평점' : star_list, '리뷰개수' : review_list,
'헤드메시지' : hm_list, '스펙' : spec_list, '링크URL' : link_list
})
for p in range(max_numofprices_list):
df_result['가격정보_{}'.format(str(p+1))] = priceinfo_list_set[p]
df_result['최저가_{}'.format(str(p+1))] = price_list_set[p]
# 데이터프레임을 엑셀파일로 저장
with pd.ExcelWriter(excel_title) as writer:
df_result.to_excel(writer)
# 함수 출력값: 크롤링 결과 데이터프레임과 엑셀파일 저장
return df_result
수고하셨습니다! 이제 이 함수 2개로 다나와에서 공기청정기 뿐만 아니라 다른 제품정보들도 크롤링 할 수 있습니다! 다른 제품, 다른 제조사를 선택해도 문제없도록 작성되었으니, 가끔 문제시 유지보수만 해주면 되겠어요.
분석 대상이 될 제품의 기본정보를 전부 수집 완료했습니다! 다음엔 제품마다 리뷰데이터를 수집해볼게요. 다나와는 말그대로 판매중인 사이트의 리뷰들이 다나와있어서 아주 유용합니다.