Artikel

12. Playwright-Grundlagen

Allgemein

Playwright ist in diesem System nur die Browser-Schicht. Es erzeugt keine Inhalte und entscheidet nicht, was veröffentlicht wird.

Aufgabe von Playwright:

Browser starten
Session laden
Plattform öffnen
Formular bedienen
Text einfügen
Bild hochladen
Screenshot speichern
Status zurückmelden

Playwright sollte möglichst langweilig arbeiten: keine Tricks, keine Umgehungslogik, keine zufällige Menschensimulation.

12.1 Browser starten

Für das Projekt reicht zunächst Chromium.

Minimalbeispiel:

from playwright.sync_api import sync_playwright


def main() -> None:
    with sync_playwright() as playwright:
        browser = playwright.chromium.launch(headless=True)
        page = browser.new_page()
        page.goto("https://example.com", wait_until="domcontentloaded")
        print(page.title())
        browser.close()


if __name__ == "__main__":
    main()

Für Debugging:

browser = playwright.chromium.launch(headless=False)

Für den Serverbetrieb:

browser = playwright.chromium.launch(headless=True)

Playwright unterscheidet Browser, BrowserContext und Page. Ein Browser kann mehrere isolierte BrowserContexts enthalten; ein Context enthält wiederum Seiten/Tabs. Nicht-persistente Contexts schreiben laut Playwright-Dokumentation keine Browserdaten dauerhaft auf die Platte. (Playwright)

12.2 Persistente Session

Für Social-Media-Plattformen sollte nicht bei jedem Lauf neu eingeloggt werden.

Es gibt zwei praktikable Varianten:

storage_state
persistenter Browser-Kontext

Für dieses Handbuch wird storage_state verwendet.

Vorteil:

einmal manuell einloggen
Cookies und Local Storage speichern
später wiederverwenden
keine Passwortautomatisierung im Normalbetrieb

Playwright kann den Storage State eines BrowserContexts speichern; dieser enthält Cookies, Local Storage und optional IndexedDB-Daten. (Playwright)

Die offizielle Authentifizierungsdokumentation beschreibt genau dieses Muster: authentifizierten Zustand speichern und später in neuen Contexts wiederverwenden. (Playwright)

12.3 Login manuell durchführen

Für den ersten Login wird der Browser sichtbar gestartet.

Beispiel:

from pathlib import Path

from playwright.sync_api import sync_playwright


def manual_login(login_url: str, storage_state_path: Path) -> None:
    storage_state_path.parent.mkdir(parents=True, exist_ok=True)

    with sync_playwright() as playwright:
        browser = playwright.chromium.launch(headless=False)
        context = browser.new_context()
        page = context.new_page()

        page.goto(login_url, wait_until="domcontentloaded")

        print("Login im geöffneten Browser durchführen.")
        print("Danach im Terminal Enter drücken.")

        input()

        context.storage_state(path=str(storage_state_path))
        browser.close()

Aufruf:

from pathlib import Path


def main() -> None:
    manual_login(
        login_url="https://www.linkedin.com/login",
        storage_state_path=Path("storage/sessions/linkedin.json"),
    )


if __name__ == "__main__":
    main()

Terminal:

python -m src.browser_session login linkedin

Wichtig:

2FA manuell erledigen
CAPTCHA nicht automatisieren
Warnungen nicht wegklicken lassen
Session-Datei nicht ins Repository aufnehmen

12.4 storage_state speichern

Der zentrale Aufruf ist:

context.storage_state(path=str(storage_state_path))

Beispiel:

context.storage_state(path="storage/sessions/linkedin.json")

Die Datei enthält Sessiondaten und muss wie ein Zugang behandelt werden.

.gitignore:

storage/sessions/

Die Playwright-Dokumentation empfiehlt, gespeicherte Authentifizierungsdaten in einem ignorierten Verzeichnis abzulegen, da sie sensible Cookies und Header enthalten können. (Playwright)

12.5 Session wiederverwenden

Beim späteren Lauf wird der gespeicherte Zustand in einen neuen Context geladen.

from pathlib import Path

from playwright.sync_api import sync_playwright


def open_with_session(storage_state_path: Path, url: str) -> None:
    if storage_state_path.exists() is False:
        raise RuntimeError("Storage state file does not exist: " + str(storage_state_path))

    with sync_playwright() as playwright:
        browser = playwright.chromium.launch(headless=True)
        context = browser.new_context(storage_state=str(storage_state_path))
        page = context.new_page()

        page.goto(url, wait_until="domcontentloaded")
        print(page.title())

        browser.close()

Playwrights Browser-API dokumentiert storage_state als Option beim Erzeugen eines BrowserContexts, um zuvor gespeicherte Login-Informationen zu laden. (Playwright)

12.6 Screenshots

Screenshots sind Pflicht für Nachvollziehbarkeit.

Speichern nach erfolgreichem Schritt:

page.screenshot(
    path="storage/screenshots/linkedin/post_20260512_001_success.png",
    full_page=True,
)

Speichern bei Fehler:

page.screenshot(
    path="storage/screenshots/linkedin/post_20260512_001_error.png",
    full_page=True,
)

Beispiel:

from pathlib import Path

from playwright.sync_api import Page


class ScreenshotService:
    def save(self, page: Page, screenshot_path: Path) -> None:
        screenshot_path.parent.mkdir(parents=True, exist_ok=True)

        page.screenshot(
            path=str(screenshot_path),
            full_page=True,
        )

Dateinamenschema:

storage/screenshots/{platform}/{reference_id}_{status}.png

Beispiele:

storage/screenshots/linkedin/draft_20260512_001_success.png
storage/screenshots/linkedin/draft_20260512_001_error.png
storage/screenshots/linkedin/draft_20260512_001_login_required.png

12.7 Selektoren stabil halten

Schlechte Selektoren:

page.locator("div:nth-child(4) > button:nth-child(2)")

Bessere Selektoren:

page.get_by_role("button", name="Posten")
page.get_by_role("textbox")
page.get_by_text("Beitrag erstellen")

Playwright unterstützt nutzernahe Selektoren wie Rollen, Text und Labels. Diese sind meist stabiler als tief verschachtelte CSS-Pfade.

Für Social-Media-Plattformen ist trotzdem Vorsicht nötig:

UI ändert sich
Texte ändern sich je Sprache
Buttons können andere Namen haben
A/B-Tests verändern Layouts
Dialoge können dazwischen erscheinen

Empfohlene Strategie:

pro Plattform Adapterklasse
Selektoren zentral definieren
keine Selektoren im Publisher verstreuen
bei Fehler Screenshot speichern
keine endlosen Fallback-Ketten

Beispiel:

from dataclasses import dataclass


@dataclass(frozen=True)
class LinkedInSelectors:
    start_post_button_name: str
    post_textbox_role: str
    submit_button_name: str


LINKEDIN_SELECTORS = LinkedInSelectors(
    start_post_button_name="Beitrag beginnen",
    post_textbox_role="textbox",
    submit_button_name="Posten",
)

Verwendung:

page.get_by_role(
    "button",
    name=LINKEDIN_SELECTORS.start_post_button_name,
).click()

Bei mehrsprachiger Oberfläche kann man Varianten erlauben:

class ButtonClicker:
    def click_first_matching_button(self, page: Page, names: list[str]) -> None:
        for name in names:
            locator = page.get_by_role("button", name=name)

            if locator.count() > 0:
                locator.first.click()
                return

        raise RuntimeError("No matching button found.")

Keine fragile Selektor-Orgie. Wenn die Oberfläche sich grundlegend ändert, soll der Adapter scheitern und einen Screenshot speichern.

12.8 Timeout-Strategie

Timeouts müssen explizit gesetzt werden.

Grundwerte:

Navigation: 30 Sekunden
Elemente: 10 Sekunden
Upload: 60 Sekunden
Posting: 30 Sekunden

Playwright wirft bei abgelaufenen Operationen einen TimeoutError; die Python-Dokumentation zeigt dafür den Import als TimeoutError as PlaywrightTimeoutError. (Playwright)

Beispiel:

from playwright.sync_api import TimeoutError as PlaywrightTimeoutError


try:
    page.get_by_role("button", name="Posten").click(timeout=10_000)
except PlaywrightTimeoutError as exception:
    raise RuntimeError("Post button not found.") from exception

Globale Timeouts pro Page:

page.set_default_timeout(10_000)
page.set_default_navigation_timeout(30_000)

Keine harten Wartezeiten als Standard:

page.wait_for_timeout(5000)

Besser:

page.get_by_role("button", name="Posten").wait_for(timeout=10_000)

Harte Wartezeiten sind nur als letzte Stabilisierung bei Uploads oder UI-Animationen vertretbar, aber nicht als primäre Steuerung.

12.9 Beispiel: browser_session.py

Datei:

src/browser_session.py

import argparse
from dataclasses import dataclass
from pathlib import Path

from playwright.sync_api import Browser
from playwright.sync_api import BrowserContext
from playwright.sync_api import Page
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
from playwright.sync_api import sync_playwright


class BrowserSessionError(Exception):
    pass


@dataclass(frozen=True)
class PlatformSessionConfig:
    platform: str
    login_url: str
    home_url: str
    storage_state_path: Path
    screenshot_path: Path


class PlatformSessionConfigFactory:
    def __init__(self, root_dir: Path) -> None:
        self.root_dir = root_dir

    def create(self, platform: str) -> PlatformSessionConfig:
        if platform == "linkedin":
            return PlatformSessionConfig(
                platform="linkedin",
                login_url="https://www.linkedin.com/login",
                home_url="https://www.linkedin.com/feed/",
                storage_state_path=self.root_dir / "storage" / "sessions" / "linkedin.json",
                screenshot_path=self.root_dir / "storage" / "screenshots" / "linkedin" / "session_check.png",
            )

        if platform == "facebook":
            return PlatformSessionConfig(
                platform="facebook",
                login_url="https://www.facebook.com/login",
                home_url="https://www.facebook.com/",
                storage_state_path=self.root_dir / "storage" / "sessions" / "facebook.json",
                screenshot_path=self.root_dir / "storage" / "screenshots" / "facebook" / "session_check.png",
            )

        if platform == "instagram":
            return PlatformSessionConfig(
                platform="instagram",
                login_url="https://www.instagram.com/accounts/login/",
                home_url="https://www.instagram.com/",
                storage_state_path=self.root_dir / "storage" / "sessions" / "instagram.json",
                screenshot_path=self.root_dir / "storage" / "screenshots" / "instagram" / "session_check.png",
            )

        raise BrowserSessionError("Unsupported platform: " + platform)


class ScreenshotService:
    def save(self, page: Page, screenshot_path: Path) -> None:
        screenshot_path.parent.mkdir(parents=True, exist_ok=True)

        page.screenshot(
            path=str(screenshot_path),
            full_page=True,
        )


class BrowserSessionService:
    def __init__(self, screenshot_service: ScreenshotService) -> None:
        self.screenshot_service = screenshot_service

    def manual_login(self, config: PlatformSessionConfig) -> None:
        config.storage_state_path.parent.mkdir(parents=True, exist_ok=True)

        with sync_playwright() as playwright:
            browser = playwright.chromium.launch(headless=False)
            context = browser.new_context()
            page = context.new_page()

            self._configure_page(page)

            page.goto(
                config.login_url,
                wait_until="domcontentloaded",
                timeout=30_000,
            )

            print("Login im Browser durchführen.")
            print("Danach im Terminal Enter drücken.")

            input()

            context.storage_state(path=str(config.storage_state_path))

            self.screenshot_service.save(
                page=page,
                screenshot_path=config.screenshot_path,
            )

            browser.close()

        print("SESSION_SAVED")
        print(str(config.storage_state_path))

    def check_session(self, config: PlatformSessionConfig, headless: bool) -> None:
        if config.storage_state_path.exists() is False:
            raise BrowserSessionError(
                "Storage state does not exist: "
                + str(config.storage_state_path)
            )

        with sync_playwright() as playwright:
            browser = playwright.chromium.launch(headless=headless)
            context = browser.new_context(storage_state=str(config.storage_state_path))
            page = context.new_page()

            self._configure_page(page)

            try:
                page.goto(
                    config.home_url,
                    wait_until="domcontentloaded",
                    timeout=30_000,
                )
            except PlaywrightTimeoutError as exception:
                self.screenshot_service.save(
                    page=page,
                    screenshot_path=config.screenshot_path,
                )

                browser.close()

                raise BrowserSessionError("Navigation timeout.") from exception

            self.screenshot_service.save(
                page=page,
                screenshot_path=config.screenshot_path,
            )

            title = page.title()

            browser.close()

        print("SESSION_CHECK_OK")
        print(config.platform)
        print(title)
        print(str(config.screenshot_path))

    def _configure_page(self, page: Page) -> None:
        page.set_default_timeout(10_000)
        page.set_default_navigation_timeout(30_000)


def build_service() -> BrowserSessionService:
    screenshot_service = ScreenshotService()

    return BrowserSessionService(
        screenshot_service=screenshot_service,
    )


def main() -> None:
    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers(dest="command", required=True)

    login_parser = subparsers.add_parser("login")
    login_parser.add_argument("platform")

    check_parser = subparsers.add_parser("check")
    check_parser.add_argument("platform")
    check_parser.add_argument("--headed", action="store_true")

    args = parser.parse_args()

    root_dir = Path(__file__).resolve().parent.parent

    config_factory = PlatformSessionConfigFactory(root_dir)
    service = build_service()

    if args.command == "login":
        config = config_factory.create(args.platform)
        service.manual_login(config)
        return

    if args.command == "check":
        config = config_factory.create(args.platform)

        headless = True

        if args.headed is True:
            headless = False

        service.check_session(
            config=config,
            headless=headless,
        )

        return

    raise BrowserSessionError("Unsupported command: " + str(args.command))


if __name__ == "__main__":
    main()

Aufrufe

Manueller Login für LinkedIn:

python -m src.browser_session login linkedin

Session prüfen:

python -m src.browser_session check linkedin

Session sichtbar prüfen:

python -m src.browser_session check linkedin --headed

Facebook:

python -m src.browser_session login facebook
python -m src.browser_session check facebook

Instagram:

python -m src.browser_session login instagram
python -m src.browser_session check instagram

Ergebnis dieses Kapitels

Die Playwright-Basis steht damit:

Browser startet sichtbar oder headless
Login erfolgt manuell
storage_state wird gespeichert
Session wird wiederverwendet
Screenshots werden systematisch erzeugt
Selektoren werden zentral gehalten
Timeouts werden explizit gesetzt
CAPTCHA und Login-Probleme werden nicht automatisiert umgangen

Damit ist die Grundlage für die eigentliche Posting-Engine gelegt.

0 Kommentare