playwright メモ

## 公式
https://playwright.dev/python/

## playwrightのよい点
- webdriverが不要
    - ブラウザとドライバのバージョン違いに煩わされない
- 基本的には明示的なwaitを入れる必要がない
    - ただし、ページや処理によっては必要となるときもある
- コード自動生成機能がある
    - そのままでは使えないことが多いが、コード書き始めのとっかかりに使えることもある

https://playwright.dev/python/docs/codegen-intro#running-codegen

- python対話モードでweb操作ができる
    - こまごまと操作の確認ができる

https://playwright.dev/python/docs/library#interactive-mode-repl

## playwrightのびみょうな点
- seleniumに比べると情報が少ない
    - ただし、公式ドキュメントを見ればたいていのことは何とかなる

## PCにインストール済みのブラウザを使う
公式ドキュメントには `playwright install` でplaywright実行用ブラウザをインストールするくだりがあるが、 `playwright install` をしなくても使える
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3846322/52655357-a8f3-b29b-5fc0-c61d1ec5a2c8.png)

https://playwright.dev/python/docs/library#installation

### インストールした場合の保存先
> %USERPROFILE%\AppData\Local\ms-playwright on Windows
~/Library/Caches/ms-playwright on macOS
~/.cache/ms-playwright on Linux

https://playwright.dev/python/docs/browsers#managing-browser-binaries

### インストール済みのブラウザの使い方
playwright起動時引数に使用するブラウザを指定する
``` python:sync
browser = playwright.chromium.launch(channel="chrome")
```
``` python:async
browser = await playwright.chromium.launch(channel="chrome")
```
https://playwright.dev/python/docs/api/class-browsertype#browser-type-launch

## python対話モードで実行
playwrightの起動・終了のみをpythonの対話モードで行い、他はすべてブラウザを直接操作することも可能
要素の操作だけをコードで確認したい時などに便利
``` python:sync
# 対話モード起動
python

# playwright起動
from playwright.sync_api import sync_playwright
playwright = sync_playwright().start()
browser = playwright.chromium.launch(channel="chrome", headless=False)
page = browser.new_page()

# ページ遷移
# ブラウザを手動操作してページ遷移し、遷移先ページをコード入力で操作することもできる
page.goto("https://playwright.dev/")
page.screenshot(path="example.png")

# playwright終了
browser.close()
playwright.stop()
```
``` python:async
# asyncで対話モード起動
python -m asyncio

# playwright起動
from playwright.async_api import async_playwright
playwright = await async_playwright().start()
browser = await playwright.chromium.launch(channel="chrome", headless=False)
page = await browser.new_page()

# ページ遷移
# ブラウザを手動操作してページ遷移し、遷移先ページをコード入力で操作することもできる
await page.goto("https://playwright.dev/")
await page.screenshot(path="example.png")

# playwright終了
await browser.close()
await playwright.stop()
```
https://playwright.dev/python/docs/library#interactive-mode-repl

## クラス化したいとき
Interactive modeでplaywrightを起動
sync_playwright().start()コールで取得したSyncPlaywrightをクラスメンバとして保持
``` python:sync
from playwright.sync_api import sync_playwright

class Crawler:
    def ___init__():
        pass

    # playwrightの起動
    def start(self):
        self.playwright = sync_playwright().start()
        self.browser = self.playwright.chromium.launch(channel="chrome", headless=False)
        self.page = self.browser.new_page()

    # playwrightの終了
    def close(self):
        self.page.close()
        self.browser.close()
        self.playwright.stop()

crawler = Crawler()
crawler.start()
# ページを開いてスクリーンショットを撮る
crawler.page.goto("https://playwright.dev/")
crawler.page.screenshot(path="example.png")
crawler.close()
```
並列

``` python:async
import asyncio

from playwright.async_api import async_playwright


class Crawler:
    def __init__(self):
        self.playwright = None
        self.browser = None

    # Playwrightの起動
    async def start(self):
        self.playwright = await async_playwright().start()
        self.browser = await self.playwright.chromium.launch(
            channel="chrome", headless=False
        )

    # ページを開いてスクリーンショットを撮る
    async def save_screenshot(self, url, screenshot_path):
        page = await self.browser.new_page()
        await page.goto(url)
        await page.screenshot(path=screenshot_path)
        await page.close()

    # Playwrightの終了
    async def close(self):
        await self.browser.close()
        await self.playwright.stop()


async def main():
    urls = [
        ("https://playwright.dev/", "playwright.png"),
        ("https://www.python.org/", "python.png"),
    ]

    crawler = Crawler()
    await crawler.start()

    # 並列実行
    async with asyncio.TaskGroup() as tg:
        for url, path in urls:
            tg.create_task(crawler.save_screenshot(url, path))

    await crawler.close()


asyncio.run(main())
```

### with構文使用での通常起動時と、Interactive modeの違い

``` python:通常起動
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("http://playwright.dev")
    print(page.title())
    browser.close()
```
sync_playwright()で作っているのはPlaywrightContextManager
with文内でコンテキストマネージャーの__enter__()が走り、SyncPlaywrightが返却される
with文をぬけるときに__exit__()が走り、切断処理が実施される

``` python:Interactive mode
from playwright.sync_api import sync_playwright
playwright = sync_playwright().start()
# Use playwright.chromium, playwright.firefox or playwright.webkit
# Pass headless=False to launch() to see the browser UI
browser = playwright.chromium.launch()
page = browser.new_page()
page.goto("https://playwright.dev/")
page.screenshot(path="example.png")
browser.close()
playwright.stop()
```
sync_playwright().start()が作っているのはSyncPlaywright
start()内で__enter__()が走り、SyncPlaywrightが返却される
以降、明示的なstop()コールを実施するまでSyncPlaywrightを使用できる
stop()コールで__exit__()が走り、切断処理が実施される

## 実行ファイル化したいとき
### Nuitka
`--include-package-data=playwright` を使用
``` :windows
nuitka ^
--standalone ^
--include-package-data=playwright ^
src.py
```
``` :mac
python -m nuitka \
--macos-create-app-bundle \
--standalone \
--include-package-data=playwright \
src.py
```
:::note warn
ver 2.1以降
windows実行ファイルではplaywright処理部分が実行できない
mac実行ファイルでは実行できる
:::

https://nuitka.net/

### cx_Freeze
`build_exe_options = {"includes": ["playwright"]}` を使用
```python:setup.py
from cx_Freeze import setup, Executable

build_exe_options = {
    "includes": ["playwright"],
}

setup(
    name="実行ファイルの名前を指定",
    version="0.1",
    options={"build_exe": build_exe_options},
    executables=[Executable("src.py", base="gui")],
)
```
https://cx-freeze.readthedocs.io/en/stable/

## セッションをつかう
実行毎の多段階認証ログインがめんどうなときなど
```python
from playwright.sync_api import sync_playwright

# セッションを保存
def save_session():
    with sync_playwright() as p:
        browser = p.chromium.launch(channel="chrome", headless=False)
        context = browser.new_context()
        page = context.new_page()

        # === 対象サイトに遷移してログイン処理など ===

        # ログイン後にセッションを保存
        context.storage_state(path="session.json")
        browser.close()

# セッションを使用
def load_session():
    with sync_playwright() as p:
        browser = p.chromium.launch(channel="chrome", headless=False)
        # 保存したセッションを使用
        context = browser.new_context(storage_state="session.json")
        page = context.new_page()
        
        # === 必要な処理を実施 ===

        browser.close()
```
https://playwright.dev/python/docs/auth#reusing-signed-in-state

## 起動済みchromeをつかう
手動操作と自動操作の両方が必要なときなど
```python
import subprocess
from contextlib import contextmanager
from pathlib import Path

from playwright.sync_api import sync_playwright

# chrome起動
@contextmanager
def open_chrome():
    url = "https://playwright.dev/"

    chrome_path = Path(r"C:\Program Files\Google\Chrome\Application\chrome.exe")
    user_dir_path = Path().cwd().joinpath("user_dir")
    options = f"--remote-debugging-port=9222 --no-first-run --no-default-browser-check --user-data-dir={user_dir_path}"

    # chromeを起動
    command = f"{chrome_path} {url} {options}"
    chrome_process = subprocess.Popen(command)

    try:
        yield
    finally:
        # chromeを終了
        chrome_process.terminate()


with open_chrome():
    # chrome接続
    with sync_playwright() as p:
        # 起動済みchromeに接続
        browser = p.chromium.connect_over_cdp("http://localhost:9222")
        default_context = browser.contexts[0]
        page = default_context.pages[0]
        print(page.url)

        browser.close()  # Playwrightで管理するリソースを解放
```
cmdで起動したchromeでもよい
```
"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 --user-data-dir=%CD%\user_dir
```

https://playwright.dev/python/docs/api/class-browsertype#browser-type-connect-over-cdp