본문 바로가기
Scraping

[파이썬으로 웹스크래핑] 가슴이 웅장해지는 셀레니움을 araboja..

by 돈민찌 2021. 8. 10.
반응형

꺄꿍

지난 번에 requests와 BeautifulSoup을 사용해 보면서, 웹이 처음으로 html으로 나타날때 requests를 get해 오는 특성 탓에, 여러가지로 부족한 스크래퍼를 만들 수 밖에 없었죠? 결과물이야 나쁘지 않았지만, 각 탭에서 초기에 나타나는 8개의 글만 스크랩해오고 스크롤을 내려야 나타나는 게시물을 못 가져온다던지, 그런 문제들 말이죠..!! 

자 문제점을 파악했으니 어떻게 해결하면 좋을까요? 스크롤을 해야겠죠? 그런데 requsets는 브라우저를 실제로 띄우는 것도 아니다 보니 직접 스크롤을 해주기도 힘들어 보입니다. 여러분이 스크랩하고 싶은 어떤 사이트는, 자세히 보기 혹은 더보기 버튼을 눌러야 모든 게시물이 보일 수도 있고, 아니면 로그인을 해야만 볼 수 있는 게시물이 있을 수도 있죠. 그렇담 접근을 바꿔야겠네요 이번에 소개할 라이브러리는 selenium 입니다!ㅎㅎ 우선 설치부터 해봅시다.

pip3 install selenium

혹시 지난 게시물을 안보셨다면, BeautifulSoup도 함께 사용해볼 예정이니 같이 설치해줍시다.

pip3 install bs4

셀레니움은 웹드라이버라는 도구를 통해 웹을 자동으로 제어하는 도구입니다. 그래서 웹드라이버가 필요한데요. 설명을 크롬으로 계속하고 있으니, 크롬웹드라이버를 설치해봅시다.

chromedriver.exe

크롬을 설치할 때 하나의 팁을 드리자면, chromedriver.exe를 Downloads 폴더가 아닌 C://Windows 폴더 아래에 설치하는 것을 추천드립니다. 잘 보이는 곳에 두려고 다른 곳에 설치하시면, 해당 디렉토리 위치를 코드에서 입력해줘야 합니다ㅎㅎ 최신버전을 받아주시는게 좋고, 가능하면 평소에 크롬도 업데이트가 나오면 해주셔야 좋습니다. 버전이 맞는게 좋더라구요. 여러분의 OS와 맞는 드라이버를 받아주세요. 

자 그럼 간단하게 지난번 맥스무비를 스크랩했던 코드를 셀레니움에 맞게 수정해보겠습니다. 사용법이 거의 유사하기 때문에 간단하게 수정가능합니다.

from selenium import webdriver
from bs4 import BeautifulSoup
import csv

driver = webdriver.Chrome()  # Windows 폴더 외에 위치에 받았다면 괄호 안에 문자열로 패스를 입력해주세요!
# driver = webdriver.Chrome('C://Users/ydm27/Downloads/chromedriver')

with open(file='max_movie.csv', newline="", encoding='utf-8', mode='w') as output_file:
    writer = csv.writer(output_file)
    writer.writerow(['link', 'title', 'img_src', 'upload_date', 'reporter'])
    target_url = 'https://www.maxmovie.com'
    target_categories = ['/review', '/interview', '/plan', '/ott']
    for category in target_categories:
        driver.get(target_url + category)
        articles = driver.find_elements_by_css_selector('li.content.clearfix')
        for article in articles:
            link = target_url + article.find_element_by_css_selector('span.imgBox.floatLeft > a').get_attribute('href')
            title = article.find_element_by_css_selector('span.textBox.floatLeft > a:nth-child(1) > h4').text
            img_src = article.find_element_by_css_selector('span.imgBox.floatLeft > a > span').get_attribute('style').replace('background-image: url("', '').replace('"); background-size: cover;', '')
            upload_date = article.find_element_by_css_selector('span.textBox.floatLeft > span').text.split(' · ')[0]
            reporter = article.find_element_by_css_selector('span.textBox.floatLeft > span > a').text
            writer.writerow([link, title, img_src, upload_date, reporter])
            print([link, title, img_src, upload_date, reporter])
driver.quit()

 

아직도 코딩 안배웠어? 5만원 깎아줄께 형아만 믿어

참 쉽죠?.?

get('href')가 get_attribute('href')로 바뀌었고, soup.find / soup.select가 아니라 find_element_by_css_selector로 바뀌는 등, 좀 더 함수가 보면 바로 의미를 파악하기 쉬운 이름으로 되어있어 코드를 이해하기도 편하네요. 그리고 뷰티풀숲과 다르게, get_attribute()로 가져오는 과정에서 띄어쓰기가 안되어있거나 하는 것을 알아서 수정해주는 부가적인 기능이 있어서, 그 부분을 위해 replace 메소드를 약간 고쳤어요. 그리고 마지막에는 불러온 크롬드라이버를 종료해주는 것 잊으면 안됩니다!ㅎㅎ 아마 코드가 제대로 작동했다면, 지난번의 뷰티풀숲과 리퀘스츠를 합친 것과 똑같은 결과물을 얻으셨을 것입니다. 사실 눈치채셨겠지만 뷰티풀숲을 사용하겠다고 해놓고 은근슬쩍 빼먹었어요!!!ㅋㅋㅋㅋ 셀레니움이 요청을 보내는 것 이상으로 웹엘리먼트를 하나하나 찾아내는 기능까지 해주고 있다는 것을 보여드리고 싶었습니다.

자 그런데 우리 앞의 리퀘스츠에서 부족한 기능이 있어서 셀레니움으로 들어온 것인데, 지금 결과물은 32줄 그대로네요? 왜냐면 우리 아직 스크롤하는 부분을 넣어주지 않았거든요! 셀레니움은 브라우저를 제어할 때 내부적으로 자바스크립트를 사용합니다. 그래서 명령을 할 때에도 자바스크립트로 내릴 수 있어요. 인스타그램, 페이스북 등을 볼 때 스크롤을 내리면 그때서야 새로운 게시물을 불러들이면서 사용자가 볼 만큼만 데이터를 사용하는 효율적인 방법을 사용해 빠르고 쾌적한 사용자 경험을 선사하잖아요. 요즘은 이렇게 되어있는 사이트가 매우 많습니다. 일단 스마트하게 스크롤을 계속 내리는 코드를 짜봅시다. 

from selenium import webdriver
import time

### 무한 스크롤 코드 ###
# 처음 브라우저 창의 높이를 구합니다. (자바스크립트 명령문)
last_height = driver.execute_script("return document.body.scrollHeight")
while True:
    # 가장 아래까지 (창의 높이만큼) 스크롤을 내립니다.
    driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    # 새로운 내용 로딩될때까지 기다림 (이를 위해 내장 모듈 time을 임포트해야해요)
    time.sleep(5)
    # 다시 현재 브라우저의 창의 높이를 구합니다. (페이지가 더 로드됐다면 늘어났겠죠?)
    new_height = driver.execute_script("return document.body.scrollHeight")
    # 새로운 창의 높이가 직전의 높이와 같을 때 (더 이상 로드될 컨텐츠가 없을때까지)
    if new_height == last_height:
    	break
    last_height = new_height

갑자기 이런 코드가 어디서 나왔냐구요? 네 저도 구글링해서 쓰던거를 저한테 맞게 이리저리 고쳐서 만든 것입니다. 지금이야 저도 이 코드의 뜻을 알고 글을 쓰지만 처음엔 몰라도 괜찮아요. 일단 복사해두고, 자주 쓰면서 익혀보세요. 자바스크립트에 대한 지식이 없으시다면 이 코드는 복사해두고 필요할 때 꺼내서 쓰시면 됩니다. 이런 식으로 스크롤을 내려야 로드되는 사이트가 요즘 많아졌거든요. 요긴하게 쓰실 거예요. webdriver의 execute_script()는 넘겨진 문자열 파라미터를 자바스크립트 코드로 변환해 브라우저에게 명령을 내립니다. 이게 어떻게 작동하지? 싶으신 분들은 이 두가지 코드를 개발자도구 콘솔에 입력해보세요.

document.body.scrollHeight;
window.scrollTo(0, document.body.scrollHeight);

이제 이 코드를 다시 맥스무비 스크랩에 이용해보겠습니다. 이번엔 아까 불러오기만 했던 뷰티풀숲을 셀레니움과 함께 사용해볼게요! 다만 이번에는 스크롤을 내려서 기사들을 가져오기 때문에 한가지 탭만 실행해도 괜찮을 것 같아 reviews 탭만 넘겨줬어요!

from selenium import webdriver
from bs4 import BeautifulSoup
import time
import csv

driver = webdriver.Chrome()  # Windows 폴더 외에 위치에 받았다면 괄호 안에 문자열로 패스를 입력해주세요!
# driver = webdriver.Chrome('C://Users/ydm27/Downloads/chromedriver')

with open(file='max_movie.csv', newline="", encoding='utf-8', mode='w') as output_file:
    writer = csv.writer(output_file)
    writer.writerow(['link', 'title', 'img_src', 'upload_date', 'reporter'])
    target_url = 'https://www.maxmovie.com'
    driver.get(target_url + '/review')
    last_height = driver.execute_script("return document.body.scrollHeight")
    while True:
        # 가장 아래까지 (창의 높이만큼) 스크롤을 내립니다.
        driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
        # 새로운 내용 로딩될때까지 기다림 (이를 위해 내장 모듈 time 을 임포트해야해요)
        time.sleep(2)
        # 다시 현재 브라우저의 창의 높이를 구합니다. (페이지가 더 로드됐다면 늘어났겠죠?)
        new_height = driver.execute_script("return document.body.scrollHeight")
        # 새로운 창의 높이가 직전의 높이와 같을 때 (더 이상 로드될 컨텐츠가 없을때까지)
        if new_height == last_height:
            break
        last_height = new_height
    response = driver.page_source
    driver.quit()
    soup = BeautifulSoup(response, 'html.parser')
    articles = soup.select('li.content.clearfix')
    for article in articles:
        link = target_url + article.select_one('span.imgBox.floatLeft > a').get('href')
        title = article.select_one('span.textBox.floatLeft > a:nth-child(1) > h4').get_text()
        img_src = article.select_one('span.imgBox.floatLeft > a > span').get('style').replace('background-image:url(', '').replace(');background-size:cover', '')
        upload_date = article.select_one('span.textBox.floatLeft > span').get_text().split(' · ')[0]
        reporter = article.select_one('span.textBox.floatLeft > span > a').get_text()
        writer.writerow([link, title, img_src, upload_date, reporter])
        print([link, title, img_src, upload_date, reporter])

코드의 구조가 이해되시나요? 그럼 결과물인 max_movie.csv를 한번 볼까요?

리뷰 탭만 가져왔는데 500개 가까운 데이터가 한번에 불려왔네요!! 앗 이거 직접 스크랩했으면 몇일은 했겠는데요!!!
역시 반복해서 뭔가 하는건 컴퓨터한테 시키는게 맞는 것 같아요 사람은 실수를 하게 되어있고 하다보면 금새 지겨워하고 지치니까요..!!  참고로 저기 마지막 줄의 03월 29일은 2년 전 3월입니닿ㅎㅎ 

사실 이 사이트에서 이런 컨텐츠를 쌓아올리는데 들였을 노력을 생각하면 스크래핑이란 기술은 너무하다 싶을 만큼 간단하게 똑딱하면 데이터를 훔쳐올 수 있으니 왠지 컨텐츠 제작자 입장에선 억울하기도 합니다. 그러니 기사 등을 스크랩할 때는 함께 가져온 url 등을 통해 출처를 반드시 명시해야 하고, robots.txt나 메타태그 등을 통해 이 사이트의 게시물을 검색봇 외에는 스크랩하지 말아라고 명시해둔 사이트는 건드리지 않는 것이 좋아요. 

요번에는 Selenium + BeautifulSoup의 콜라보로 사이트의 데이터를 가져와봤는데요. 위에서 썼던 코드처럼, 웹드라이버의 find_element 메소드를 이용해서 뷰티풀숲 없이 모든 데이터를 가져와도 괜찮아요. 다만 안정성과 속도 면에서 이렇게 한 페이지에 불러온 수많은 데이터를 소스를 베껴온 다음 드라이버는 닫아버리고 나머지 html 파싱 작업을 하는 것이 좋아서 이렇게 작업을 해봤습니다. 셀레니움은 보시다시피 이렇게 자바스크립트 코드를 명령할 수 있기 때문에, <더보기> <자세히 보기> 등의 버튼을 누르게 할 수도 있고, 심지어는 로그인 폼 등에 아이디와 비밀번호를 입력하게 할 수도 있어요. 이런 편리함의 뒷면엔 항상 그것을 악용하는 사람들이 있기 때문에, 일부 사이트에 로그인할 때 나는 로봇이 아닙니다 같은 1차적인 차단툴이 작동하는 것이예요..

셀레니움의 actionchains 모듈을 사용하면 마우스 클릭, 키보드 입력, 드래그, 더블클릭, 기다림 등등 다양한 입력 액션을 순서대로 입력할 수도 있고, 특정 요소가 클릭 가능한 상태나 눈에 보이는 상태가 될 때까지 기다리게 하는 explicitly_wait 기능이나, 언제든 로딩 시간을 기본적으로 기다려주는 implicitly_wait 기능이 있으니, 관련된 키워드를 말씀 드릴게요 한번씩 찾아보시길 바랍니다. 여기서 다 말씀드리기엔 시간이 모자랄 것 같아요. 다음 글에서는 뷰티풀숲을 떼 버리고 셀레니움의 기능에 집중해봅시다. 아래는 공부거리들만 던져놓습니다. 당연히 어렵고 이해안되실겁니다. 대충 이런게 필요할때 셀레니움이 도움을 줄 수 있구나 하는 것만 이해하시면 충분합니다.

<참고로 XPATH는 개발자도구에서 Copy Selector 대신 Copy Xpath를 클릭하시면 됩니다. 선택자의 문법과 크게 다르지 않기 때문에(:nth-child(1)이 [0]이 되는 정도의 차이..?) 이해하기 어렵지 않으실 것입니다!!>

driver.find_element_by_id("")
driver.find_element_by_xpath("")
driver.find_element_by_id("")
driver.find_element_by_name("")
driver.find_element_by_tag_name("")
driver.find_element_by_class_name("")
driver.find_element_by_css_selector("")
driver.find_element_by_link("")
driver.find_element_by_link_text("")
driver.find_element_by_partial_link_text("")
driver.find_element(By.XPATH, "")
>>> 특정한 엘리먼트를 특정하는 다양한 방법. 용도에 맞게 사용한다.

driver.save_screenshot('screenshot.png')
>>> 현재 드라이버 창의 스크린샷을 저장하는 코드

2021.08.11 - [Python::web_scraping] - [파이썬으로 웹스크래핑] 셀레니움 기능탐구(1) 요소찾기

 

[파이썬으로 웹스크래핑] 셀레니움 기능탐구(1) 요소찾기

자꾸 뭔가 좀 설명하려다가 코드 하나 가져와서 참 쉽죠?를 반복하니까 아는 사람만 재미있게 보고 깜깜이인 초보자 분들은 힘들 수도 있겠다는 생각이 들어 핵심적인 기능 소개를 몇가지 해보

cat-minzzi.tistory.com

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

driver = webdriver.Chrome('chromedriver')

# 일반적인 대기
import time
time.sleep(5)

# 암묵적인 대기 (Implicitly wait)
driver.implicitly_wait(time_to_wait=5)

# 명시적인 대기 (Explicitly wait)
driver.get(url='https://www.google.com/')

# 명시적인 대기 활용 (특정 요소 대기)
try:
    element = WebDriverWait(driver, 5).until(
        EC.presence_of_element_located((By.CLASS_NAME , 'gLFyf'))
    )
finally:
    driver.quit()
    
expected_conditions의 조건들
title_is
title_contains
presence_of_element_located
visibility_of_element_located
visibility_of
presence_of_all_elements_located
text_to_be_present_in_element
text_to_be_present_in_element_value
frame_to_be_available_and_switch_to_it
invisibility_of_element_located
element_to_be_clickable
staleness_of
element_to_be_selected
element_located_to_be_selected
element_selection_state_to_be
element_located_selection_state_to_be
alert_is_present

2021.08.11 - [Python::web_scraping] - [파이썬으로 웹스크래핑] 셀레니움 기능탐구(2) 기다려! 대기하기

 

[파이썬으로 웹스크래핑] 셀레니움 기능탐구(2) 기다려! 대기하기

앞서 실행한 스크래핑 코드들을 보면, 프로그래밍한 코드가 동작할 때 사람의 눈보다 훨씬 빠르게 데이터를 읽어오기 때문에, 페이지가 데이터를 불러오기도 전에 당장 눈 앞에 보이는 껍데기 h

cat-minzzi.tistory.com

# webdriver 아래에는 다음과 같은 모듈들이 있습니다!!

webdriver.Firefox  
# 파이어폭스!! (사생활보호기능이 뛰어난 mozilla 재단의 브라우저)
webdriver.FirefoxProfile
webdriver.Chrome  
# 크롬!! (크.롬.좋.아.)
webdriver.ChromeOptions
webdriver.Ie  
# 인터넷익스플로러! (....)
webdriver.Opera  
# 오페라!! (무료 VPN 기능이 있는 브라우저)
webdriver.PhantomJS  
# 팬텀JS (GUI가 없는 헤드리스 브라우저)
webdriver.Remote
webdriver.DesiredCapabilities
webdriver.ActionChains  
# 액션체인즈(키보드,마우스드래그 등 다양한 동작을 연달아 수행하게 할 수 있다!)
webdriver.TouchActions
# 터치액션즈(싱글탭, 더블탭, 롱프레스 등 스마트폰의 제스쳐를 수행함)
webdriver.Proxy
from selenium.webdriver import ActionChains

menu = driver.find_element_by_css_selector('.nav')
hidden_submenu = driver.find_element_by_css_selector('.nav #submenu1')

ActionChains(driver).move_to_element(menu).click(hidden_submenu).perform()

actions = ActionChains(driver)
actions.move_to_element(menu)
actions.click(hidden_submenu)
actions.perform()

>>> 6번줄 코드와 8~11번줄 코드는 같은 뜻입니다.
>>> ActionChains(driver)를 통해 액션체인 객체를 형성하고,
>>> 취할 액션을 순서대로 입력하고, perform()을 입력해야 완료됩니다.
# 브라우저 창과 관련된 코드들!

 # 창 최소화 (작업표시줄에 존재하며 백그라운드에서 동작함)
driver.minimize_window()
 # 창 최대화 (창 크기 조절과는 별개의 동작임)
driver.maximize_window()
 # 창의 크기를 조절하는
driver.set_window_size(width=1920, height=1080)
 # 창의 크기를 1920*1080으로 조절한 뒤 아예 창을 보이지 않게 한 뒤 백그라운드에서 작업하기.
options = webdriver.ChromeOptions()
options.add_argument('headless')
options.add_argument('window-size=1920x1080')
driver = webdriver.Chrome('chromedriver', options=options)
반응형

댓글