[Python] 파이썬 크롤링 도구 playwright

728x90

1. playwright란

playwright란 마이크로소프트 사에서 만들고 유지관리하고 있는 크롤링 도구입니다. 매번 드라이버를 연결해야하는 셀레니움과 달리 처음에 한번 크로미넘, 파이어폭스, 웹킷등을 설치하고나면 해당 브라우저를 계속 사용합니다. 물론 셀레니움처럼 설치되어있는 크롬을 사용하는 것도 가능합니다. 장점은 node의 퍼펫티어 구동방식과 유사하여 속도가 매우빠르며 모던 웹페이지에 적합하다고 합니다. 단점으로는 셀레니움에 비해 자료가 적다는 것입니다. 

 

공식문서는 아래와 같습니다.

https://playwright.dev/

 

Fast and reliable end-to-end testing for modern web apps | Playwright

Cross-browser end-to-end testing for modern web apps

playwright.dev

 

 

2. playwright 설치

playwright 설치

pip install playwright

 

브라우저 설치

playwright install

 

 

3. 기본코드

import asyncio
from playwright.async_api import async_playwright

class Crawler:
    def __init__(self):
        pass

    async def crawler(self):
        async with async_playwright() as p:
            browser = await p.chromium.launch(
                ## 옵션 설정 가능함
                headless=False
            )
            context = await browser.new_context()
            page = await context.new_page()

            await page.goto("https://naver.com")

            await asyncio.sleep(10)


crawler = Crawler()
result = asyncio.run(crawler.crawler())

 

위 코드는 비동기방식으로 구현하였습니다. 여러 페이지를 동시에 다루려면 비동기방식이 유리하고 단일 페이지를 순차적으로 다루려면 동기방식과 비동기방식의 차이는 거의 없습니다.

playwright는 기본적으로 headless 모드입니다. 따라서 headless옵션을 false로 해야 브라우저가 실행되는것을 볼 수 있습니다.

with문을 사용하는 이유는 with문 안에서 playwright를 실행하면 close 메서드를 호출하지않아도 자동으로 종료되기 때문입니다. 

 

4. 대기옵션들

1) wait_for_selector -> 요소대기

요소를 대기하는 옵션입니다. 기본으로 CSS SELECTOR 와 동일하며 XPATH 입력시에는 "xpath=xpath값"으로 사용합니다.

예를 들면 아래와 같습니다.

page.wait_for_selector("#inId") #id가 inId인 요소 찾기

page.wait_for_selector(".btnConfirmWrap") # 클래스가 btnConfirmWrap인 요소 찾기

page.wait_for_selector("div.class") # 클래스가 class인 div태그 찾기

page.wait_for_selector(
    "xpath=//table[contains(@class, 'tbl_list4')]"
) # 클래스에 tbl_list4가 포함되어있는 table을 xpath로 찾기

2) wait_for_load_state -> 상태대기

# 페이지 로딩이 완료될 때까지 대기
await page.wait_for_load_state("load")

# DOMContentLoaded 이벤트가 발생할 때까지 대기
await page.wait_for_load_state("domcontentloaded")

# 네트워크가 안정적일 때까지 대기 (네트워크 연결이 최소 0으로 유지)
await page.wait_for_load_state("networkidle")

 

load는 window.onload 이벤트가 발생할 때까지 대기하는 옵션입니다. 모든 리소스(이미지, CSS 등)가 로딩 완료될때까지 대기합니다. domcontentloaded는 DOMContentLoaded 이벤트 발생 시까지 대기합니다. HTML 문서가 파싱되었지만 이미지나 CSS는 로딩 중일 수도 있습니다. 마지막으로 networkidle는 500ms 이상 네트워크 연결이 없는 상태가 될때까지 대기합니다. 네트워크 연결이 없다는 것은 SPA나 AJAX 요청이 끝났다는 의미입니다. 따라서 비동기로 화면이 로딩될때 유용하게 사용합니다.

3) wait_for_event -> 이벤트 발생시까지 대기

wait_for_event의 기본 사용방법은 event = page.wait_for_event(event_name, predicate=None, timeout=None) 입니다. 여기서 event_name은 발생할 이벤트 명이고 predicate는 조건, timeout은 시간입니다. 

이벤트에는 대표적으로 팝업이 뜰때까지 대기하는 것이 있습니다.

특히 팝업이 뜰때까지 대기하는 것은 반드시 page.context.wait_for_event("page")로 호출해야 합니다.

4) wait_for_function -> 함수대기

playwright에는 자바스크립트가 다운되어 특정 함수가 나타날때까지 대기하는 기능도 있습니다. 

사용방법은 아래와 같습니다.

wait_for_function("typeof 함수명 === 'function'")

 

이 기능을 통해 모든 다운로드를 기다리는게 아니라 함수가 나타나면 바로 함수를 실행할 수 있습니다.

5) locator()

locator 자체는 대기 기능이 없습니다. 다만 locator 메서드를 실행하면 Locator객체를 반환하는데 Locator를 이용한 액션들에게는 대기시간이 주어집니다. 예를 들면 click, text_content, is_visible 등이 있습니다.

아래와 같은 방식으로 사용가능합니다.

locator = page.locator("div")

locator.click(timeout=3000)
locator.text_content(timeout=3000)
locator.is_visible(timeout=3000)

5. 팝업 이동

import asyncio
from playwright.async_api import async_playwright

class Crawler:
    def __init__(self):
        pass

    async def crawler(self):
        async with async_playwright() as p:
            browser = await p.chromium.launch(
                ## 옵션 설정 가능함
                headless=False
            )
            context = await browser.new_context()
            page = await context.new_page()

            await page.goto("https://www.lotteimall.com/main/viewMain.lotte")

            ## 페이지가 로딩될때까지 대기
            await page.wait_for_load_state("load")

            ## 로그인 함수 호출
            await page.evaluate("fnChkLogin();")

            ## 팝업 페이지 열리는 것을 감지 후 이동
            login_popup = await page.context.wait_for_event("page")

            ## 최대 3초 동안 id가  login_id인 요소를 찾음
            login_id_input = await login_popup.wait_for_selector("#login_id",state="attached", timeout=3000)
            
            # xpath를 사용하려면 아래처럼 사용
            # login_id_input = await login_popup.wait_for_selector("xpath=//*[@id='login_id']",state="visible",timeout=3000)

            await login_id_input.fill("test")

            await asyncio.sleep(10)


crawler = Crawler()
result = asyncio.run(crawler.crawler())

 

셀레니움과는 달리 driver가 이동하지 않아서 팝업을 별개로 다룰 수 있습니다. 또한 wait_for_selector는 드라이버의 WebdriverWait 옵션과 동일하며, state의 기본값은 visible이며, 요소가 보일때까지 대기합니다. state에 attached를 입력시 요소가 존재할때까지 대기할 수 있습니다.

 

6. 경고창 처리

import asyncio
from playwright.async_api import async_playwright

class Crawler:
    def __init__(self):
        pass

    async def crawler(self):
        async with async_playwright() as p:
            browser = await p.chromium.launch(
                ## 옵션 설정 가능함
                headless=False
            )
            context = await browser.new_context()
            page = await context.new_page()

            page.on("dialog",self.handle_dialog)
            await page.goto("https://www.lotteon.com/p/member/login/common?rtnUrl=https://www.lotteon.com/p/mylotte/index/main")

            login_entry = await page.wait_for_selector("#inId")
            await login_entry.fill("aaaa")

            pw_entry = await page.wait_for_selector("#Password")
            await pw_entry.fill("bbbb")

            btnConfirmWrap = await page.wait_for_selector(".btnConfirmWrap")
            login_btn = await btnConfirmWrap.wait_for_selector("button")
            await login_btn.click()

            await asyncio.sleep(10)
	
    # 경고창 핸들러
    async def handle_dialog(self,dialog):
        print(f"경고창 발생: {dialog.message}")
        await asyncio.sleep(2)
        await dialog.accept()  # confirm/prompt도 accept 가능



crawler = Crawler()
result = asyncio.run(crawler.crawler())

 

playwright에서 경고창은 예외가 아닌 이벤트로 처리합니다. 따라서 page.on('dialog',이벤트핸들러)를 해놓으면 해당 메서드안에서 언제라도 경고창이 뜨면 이벤트 핸들러에서 처리합니다. 또한 경고창이 여러번 발생해도 동일하게 핸들러에서 처리합니다.

위 코드는 롯데온 쇼핑몰에서 일부러 아이디와 비밀번호를 잘못 입력한 후 로그인 시도하는 코드입니다.

 

7. 프레임 이동

playwright에서 프레임은 새로운 객체를 반환합니다. 따라서 page와 별개의 객체로 동작하기 때문에 셀레니움처럼 프레임안으로 옮길 필요가 없어 더욱 안전합니다.

playwright에서 프레임은 frame과 frame_locator 메서드를 가지고 있습니다.

먼저 frame 메서드는 Frame객체를 반환하며, 내부에서 동작합니다. 반면에 frame_locator 메서드는 프레임밖에서 프레임안에 요소를 제어하며 FrameLocator객체를 반환합니다. 

 

두개의 메서드는 아래와 같은 차이가 있습니다.

import asyncio
from playwright.async_api import async_playwright

class Crawler:
    def __init__(self):
        pass

    async def crawler(self):
        async with async_playwright() as p:
            browser = await p.chromium.launch(
                ## 옵션 설정 가능함
                headless=False
            )
            context = await browser.new_context()
            page = await context.new_page()

            page.on("dialog",self.handle_dialog)
            await page.goto("https://news.naver.com/")

            frame1 = page.main_frame # url와 name만으로 프레임을 찾기어려워 page의 main_frame으로 대체
            frame2 = page.frame_locator("xpath=//iframe[@title='구독 및 추천 채널']")

            print(type(frame1)) # <class 'playwright.async_api._generated.Frame'>
            print(type(frame2)) # <class 'playwright.async_api._generated.FrameLocator'>

            print(await frame1.content()) ## html 출력
            print(frame2.nth(0)) # <FrameLocator frame=<Frame name= url='https://news.naver.com/'> selector="xpath=//iframe[@title='구독 및 추천 채널'] >> nth=0">
            await asyncio.sleep(10)

    async def handle_dialog(self,dialog):
        print(f"경고창 발생: {dialog.message}")
        await asyncio.sleep(2)
        await dialog.accept()  # confirm/prompt도 accept 가능



crawler = Crawler()
result = asyncio.run(crawler.crawler())

 

frame 객체는 page객체와 거의 유사하게 동작합니다. 반면에 FrameLocator객체는 프레임 내부의 요소를 제어하는 것은 가능하지만, evaluate등의 자바스크립트 실행을 불가능합니다.