먹고 기도하고 코딩하라

[생존형 튜토리얼] 파이썬 selenium으로 데이터 크롤링 본문

개발일지

[생존형 튜토리얼] 파이썬 selenium으로 데이터 크롤링

사과먹는사람 2021. 2. 15. 17:05
728x90
728x90

 

예전에는 beautifulsoup4를 이용해서 크롤링을 했는데, 이번에는 selenium으로 하기로 했습니다.

당장 필요한 코드를 짜기 위해 필수적인 사용법만 익히고 바로 사용했습니다.

beautifulsoup4와 셀렉팅하는 방식이 유사해서 사용하는 게 그리 어렵지는 않았네요.

 

아래의 글 두 개를 참고해서 코드를 짰습니다.

Python Selenium 사용법 [파이썬 셀레늄 사용법, 크롤링]

나만의 웹 크롤러 만들기(3): Selenium으로 무적 크롤러 만들기

 

 

초기 목표는 올리브영 상품 크롤링이었지만 올리브영 홈페이지의 robots.txt를 까본 결과.. 구글과 네이버 크롤러 외의 다른 봇은 전체 페이지 크롤링 disallow하는 바람에 랄라블라로 돌리기로 했습니다.

 

 

그래서 원래 짜둔 올리브영 크롤링 코드는 역사의 뒤안길(?)로 사라지게 두기로 했습니다.

어떤 홈페이지 타겟으로 크롤링을 하실 때는 반드시 해당 홈페이지의 robots.txt를 확인하고, user-agent *에서 allow가 되어 있는 부분만 크롤링하도록 합시다. 아직은 권고 사항 정도라서 큰 효력을 갖지는 않고, 학습 용도라면 크롤링을 눈 감아주는 분위기지만 너무 어뷰징해서 트래픽 폭주하면 문제의 소지가 될 수 있습니다.

랄라블라 크롤링은 다른 팀원 분께 맡겨서 현재 제가 코드가 없는 고로.. 일단 올리브영 홈페이지를 타겟으로 한 코드를 예제로 쓰겠지만, 따라하진 마시고 문법 위주로 보시길 바랍니다.

 

 

목표

입력 : 상품 카테고리 코드(String)

출력 : 해당 상품 카테고리 아래 상품 목록에 든 상품이 정보가 든 JSON 파일

 

상품 하나당 상품의 정보가 담긴 json 파일 한 개가 산출되는 것을 목표로 했습니다.

가령 제가 상품 번호가 A000000105563인 상품을 크롤링하면 해당 상품이 속한 카테고리를 나타내는 디렉터리 밑에 A000000105563.json이라는 파일이 만들어지도록 했습니다. 

이 json 파일에는 상품 번호, 이름, 브랜드, 카테고리, 이미지, 상품 설명, 별점, 리뷰 개수, 할인가, 정가, 옵션 개수, 옵션의 이름, 옵션의 가격, 옵션의 이미지 목록 등이 들어 있습니다.

 

 

준비

파이썬 3 / selenium 설치, chromedriver 다운로드

필자 환경

  • Windows 10 64비트
  • 파이썬 3.8.3
  • 크롬 88.0.4324.182

 

selenium 설치는 그다지 어렵지 않습니다. 도움이 필요하다면 이 글을 참고하세요. 이 글은 chromedriver 다운로드까지 한 큐에 다루고 있으므로 보기 좋습니다.

 

저는 같은 디렉터리에 category_list.py 라는 파일이 있고, 여기에 beauty_list, health_food_list, life_list라는 거대한 딕셔너리가 있습니다. 카테고리 분류를 위해 따로 분리해둔 건데, 이 안에는 각 소분류 카테고리별로 어떤 코드를 갖고 있는지가 적혀 있습니다.

이런 식으로 depth가 좀 있게 구성이 되어 있는데요. 

health_food_list = {
  'health_hygeine': {
    'dentalcare': {
      'toothbrush': '1000002000300010001',
      'toothpaste': '1000002000300010002'
    },
    'eyecare': {
      'lenscare': '1000002000300020001',
      'lenssterilizer': '1000002000300020002'
    }
  },
  'healthfood': {
    'vitamin': {
      'complex': '1000002000100060001'
    }
  }
}

종합 비타민의 경우 health_food_list['health_food']['vitamin']['complex']로 코드값을 찾을 수 있게 됩니다.

드라이버가 알아서 카테고리 코드를 바꿔가며 크롤링하길 바라는 마음에서 코드값을 찾아 다 분류를 해 놨습니다.

 

다음과 같이 작성했습니다.

import os
import sys
import json
from collections import OrderedDict
from time import sleep
from selenium import webdriver
from category_list import beauty_list, health_food_list, life_list

goods_list = {
  'beauty_list': beauty_list, 
  'health_food_list': health_food_list, 
  'life_list': life_list
}

# 크롬드라이버 위치 절대경로로 설정
driver = webdriver.Chrome("C:\\Python38\\chromedriver")
url = 'https://www.oliveyoung.co.kr/store/display/getMCategoryList.do?dispCatNo='
data = OrderedDict()

json 포맷 사용을 위해 json 모듈을 import하고, 저장하고, json 파일마다 같은 순서 포맷대로 구성하기 위해 OrderedDict를 사용합니다.

selenium에서 webdriver import도 했구요.

일단 driver 변수로 웹드라이버를 가져오는 게 먼저인데요. 크롬 드라이버가 있는 위치를 절대경로 path로 주시면 됩니다. 그런 다음 여러 페이지를 돌아다녀도 변하지 않는 고정 URL을 미리 url 변수에 할당해 두시구요. data는 OrderedDict로 하나 만듭니다. 이 data 내용이 곧 json 파일 내용이 됩니다.

다른 페이지로 실습하실 분들은 이 정도만 쓰셔도 될 것 같습니다.

from selenium import webdriver

# 크롬드라이버 위치 절대경로로 설정
driver = webdriver.Chrome('path')
url = '경로'
driver.get(url)

 

사용법

전체 코드를 다루지 않고 문법 위주로 어떻게 사용하는지를 좀 보여 드리겠습니다.

 

(1) 웹페이지 열기

driver.get(url+code)

driver.get(URL)을 하면 URL을 열게 됩니다. chromedriver를 웹드라이버로 썼으니 크롬으로 열립니다.

 

 

(2) 대기

(2) - 가. 암묵적 대기 (implicitly_wait)

from selenium import webdriver

driver = webdriver.Chrome('path')
driver.implicitly_wait(10) # seconds
driver.get("http://somedomain/url_that_delays_loading")
myDynamicElement = driver.find_element_by_id("myDynamicElement")

driver.implicitly_wait(sec)는 웹드라이버에게 DOM에 당장은 잡히지 않는 어떤 요소(들)를 찾기까지, 즉 NoSuchElementException을 던지기 전에 좀 기다려 보라고 초 단위의 시간을 줍니다. 기본 셋팅은 0입니다.

이 코드는 driver.get 이후 myDynamicElement라는 요소를 찾기까지 10초간 시간을 주게 됩니다. 10초가 되기 전 찾으면 대기는 끝납니다. 하지만 10초가 넘어가도 DOM 요소가 로드되지 않는 경우에는 NoSuchElementException 에러가 나면서 프로그램이 뻗게 됩니다. 이런 경우를 대비해 try-except로 감싸 주거나 시간을 조금 늘리는 것이 좋겠습니다.

 

(2) - 나. 명시적 대기 

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

try:
    element = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.ID, "myDynamicElement"))
    )
finally:
    driver.quit()

문서에 있는 명시적 대기의 예제입니다. implicitly_wait과 달리 By, WebDriverWait, expected_conditions 모듈을 추가로 import해줘야 합니다. explicitly_wait라는 메소드가 따로 존재하는 것이 아니라서 이런 포맷으로 쓰게 됩니다. WebDriverWait와 ExpectedCondition 조합만이 명시적 대기를 할 수 있는 유일한 조합이라고 합니다.

요지는, 더 진행시키기 전 어떤 특정 조건을 만족하도록 기다리라고 지시하는 것입니다. 위의 경우, driver.get 이후 웹 드라이버는 myDynamicElement라는 id를 가진 요소를 찾을 때까지 10초 안에 찾을 때까지 대기하게 됩니다. 해당 시간 내로 찾지 못하면 TimeoutException 에러가 발생하고요. 에러가 나든 나지 않든 driver가 닫히게 됩니다.

기본적으로, WebDriverWait는 until 안의 문장이 성공적으로 실행되기 전까지 0.5초마다 ExpectedCondition을 호출하게 됩니다. ExpectedCondition은 성공할 때 true, 엘리먼트 찾기를 실패했을 때는 null이 아닌 뭔가를 return하게 됩니다. 

 

(2) - 다. 지정 시간만큼 무조건 대기 time.sleep()

#sleep1.py
import time
for i in range(10):
    print(i)
    time.sleep(1)

'점프 투 파이썬'의 sleep 예제입니다. 0부터 9까지 1초마다 숫자를 1씩 늘려가며 출력하는 예제인데요.

위와 같이 time.sleep(n)은 인자로 들어가는 시간 n초를 쉬게 됩니다.

간단한 코드이지만, 기다릴 수 있는 제한 시간을 주고 중간에 찾으면 바로 대기를 끝나고 돌아가는 implicitly_wait과 달리 무조건 쉬기 때문에 굉장히 비효율적이라고 할 수 있습니다.

 

 

(3) 셀렉팅 (find_element_by / find_elements_by)

(3) - 가. xpath로 찾기 (find_element_by_xpath)

items = driver.find_elements_by_xpath('//li[@criteo-goods]')

XPath가 뭔가 하고 검색해 보면 확장 생성 언어(XML)의 구조를 통해 경로 위에 지정한 구문을 사용하여 항목을 배치하고 처리하는 방법을 기술하는 언어라고 설명이 돼 있는데요. 쉽게 말하면 XML 문서에서 특정 요소, 부분의 위치를 찾을 때 쓰는 언어입니다. HTML 문서에서도 요소를 찾을 수 있습니다.

저는 웬만하면 클래스나 id로 셀렉팅하기를 좋아하는데, 타겟 태그에 특이한 클래스나 id 없이 데이터 속성만 떨렁 있으면 찾기가 힘들어 좀 난처했습니다. 이럴 때 쓰기 좋은 게 xpath인 것 같아요.

위의 경우에는 문서 내의 위치와 상관없이 모든 li 태그(//li), 그 중에서도 criteo-goods라는 속성([@criteo-goods])을 가진 태그들만 셀렉팅해 items에 넣어 줍니다. 

 

실제로 세부 카테고리의 상품 리스트들을 볼 수 있는 페이지에는 criteo-goods라는 li 태그로 각 상품을 감싸고 있었습니다.

보시면 criteo-goods는 문서에서 25개가 검색되고 있는데요. 나머지 한 개는 태그로 들어가는 게 아니라 스크립트 코드여서 상관 없고, 24개만 criteo-goods 데이터 속성을 가진 li 태그들이었습니다. 24개인 이유는 상품 리스트에서 한 페이지에 노출되는 상품 개수가 24개라서 그렇습니다. 36, 48개로 바꿀 수 있는데 이 숫자를 바꾸면 criteo-goods를 가진 태그도 따라서 늘어납니다.

xpath를 잘 쓰면 이렇게까지도 쓸 수 있는데요.

item = driver.find_element_by_xpath(
  '//*[@id="Contents"]/ul[%s]/li[%s]/div/a' 
  % ((count // 4) + 2, (count % 4) + 1)
)

이것을 해석해 보면

문서 내의 위치와 상관없이(//)
id가 Contents인 모든 요소 아래(*[@id="Contents"]/)
count//4+2번째 ul 태그 아래 (ul[%s]/)
count%4+1번째 li 태그 아래 (li[%s]/)
div 밑의 a 태그(div/a)

가 item에 들어가게 됩니다.

 

코드와 함께 XPath가 맞는지 보세요

 

사실 ul 태그에는 cate_prd_list라는 클래스가 붙어 있긴 했지만, 소 카테고리의 한 페이지에서 count를 늘려 가며 그 count를 이용해 수집해야 할 상품의 위치를 추적하는 게 더 효율적이라는 생각이 들어서 저렇게 구성하게 됐습니다.

XPath, 모르면 헷갈리기 쉬운데(제가 그랬습니다) 개발자 도구 열고 문서 내에서 원하는 요소에서 Copy XPath 해 주면 복사해주긴 합니다. 여기서 조금만 바꾸면 되구요. ^^

 

(3) - 나. id로 찾기 (find_element_by_id)

id로는 다음과 같이 찾을 수 있습니다.

img = driver.find_element_by_id('mainImg')

너무 당연한 얘기지만 HTML 문서에서 특정 id를 가진 요소는 단 1개뿐입니다. 그렇기 때문에 find_element_by_id로만 찾으셔야 합니다. 위의 코드의 경우, 문서에서 mainImg라는 id를 가진 요소를 찾아 img에 할당합니다.

 

(3) - 다. 클래스 이름으로 찾기 (find_element_by_class_name)

클래스 이름으로는 다음과 같이 찾을 수 있습니다.

# 요소 한 개만 찾기
number = driver.find_element_by_class_name('prd_btn_area > .btnZzim')

# 여러 요소 찾기
cat = driver.find_elements_by_class_name('loc_history > li > .cate_y')

위의 경우 number에는 전체 문서에서 prd_btn_area를 클래스로 가진 제일 첫 요소를 찾고, 그 요소 바로 밑에 있는 btnZzim 클래스가 붙은 요소가 할당됩니다. number를 찍어보면 WebElement 타입으로 나오게 됩니다.

cat에는 전체 문서에서 loc_history 클래스인 요소 밑의 li 태그 밑의 cate_y 클래스가 붙은 모든 요소들이 할당됩니다. 그래서 cat은 찍어 보면 list 타입으로 나옵니다.

 

막간을 이용한 팁 제공 - 자식/자손 요소 찾기

beautifulsoup4로 셀렉팅을 해 보신 분이라면 '>'가 익숙하실 텐데요. 만약 요소 셀렉팅을 안 해보신 분이라면 '>'는 어떤 요소의 직계 자식(?), 즉 바로 한 depth 밑에 있는 자식 요소를 가리키는 것이라고 알고 계시면 됩니다.

어차피 prd_btn_area나 btnZzim이나 클래스로 찾는 건 매한가진데, 왜 저렇게 쓰는가 궁금해하실 분도 계실 것 같은데요. 이유는 2가지입니다. depth가 너무 깊은 데에 묻혀 있거나 btnZzim이라는 클래스가 붙은 첫 요소가 제가 찾는 그 요소가 아닐 수도 있기 때문입니다.

제가 큰 컨테이너 요소를 찾고 작은 요소로 좁혀 들어가는 방식을 더 선호해서 그렇기도 합니다. ㅎ

 

바로 밑에 있는 자식이 아니라 자손 요소를 찾으려면 어떻게 해야 할까요?

item_imgs = driver.find_elements_by_class_name('detail_area img')

간단하게 그냥 띄어서 써주시면 됩니다. item_imgs에는 detail_area를 클래스로 가진 요소 밑의 모든 img 태그들이 할당됩니다.

이 때 주의하실 점은 자식, 자손 요소를 찾을 때 클래스나 id로 찾으시려면 클래스 앞에는 온점(.), id 앞에는 해시(#)를 붙여주셔야 한다는 점입니다.

# 자식 요소 중 cate_y 클래스인 요소들을 찾기
cat = driver.find_elements_by_class_name('loc_history > li > .cate_y')

# 자손 요소 중 special 아이디인 요소 찾기
discount_price = driver.find_element_by_class_name('price-2 #special')

 

(3) - 라. 태그 이름으로 찾기 (find_element_by_tag_name)

태그 이름으로는 다음과 같이 찾을 수 있습니다.

options = driver.find_elements_by_tag_name('li > a > div > .option_value')

options에는 li 태그 아래 a 태그 아래 div 태그 아래 option_value를 클래스로 가진 모든 요소가 할당됩니다.

 

 

(4) 요소의 속성 가져오기 (WebElement.get_attribute)

  number = driver.find_element_by_class_name('prd_btn_area > .btnZzim')\
    .get_attribute('data-ref-goodsno')

find_element_by로 찾은 WebElement.get_attribute('attr') 하면 해당 요소에서 'attr'이라는 속성의 value 값을 찾아오게 됩니다.

위의 코드는 문서에서 prd_btn_area가 클래스로 붙은 요소 밑의 btnZzim이 클래스로 붙은 요소에서 'data-ref-goodsno'라는 속성의 값을 number에 할당하도록 합니다.


저는 상품 번호를 가져오기 위해 위와 같이 썼습니다.

커스텀 데이터 속성으로 되어 있는 것들, 혹은 img 태그의 src이나 a 태그의 href 속성 같은 것들 다 저렇게 가져오시면 됩니다.

 

 

(5) 요소의 텍스트만 가져오기 (WebElement.text)

brand = (driver.find_element_by_class_name('prd_brand')).text

find_element_by로 찾은 WebElement에서 .text를 쓰면 해당 태그 전체가 나오는 게 아니라 텍스트만 아름답게 나오게 됩니다. 

 

 

(6) 요소 클릭

WebElement.click()

driver.click() 이렇게 쓰시면 안 되구요. driver에서 find_element_by로 찾은 요소를 클릭하는 게 .click() 메소드입니다.

단순하게 클릭하는 기능만 하는데, <a> 태그가 있는 WebElement를 클릭하거나 버튼을 클릭하게 할 수도 있겠죠? 쓰기 나름입니다.

 


 

생존을 위한 사용법입니다. 진짜 베이직한 문법들만 담았고, DOM 요소를 조작하셔야 한다면 맨 위에 제가 링크해둔 글 2개를 읽어 보시면 좋겠습니다. 설명이 잘 되어 있고 이해하기도 쉽습니다.
써 보니까 저는 beautifulsoup4보다 selenium 써서 데이터 긁는 게 더 쉬운 것 같네요. 페이지 앞뒤로 이동하면서 끝없이 긁어올 수 있다는 것도 장점이구요. 특히 시간 대기 부분을 살펴보면서 많이 배웠습니다. "재수 없어 NoSuchElement 나오면서 뻗는다"라고 생각했던 부분들은 암묵적 대기가 아니라 명시적 대기로 바꿔야겠다는 생각도 드네요. ㅎ

 

이 글이 도움이 되셨다면 광고 한 번 클릭 부탁드립니다. ㅎㅎ

 

 

References

selenium 문서

XPath란 무엇일까?

점프투파이썬 05-6 라이브러리

Implicit, Explicit, & Fluent Wait in Selenium WebDriver

 

 

728x90
반응형
Comments