파이썬 자동화 #4 웹 스크래핑 ②: Playwright로 동적 페이지 다루기
3편에서 requests로 HTML을 받아 BeautifulSoup으로 파싱하는 방법을 익혔습니다. 그런데 같은 코드를 어떤 사이트에 적용하면 이상한 일이 생깁니다. 브라우저에서는 분명 데이터가 보이는데, 코드로 받은 HTML은 빈 껍데기입니다. 이번 글에서는 이런 자바스크립트 렌더링 페이지를 Playwright로 다루는 방법을 정리하겠습니다.
빈 HTML이 오는 이유 #
연습 사이트 quotes.toscrape.com에는 자바스크립트로 렌더링되는 버전인 /js/ 경로가 있습니다. 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 chromiumuv를 쓰고 있다면 uv add playwright 후 uv run playwright install chromium을 실행하면 됩니다. playwright install chromium은 Playwright 전용 크로미움 브라우저를 받는 단계입니다. 평소 쓰는 크롬과는 별개로 관리되므로 기존 브라우저 설정에 영향을 주지 않습니다.
첫 스크립트: 페이지 열고 스크린샷 #
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()) # 10time.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에 대응하는 흐름이라 구조가 그대로 겹쳐 보일 것입니다.
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 메일·알림)에서는 지금까지 만든 스크립트의 결과를 사람에게 전달하는 방법을 다루겠습니다. 스크래핑한 데이터를 메일로 보내고, 슬랙 같은 메신저로 알림을 쏘는 흐름까지 연결하겠습니다.