파이썬 자동화 #4 웹 스크래핑 ②: Playwright로 동적 페이지 다루기

5 분 소요

3편에서 requests로 HTML을 받아 BeautifulSoup으로 파싱하는 방법을 익혔습니다. 그런데 같은 코드를 어떤 사이트에 적용하면 이상한 일이 생깁니다. 브라우저에서는 분명 데이터가 보이는데, 코드로 받은 HTML은 빈 껍데기입니다. 이번 글에서는 이런 자바스크립트 렌더링 페이지를 Playwright로 다루는 방법을 정리하겠습니다.

빈 HTML이 오는 이유 #

연습 사이트 quotes.toscrape.com에는 자바스크립트로 렌더링되는 버전인 /js/ 경로가 있습니다. 3편 방식 그대로 받아보겠습니다.

3편 방식으로 받아보기
import requests
from bs4 import BeautifulSoup

html = requests.get("https://quotes.toscrape.com/js/").text
soup = BeautifulSoup(html, "html.parser")
print(len(soup.select(".quote")))  # 0

브라우저로 열면 명언이 10개 보이는데 코드는 0개라고 답합니다. 이 페이지의 서버는 빈 틀의 HTML과 자바스크립트 코드만 보내고, 실제 데이터는 브라우저가 그 자바스크립트를 실행한 뒤에야 화면에 그려지기 때문입니다. requests는 자바스크립트를 실행하지 못하므로 빈 틀만 받습니다. 해법은 한 가지입니다. 자바스크립트를 실행할 수 있는 진짜 브라우저를 코드로 조종하는 것이고, 그 도구가 Playwright입니다.

Playwright 설치 #

설치는 두 단계입니다. 파이썬 패키지를 설치한 뒤, 조종할 브라우저 바이너리를 따로 내려받습니다.

설치
pip install playwright
playwright install chromium

uv를 쓰고 있다면 uv add playwright 후 uv run playwright install chromium을 실행하면 됩니다. playwright install chromium은 Playwright 전용 크로미움 브라우저를 받는 단계입니다. 평소 쓰는 크롬과는 별개로 관리되므로 기존 브라우저 설정에 영향을 주지 않습니다.

첫 스크립트: 페이지 열고 스크린샷 #

first_browser.py
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()  # headless=True가 기본
    page = browser.new_page()
    page.goto("https://quotes.toscrape.com/js/")
    page.screenshot(path="page.png")
    browser.close()

실행하면 화면에는 아무것도 뜨지 않지만 page.png에는 명언 10개가 렌더링된 화면이 찍혀 있습니다. 기본값이 headless 모드, 즉 화면 없이 백그라운드로 도는 브라우저이기 때문입니다. 동작을 눈으로 확인하며 디버깅하고 싶을 때는 p.chromium.launch(headless=False)로 바꾸면 실제 브라우저 창이 뜨면서 코드가 조종하는 과정이 그대로 보입니다.

셀렉터와 대기: wait_for_selector #

동적 페이지 스크래핑의 핵심 개념은 대기입니다. 페이지를 열어도 데이터는 자바스크립트가 실행을 마친 뒤에야 나타나므로, 코드가 너무 일찍 읽으면 또 빈 결과를 받습니다. Playwright의 답은 wait_for_selector입니다.

요소가 나타날 때까지 기다리기
page.goto("https://quotes.toscrape.com/js/")
page.wait_for_selector(".quote")       # .quote가 나타날 때까지 대기
print(page.locator(".quote").count())  # 10

time.sleep(5) 같은 고정 대기와의 차이가 중요합니다. sleep은 데이터가 1초 만에 떠도 5초를 다 기다리고, 5초 안에 안 뜨면 그대로 실패합니다. wait_for_selector는 요소가 나타나는 순간 바로 다음 줄로 진행하고, 기본 30초까지는 계속 기다려 줍니다. 빠를 때는 빠르게, 느릴 때는 끈기 있게 동작하는 셈입니다. 참고로 locator로 클릭이나 텍스트 추출을 할 때는 Playwright가 자동으로 대기해 주므로, 명시적 대기는 페이지 진입 직후처럼 기준점이 필요한 곳에 쓰면 됩니다.

클릭·입력·로그인 #

로그인이 필요한 페이지도 같은 방식으로 처리합니다. 입력은 fill, 클릭은 click입니다.

로그인 자동화
import os

USER = os.environ["QUOTES_USER"]
PASSWORD = os.environ["QUOTES_PASSWORD"]
page.goto("https://quotes.toscrape.com/login")
page.fill("#username", USER)
page.fill("#password", PASSWORD)
page.click("input[type=submit]")
page.wait_for_selector("a[href='/logout']")  # 로그인 성공 확인

한 가지 보안 주의가 필요합니다. 비밀번호를 코드에 직접 적으면 안 됩니다. 코드는 깃에 올라가고 화면에 공유되기 때문입니다. 위 코드처럼 환경변수로 분리하고, 실행 전에 터미널에서 export QUOTES_PASSWORD=비밀번호로 넣어주는 방식이 기본입니다. 마지막 줄의 wait_for_selector도 눈여겨봐 주시기 바랍니다. 로그아웃 링크가 나타났다는 것은 로그인이 실제로 성공했다는 뜻이므로, 다음 단계로 넘어가기 전의 확인 장치 역할을 합니다.

무한 스크롤·더보기 처리 #

스크롤을 내릴 때마다 데이터가 추가로 로드되는 페이지도 흔합니다. 연습 사이트의 /scroll 경로가 정확히 이 구조입니다. 요령은 스크롤을 내리고, 데이터 개수가 더 이상 늘지 않을 때까지 반복하는 것입니다.

무한 스크롤 끝까지 읽기
page.goto("https://quotes.toscrape.com/scroll")
page.wait_for_selector(".quote")
prev = 0
while True:
    page.mouse.wheel(0, 5000)    # 아래로 스크롤
    page.wait_for_timeout(1000)  # 로딩 시간 1초
    count = page.locator(".quote").count()
    if count == prev:            # 더 이상 늘지 않으면 끝 (100개)
        break
    prev = count

더보기 버튼을 눌러야 다음 데이터가 나오는 페이지라면 구조는 더 단순합니다. 버튼이 존재하는 동안 page.click("text=더보기")를 반복하면 됩니다.

데이터 추출해 CSV로 저장 #

마무리는 3편과 같습니다. 렌더링이 끝난 페이지에서 locator와 inner_text()로 데이터를 뽑아 CSV로 저장합니다. BeautifulSoup의 select와 get_text에 대응하는 흐름이라 구조가 그대로 겹쳐 보일 것입니다.

scrape_quotes.py
import csv
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://quotes.toscrape.com/js/")
    page.wait_for_selector(".quote")
    rows = []
    for q in page.locator(".quote").all():
        text = q.locator(".text").inner_text()
        author = q.locator(".author").inner_text()
        rows.append([text, author])
    browser.close()

with open("quotes.csv", "w", newline="", encoding="utf-8-sig") as f:
    writer = csv.writer(f)
    writer.writerow(["quote", "author"])
    writer.writerows(rows)

어느 쪽을 쓸까 #

Playwright가 더 강력하다고 해서 항상 Playwright를 쓸 이유는 없습니다. 브라우저를 통째로 띄우는 만큼 느리고 무겁기 때문입니다.

requests + BeautifulSoup (3편)Playwright (이번 글)
속도·리소스빠르고 가벼움 (HTML 한 번 수신)느리고 무거움 (브라우저 구동)
자바스크립트 렌더링불가가능
로그인·클릭·스크롤제한적가능

판별법도 간단합니다. 브라우저에서 페이지 소스 보기(Ctrl+U)를 열었을 때 원하는 데이터가 보이면 정적 페이지이므로 3편 방식이 가볍고 빠릅니다. 소스에는 없는데 화면에는 보인다면 동적 페이지이므로 이번 글의 Playwright가 필요합니다.

정리 #

이번 글에서 다룬 내용입니다.

  • 빈 HTML의 원인과 선택 기준: 데이터를 자바스크립트가 그리면 requests는 빈 틀만 받으므로, 페이지 소스에 데이터가 없을 때만 Playwright를 사용
  • Playwright 기본기: 설치 두 단계, headless 브라우저로 goto와 스크린샷
  • 핵심 개념: sleep 고정 대기 대신 wait_for_selector로 요소가 나타날 때까지 대기
  • 실전 패턴: 로그인 자동화(비밀번호는 환경변수), 무한 스크롤, CSV 저장

다음 글(#5 메일·알림)에서는 지금까지 만든 스크립트의 결과를 사람에게 전달하는 방법을 다루겠습니다. 스크래핑한 데이터를 메일로 보내고, 슬랙 같은 메신저로 알림을 쏘는 흐름까지 연결하겠습니다.

X