데이터을 동적으로 불러오는 웹 페이지를 수집하는 것은 어려운 일입니다. 이런 페이지는 사용자가 행동(클릭)할 때 데이터를 가져오기 때문입니다. 단순히 GET, POST 요청으로 필요한 내용을 수집하기가 쉽지 않습니다.
동적으로 구성한 페이지의 경우 Selenium 을 사용하면 보다 쉽게 수집할 수 있습니다. 이 패키지는 실제 사용자가 브라우저 상에서 클릭하는 것과 같은 동작을 Python 으로 실행할 수 있도록 합니다.
Selenium 을 사용하여 웹 페이지를 수집하는 방법을 살펴보겠습니다.
pip 명령을 사용하여 selenium 패키지를 설치합니다.
$ pip install selenum
여기에서 사용하는 패키지는 다음과 같습니다. 이 코드는 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 에서 제공하는 함수를 바로 사용하는 것도 가능하지만, 액션들의 조합을 선언적으로 하기 위해 별도로 정의합니다.
다음 액션을 진행하기 전에 대기하는 액션입니다. 페이지가 완전히 로딩되기 전에 다른 액션을 진행하면 오류가 발생할 수 있습니다.
def sleep(sec, driver): time.sleep(sec)
브라우저 주소창에 주소를 입력하는 액션입니다.
def get(url, driver): driver.get(url)
지정한 요소(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
지정한 요소들 중에 index 번째 요소를 클릭하는 액션입니다.
def click_index(index, selector, driver): driver.find_elements(By.CSS_SELECTOR, selector)[index].click()
현재 페이지를 저장하는 액션입니다.
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'], ], ]
앞에서 정의한 액션들을 실행 가능한 구조로 변경합니다.
코드는 한 줄이지만 읽기가 좀 어려울 수 있습니다. 우선 parital 함수를 사용하여 각 액션들을 driver 인수를 받는 함수로 만듭니다. 예를 들어 get(url, driver) 함수는 get(driver) 가 됩니다. 중복리스트 구조를 사용하기 때문에 chain 함수를 사용하여, 플랫하게 변경합니다.
def build(cmds): return [ functools.partial(cmd[0], *cmd[1:]) for cmd in itertools.chain(*cmds) ]
빌드한 명령들을 드라이버 (예: 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
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
사용자의 행동을 액션으로 추상화하고, 액션들을 선언, 빌드, 실행 해 보았습니다. 단 몇 줄의 코드로 동적으로 구성한 페이지를 수집할 수 있습니다.
텍스트를 입력 (예: 검색, 로그인 등), 쿠키 설정 등의 액션들을 생각해 볼 수 있습니다. 모두 같은 방식으로 액션 정의하고, 실행할 수 있습니다.