파이썬 자동화 #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 playwrightuv 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편과 같습니다. 렌더링이 끝난 페이지에서 locatorinner_text()로 데이터를 뽑아 CSV로 저장합니다. BeautifulSoup의 selectget_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