12. Playwright-Grundlagen
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