Selenium 을 사용하여 웹 페이지 수집하기

seleniumpythoncrawl

데이터을 동적으로 불러오는 웹 페이지를 수집하는 것은 어려운 일입니다. 이런 페이지는 사용자가 행동(클릭)할 때 데이터를 가져오기 때문입니다. 단순히 GET, POST 요청으로 필요한 내용을 수집하기가 쉽지 않습니다.

동적으로 구성한 페이지의 경우 Selenium 을 사용하면 보다 쉽게 수집할 수 있습니다. 이 패키지는 실제 사용자가 브라우저 상에서 클릭하는 것과 같은 동작을 Python 으로 실행할 수 있도록 합니다.

Selenium 을 사용하여 웹 페이지를 수집하는 방법을 살펴보겠습니다.

Selenium 설치

pip 명령을 사용하여 selenium 패키지를 설치합니다.

$ pip install selenum

주요 패키지 import

여기에서 사용하는 패키지는 다음과 같습니다. 이 코드는 Ubuntu 환경에서 실행하기 때문에 Firefox 를 사용합니다. 만약 Windows, OSX 환경에서 실행한다면 Edge, Chrome 을 사용할 수 있습니다.

import functools
import itertools
import time
import traceback
from selenium.webdriver import Firefox
from selenium.webdriver.common.by import By

액션

페이지 내의 링크를 클릭, 저장 등 사용자가 할 수 있는 행위를 액션으로 볼 수 있습니다. selenium 에서 제공하는 함수를 바로 사용하는 것도 가능하지만, 액션들의 조합을 선언적으로 하기 위해 별도로 정의합니다.

sleep

다음 액션을 진행하기 전에 대기하는 액션입니다. 페이지가 완전히 로딩되기 전에 다른 액션을 진행하면 오류가 발생할 수 있습니다.

def sleep(sec, driver):
    time.sleep(sec)

get

브라우저 주소창에 주소를 입력하는 액션입니다.

def get(url, driver):
    driver.get(url)

click

지정한 요소(element)를 클릭하는 액션입니다. 요소의 선택 방법은 css selector 를 사용하며, 주어진 텍스트를 포함하는 요소를 클릭합니다. css selector 를 사용하는 방법은 CSS Selector Reference 를 참고합니다.

def click(text, selector, driver):
    for x in driver.find_elements(By.CSS_SELECTOR, selector):
        if x.text == text:
            x.click()
            break

click_index

지정한 요소들 중에 index 번째 요소를 클릭하는 액션입니다.

def click_index(index, selector, driver):
    driver.find_elements(By.CSS_SELECTOR, selector)[index].click()

save_current_page

현재 페이지를 저장하는 액션입니다.

def save_current_page(filename, driver):
    with open(filename, 'w') as fp:
        fp.write(driver.page_source)

액션 빌드

이제 이 액션들을 어떤 방식으로 조합하고 사용할지에 대해 고민이 필요합니다. 몇 가지 요구사항을 고려해보았습니다.

액션의 조합을 아래와 같이 정의할 있습니다. 이 예시는 네이버 블로그를 주소창에 입력하고, 페이지를 저장하고, 2페이지로 이동하고, 1초 쉬었다가 페이지를 저장합니다.

[
  [
    [get, 'https://blog.naver.com'],
    [save_current_page, 'crawl-blog-naver-com/home-sec-all-page-1.html'],
    [click_index, 1, '.pagination a'],
    [sleep, 1],
    [save_current_page, 'crawl-blog-naver-com/home-all-page-2.html'],
  ],
]

build

앞에서 정의한 액션들을 실행 가능한 구조로 변경합니다.

코드는 한 줄이지만 읽기가 좀 어려울 수 있습니다. 우선 parital 함수를 사용하여 각 액션들을 driver 인수를 받는 함수로 만듭니다. 예를 들어 get(url, driver) 함수는 get(driver) 가 됩니다. 중복리스트 구조를 사용하기 때문에 chain 함수를 사용하여, 플랫하게 변경합니다.

def build(cmds):
    return [ functools.partial(cmd[0], *cmd[1:]) for cmd in itertools.chain(*cmds) ]

액션 실행

run

빌드한 명령들을 드라이버 (예: Firefox, Edge, Chrome) 에 실행합니다.

def run(functions, driver):
    retval = driver
    for f in functions:
        if retval is None:
            return None
        try:
            print("execute: {}".format(f))
            f(driver)
        except Exception as e:
            print(traceback.format_exc())
            retval = None

execute

build, run 함수를 각각 실행하지 않고, 한번에 할 수 있도록 조합(compose)하였습니다.

execute = lambda cmds: functools.partial(run, build(cmds))

실행 결과

네이버 블로그 첫 페이지를 수집하기 위한 예시입니다. 네이버 블로그 홈을 수집하고, 페이지 이동(1, 2, 3 페이지)하여 수집합니다. 두번째 섹션을 수집하고, 페이지 이동(1, 2, 3 페이지)하여 수집합니다.

cmds = [
    [
        [get, 'https://blog.naver.com'],
        [save_current_page, 'crawl-blog-naver-com/home-all-page-1.html'],
        [click_index, 1, '.pagination a'],
        [sleep, 1],
        [save_current_page, 'crawl-blog-naver-com/home-all-page-2.html'],
        [sleep, 1],
        [click_index, 2, '.pagination a'],
        [sleep, 1],
        [save_current_page, 'crawl-blog-naver-com/home-all-page-3.html'],
        [sleep, 1],
    ],
    [
        [click_index, 1, 'div[class*=navigator_category] a'],
        [sleep, 1],
        [save_current_page, 'crawl-blog-naver-com/home-sec-1-page-1.html'],
        [sleep, 1],
        [click_index, 1, '.pagination a'],
        [sleep, 1],
        [save_current_page, 'crawl-blog-naver-com/home-sec-1-page-2.html'],
        [sleep, 1],
        [click_index, 2, '.pagination a'],
        [sleep, 1],
        [save_current_page, 'crawl-blog-naver-com/home-sec-1-page-3.html'],
    ]
]  

이제 이 액션들을 실행해 보겠습니다.

driver = Firefox()
execute(cmds)(driver)
execute: functools.partial(<function get at 0x7f0c388cf640>, 'https://blog.naver.com')
execute: functools.partial(<function save_current_page at 0x7f0c388cc820>, 'crawl-blog-naver-com/home-all-page-1.html')
execute: functools.partial(<function click_index at 0x7f0c388cd6c0>, 1, '.pagination a')
execute: functools.partial(<function sleep at 0x7f0c3887a320>, 1)
execute: functools.partial(<function save_current_page at 0x7f0c388cc820>, 'crawl-blog-naver-com/home-all-page-2.html')
execute: functools.partial(<function sleep at 0x7f0c3887a320>, 1)
execute: functools.partial(<function click_index at 0x7f0c388cd6c0>, 2, '.pagination a')
execute: functools.partial(<function sleep at 0x7f0c3887a320>, 1)
execute: functools.partial(<function save_current_page at 0x7f0c388cc820>, 'crawl-blog-naver-com/home-all-page-3.html')
execute: functools.partial(<function sleep at 0x7f0c3887a320>, 1)
execute: functools.partial(<function click_index at 0x7f0c388cd6c0>, 1, 'div[class*=navigator_category] a')
execute: functools.partial(<function sleep at 0x7f0c3887a320>, 1)
execute: functools.partial(<function save_current_page at 0x7f0c388cc820>, 'crawl-blog-naver-com/home-sec-1-page-1.html')
execute: functools.partial(<function sleep at 0x7f0c3887a320>, 1)
execute: functools.partial(<function click_index at 0x7f0c388cd6c0>, 1, '.pagination a')
execute: functools.partial(<function sleep at 0x7f0c3887a320>, 1)
execute: functools.partial(<function save_current_page at 0x7f0c388cc820>, 'crawl-blog-naver-com/home-sec-1-page-2.html')
execute: functools.partial(<function sleep at 0x7f0c3887a320>, 1)
execute: functools.partial(<function click_index at 0x7f0c388cd6c0>, 2, '.pagination a')
execute: functools.partial(<function sleep at 0x7f0c3887a320>, 1)
execute: functools.partial(<function save_current_page at 0x7f0c388cc820>, 'crawl-blog-naver-com/home-sec-1-page-3.html')

페이지 수집이 잘 되었는지 디렉토리를 살펴봅니다.

  $ ls -al crawl-blog-naver-com
  total 2024
  drwxr-xr-x 2 inforgra inforgra   4096 Aug 25 20:38 ./
  drwxr-xr-x 5 inforgra inforgra   4096 Aug 25 20:38 ../
  -rw-r--r-- 1 inforgra inforgra 340958 Aug 25 20:40 home-all-page-1.html
  -rw-r--r-- 1 inforgra inforgra 342271 Aug 25 20:40 home-all-page-2.html
  -rw-r--r-- 1 inforgra inforgra 341998 Aug 25 20:40 home-all-page-3.html
  -rw-r--r-- 1 inforgra inforgra 340022 Aug 25 20:40 home-sec-1-page-1.html
  -rw-r--r-- 1 inforgra inforgra 340588 Aug 25 20:40 home-sec-1-page-2.html
  -rw-r--r-- 1 inforgra inforgra 342351 Aug 25 20:40 home-sec-1-page-3.html

마치며

사용자의 행동을 액션으로 추상화하고, 액션들을 선언, 빌드, 실행 해 보았습니다. 단 몇 줄의 코드로 동적으로 구성한 페이지를 수집할 수 있습니다.

텍스트를 입력 (예: 검색, 로그인 등), 쿠키 설정 등의 액션들을 생각해 볼 수 있습니다. 모두 같은 방식으로 액션 정의하고, 실행할 수 있습니다.

참고