[실무 강의] 파이썬 Playwright로 웹 페이지 특정 데이터 자동 수집 실습 완벽 마스터 튜토리얼

학습 목표

본 강의의 목표는 현대적인 웹 브라우저 자동화 도구인 Playwright를 사용하여 동적 웹 페이지에서 데이터를 효율적으로 수집하는 기술을 습득하는 것입니다. 과거 Selenium이 주도하던 시장에서 Playwright는 더 빠른 속도, 강력한 자동 대기(Auto-waiting) 기능, 그리고 비동기 프로그래밍 지원을 통해 실무 표준으로 자리 잡았습니다. 이번 실습을 통해 여러분은 복잡한 자바스크립트 렌더링 페이지에서도 막힘없이 데이터를 추출하고, 이를 구조화된 파일로 저장하는 전체 프로세스를 마스터하게 될 것입니다.

사전 준비 사항

본 실습을 진행하기 위해 다음과 같은 개발 환경이 구축되어 있어야 합니다. 2026년 기준 최신 안정화 버전을 권장합니다.

  • 운영체제(OS): Windows 11, macOS Sequoia, 또는 Ubuntu 24.04 LTS 이상
  • 코드 에디터: Visual Studio Code (VSCode) 최신 버전
  • 파이썬 버전: Python 3.11 이상 (비동기 처리를 위한 최적화 버전)
  • 필수 라이브러리 설치: 터미널(Terminal) 또는 명령 프롬프트에서 아래 명령어를 순차적으로 실행하세요.
# Playwright 라이브러리 설치
pip install playwright pandas openpyxl

# Playwright 전용 브라우저 엔진(Chromium, Firefox, WebKit) 설치
playwright install chromium

위 설치 과정이 완료되면 Playwright를 사용하여 실제 브라우저를 제어할 준비가 끝납니다.

단계별 실습 과정

1. Playwright 기본 구조 이해 및 비동기 환경 설정

Playwright는 기본적으로 비동기(Asynchronous) 방식으로 작동할 때 가장 강력한 성능을 발휘합니다. 파이썬의 asyncio 라이브러리를 활용하여 브라우저를 실행하고 페이지를 여는 기본 골격 코드를 작성해 보겠습니다. 이 방식은 여러 페이지를 동시에 처리하거나 네트워크 대기 시간을 효율적으로 관리하는 데 필수적입니다.

import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        # 브라우저 실행 (headless=False로 설정하면 브라우저가 뜨는 것을 볼 수 있습니다)
        browser = await p.chromium.launch(headless=False)
        page = await browser.new_page()
        
        # 대상 URL 접속
        await page.goto('https://news.naver.com/main/main.naver?mode=LSD&mid=shm&sid1=105')
        
        print(f"페이지 제목: {await page.title()}")
        await browser.close()

asyncio.run(main())

2. 대상 웹 사이트 분석 및 CSS 셀렉터 추출

데이터 수집의 핵심은 내가 원하는 정보가 HTML 구조상 어디에 위치하는지 정확히 파악하는 것입니다. 크롬 개발자 도구(F12)를 열어 수집하고자 하는 요소의 클래스(Class) 명이나 ID를 확인합니다. 예를 들어, 뉴스 제목을 수집하고자 한다면 해당 제목을 감싸고 있는 <a> 태그나 <strong> 태그의 셀렉터를 복사합니다.

Playwright는 page.locator() 함수를 통해 강력한 선택 기능을 제공합니다. 단순 CSS 셀렉터뿐만 아니라 텍스트 내용으로 요소를 찾는 get_by_text(), 역할로 찾는 get_by_role() 등 다양한 API를 지원하여 코드의 가독성을 높여줍니다.

3. 다중 데이터 수집 및 자동 대기 처리

실제 실무에서는 단일 데이터가 아닌 목록 형태의 데이터를 수집하는 경우가 많습니다. Playwright의 자동 대기 기능은 요소가 화면에 나타날 때까지 명시적으로 time.sleep()을 호출할 필요 없이 안전하게 데이터를 가져오게 해줍니다. 다음은 특정 뉴스 섹션의 제목과 링크를 모두 수집하는 심화 코드입니다.

async def scrape_news():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page()
        
        # 뉴스 페이지 접속
        await page.goto('https://news.naver.com/section/105')
        
        # 뉴스 아이템 리스트가 로드될 때까지 대기
        await page.wait_for_selector('.sa_text_title')
        
        # 모든 뉴스 제목 요소 선택
        news_elements = await page.query_selector_all('.sa_text_title')
        
        results = []
        for element in news_elements:
            title = await element.inner_text()
            link = await element.get_attribute('href')
            results.append({
                'title': title.strip(),
                'link': link
            })
            
        await browser.close()
        return results

4. 무한 스크롤 및 동적 콘텐츠 대응

많은 현대 웹사이트는 스크롤을 내릴 때마다 새로운 콘텐츠가 로드되는 ‘무한 스크롤’ 방식을 채택합니다. Playwright에서는 page.mouse.wheel() 또는 page.evaluate()를 사용하여 자바스크립트 스크롤 명령을 내릴 수 있습니다. 데이터 수집 전 페이지 끝까지 스크롤하여 모든 데이터를 로드하는 로직을 추가하는 것이 중요합니다.

# 스크롤 다운 로직 예시
for _ in range(5):
    await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
    await page.wait_for_timeout(1000) # 1초 대기하며 로딩 기다림

결과 확인 및 데이터 저장

수집된 데이터는 분석을 위해 보통 CSV 또는 Excel 파일로 저장합니다. 파이썬의 Pandas 라이브러리를 사용하면 리스트 형태의 데이터를 단 몇 줄의 코드로 깔끔한 표 형태로 변환하고 저장할 수 있습니다.

import pandas as pd

# 위에서 수집한 results 리스트를 데이터프레임으로 변환
async def save_data():
    data = await scrape_news()
    df = pd.DataFrame(data)
    
    # 결과 출력
    print(df.head())
    
    # CSV 파일로 저장 (한글 깨짐 방지를 위해 utf-8-sig 사용)
    df.to_csv('it_news_results.csv', index=False, encoding='utf-8-sig')
    print("데이터 저장 완료: it_news_results.csv")

asyncio.run(save_data())

위 코드를 실행하면 터미널에 수집된 데이터의 상단 부분이 출력되며, 작업 디렉토리에 엑셀에서 바로 열 수 있는 CSV 파일이 생성됩니다.

마무리 및 실무 활용 팁

오늘 우리는 Playwright를 활용하여 현대적인 웹 크롤링 파이프라인을 구축해 보았습니다. 이 도구의 가장 큰 장점은 브라우저 컨텍스트를 분리하여 세션을 관리하거나, 네트워크 요청을 가로채서(Intercept) 불필요한 이미지 로딩을 막아 속도를 획기적으로 높일 수 있다는 점입니다.

실무 활용 시 주의사항:
1. Robots.txt 확인: 수집 대상 사이트의 수집 허용 범위를 반드시 확인하세요.
2. User-Agent 설정: 봇으로 차단되는 것을 방지하기 위해 실제 브라우저의 User-Agent 정보를 헤더에 포함하는 것이 좋습니다.
3. 과도한 요청 방지: 짧은 시간에 수천 번의 요청을 보내는 것은 서버에 부담을 주며 IP 차단의 원인이 됩니다. wait_for_timeout을 적절히 섞어 인간의 행동 패턴과 유사하게 구현하세요.

이 튜토리얼을 바탕으로 여러분만의 자동화 도구를 만들어 업무 효율을 극대화해 보시기 바랍니다. 데이터 수집은 단순한 반복 업무를 자동화하는 것을 넘어, 비즈니스 인사이트를 도출하는 가장 첫 번째 단계입니다.

댓글 남기기


Warning: getimagesize(): https:// wrapper is disabled in the server configuration by allow_url_fopen=0 in /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/media.cls.php on line 1158

Warning: getimagesize(https://is1-ssl.mzstatic.com/image/thumb/Music122/v4/ae/61/c5/ae61c561-b95b-239b-3266-f05809ddaa61/artwork.jpg/1200x630wp-60.jpg): Failed to open stream: no suitable wrapper could be found in /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/media.cls.php on line 1158

Warning: getimagesize(): https:// wrapper is disabled in the server configuration by allow_url_fopen=0 in /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/media.cls.php on line 1158

Warning: getimagesize(https://image.yes24.com/goods/95959487/XL): Failed to open stream: no suitable wrapper could be found in /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/media.cls.php on line 1158

Warning: getimagesize(): https:// wrapper is disabled in the server configuration by allow_url_fopen=0 in /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/media.cls.php on line 1158

Warning: getimagesize(https://image.yes24.com/goods/101725726/XL): Failed to open stream: no suitable wrapper could be found in /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/media.cls.php on line 1158

Fatal error: Uncaught ErrorException: file_put_contents(): Write of 19 bytes failed with errno=122 Disk quota exceeded in /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/file.cls.php:177 Stack trace: #0 [internal function]: litespeed_exception_handler() #1 /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/file.cls.php(177): file_put_contents() #2 /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/optimizer.cls.php(135): LiteSpeed\File::save() #3 /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/optimize.cls.php(845): LiteSpeed\Optimizer->serve() #4 /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/optimize.cls.php(338): LiteSpeed\Optimize->_build_hash_url() #5 /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/optimize.cls.php(265): LiteSpeed\Optimize->_optimize() #6 /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/optimize.cls.php(226): LiteSpeed\Optimize->_finalize() #7 /hosting/apdldk/html/wp-includes/class-wp-hook.php(341): LiteSpeed\Optimize->finalize() #8 /hosting/apdldk/html/wp-includes/plugin.php(205): WP_Hook->apply_filters() #9 /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/core.cls.php(464): apply_filters() #10 [internal function]: LiteSpeed\Core->send_headers_force() #11 /hosting/apdldk/html/wp-includes/functions.php(5481): ob_end_flush() #12 /hosting/apdldk/html/wp-includes/class-wp-hook.php(341): wp_ob_end_flush_all() #13 /hosting/apdldk/html/wp-includes/class-wp-hook.php(365): WP_Hook->apply_filters() #14 /hosting/apdldk/html/wp-includes/plugin.php(522): WP_Hook->do_action() #15 /hosting/apdldk/html/wp-includes/load.php(1308): do_action() #16 [internal function]: shutdown_action_hook() #17 {main} thrown in /hosting/apdldk/html/wp-content/plugins/litespeed-cache/src/file.cls.php on line 177