15. Instagram-Automatisierung
Instagram ist für den Verlag vor allem ein Bildkanal. Die Plattform eignet sich weniger für längere fachliche Argumente und stärker für visuelle Formate:
Covermotive
Zitatkarten
Buchästhetik
Autorenmotive
Reihenmotive
kurze literarische Einordnungen
Die Automatisierung sollte hier besonders eng begrenzt sein:
Bild vorbereiten
Caption erzeugen
Hashtags begrenzen
Beitrag im Browser vorbereiten
Screenshot speichern
veröffentlichen oder Dry-Run abbrechen
Instagram unterstützt das Erstellen von Beiträgen über die Desktop-Weboberfläche; der typische Ablauf ist Login, Create-Button, Mediendatei auswählen, Caption ergänzen und teilen. Aktuelle Desktop-Anleitungen beschreiben genau diesen Browser-Workflow. (EvergreenFeed)
15.1 Besonderheiten bei Instagram
Instagram unterscheidet sich von LinkedIn und Facebook in mehreren Punkten.
Bild oder Video steht im Zentrum.
Caption ist sekundär.
Links in Captions sind nicht wie normale Weblinks nutzbar.
Hashtags sind wichtiger, aber schnell störend.
Desktop-UI kann sich ändern.
Upload-Dialoge sind empfindlicher als reine Textfelder.
Für dieses System wird zunächst nur ein einfacher Bildbeitrag unterstützt:
ein Bild
eine Caption
maximal 5 Hashtags
kein automatisches Kommentieren
kein automatisches Liken
kein automatisches Folgen
keine Story-Automatisierung
keine Reels-Automatisierung
Nicht im MVP:
Carousel-Posts
Reels
Stories
DMs
Kommentarautomatisierung
Follower-Interaktionen
Hashtag-Scraping
Die technische Strategie bleibt:
Instagram im Browser öffnen
gespeicherte Session laden
Create-Dialog öffnen
Bild hochladen
Caption einfügen
Dry-Run oder teilen
Screenshot speichern
Status zurückgeben
Playwright unterstützt Datei-Uploads über locator.set_input_files() auf input[type=file]; relative Pfade werden relativ zum aktuellen Arbeitsverzeichnis aufgelöst. (Playwright)
15.2 Bildpflicht
Für Instagram ist ein Bild Pflicht.
Ein Instagram-Post ohne image_path wird abgelehnt.
from pathlib import Path
class InstagramImageValidator:
def validate_required_file(self, image_path: str | None) -> None:
if image_path is None:
raise InstagramPublisherError("Instagram post requires image_path.")
path = Path(image_path)
if path.exists() is False:
raise InstagramPublisherError("Image file does not exist: " + image_path)
if path.is_file() is False:
raise InstagramPublisherError("Image path is not a file: " + image_path)
allowed_suffixes = [
".jpg",
".jpeg",
".png",
".webp",
]
if path.suffix.lower() not in allowed_suffixes:
raise InstagramPublisherError("Unsupported image file type: " + image_path)
Empfohlene Bildgrößen für das Verlagssystem:
Quadratisch: 1080 × 1080
Hochformat: 1080 × 1350
Story/Reel nicht im MVP
Für Buchcover oder Zitatkarten ist 1080 × 1350 meist besser, weil das Motiv im Feed mehr Raum bekommt.
Technische Bildprüfung kann später ergänzt werden:
from pathlib import Path
from PIL import Image
class InstagramImageDimensionValidator:
def validate_dimensions(self, image_path: str) -> None:
path = Path(image_path)
with Image.open(path) as image:
width, height = image.size
allowed_sizes = [
(1080, 1080),
(1080, 1350),
]
if (width, height) not in allowed_sizes:
raise InstagramPublisherError(
"Unexpected Instagram image size: "
+ str(width)
+ "x"
+ str(height)
)
Pillow wäre dafür zusätzliche Abhängigkeit:
pip install pillow
Für das MVP reicht die Dateiprüfung.
15.3 Caption erzeugen
Die Caption wird nicht direkt aus dem Buchtitel erzeugt, sondern aus normalisierten Eingangsdaten.
Geeignete Eingangsdaten:
Buchtitel
Autor
kurze Beschreibung
Zitat
Einordnung
Bildbeschreibung
Tags
Prompt-Datei:
prompts/instagram_caption.txt
Du schreibst eine Instagram-Caption für einen kleinen deutschen E-Book-Verlag.
Thema:
{{ title }}
Ausgangsmaterial:
{{ body }}
Autor:
{{ author }}
Buchtitel:
{{ book_title }}
Bildbeschreibung:
{{ image_description }}
Ziel:
Die Caption soll Bildmotiv, literarischen Kontext und Verlag sachlich verbinden.
Stil:
- knapp
- bildnah
- literarisch, aber nicht kitschig
- keine übertriebene Werbung
- keine Emojis
- keine Floskeln
Regeln:
- maximal 700 Zeichen
- maximal 5 Hashtags
- keine Hashtags im Fließtext
- kein direkter Kaufdruck
- keine erfundenen Fakten
- keine Formulierungen wie „perfekt für gemütliche Abende“
- keine direkte Ansprache mit „Du“
Ausgabe:
Erzeuge nur die Caption.
Caption-Modell:
from dataclasses import dataclass
@dataclass(frozen=True)
class InstagramCaptionRequest:
title: str
body: str
author: str
book_title: str
image_description: str
tags: list[str]
Caption-Builder:
class InstagramCaptionBuilder:
def __init__(
self,
prompt_renderer: PromptTemplateRenderer,
openai_client: OpenAiClient,
hashtag_generator: "InstagramHashtagGenerator",
safety_identifier: str,
model: str,
) -> None:
self.prompt_renderer = prompt_renderer
self.openai_client = openai_client
self.hashtag_generator = hashtag_generator
self.safety_identifier = safety_identifier
self.model = model
def create_caption(self, caption_request: InstagramCaptionRequest) -> str:
input_text = self.prompt_renderer.render(
"instagram_caption.txt",
{
"title": caption_request.title,
"body": caption_request.body,
"author": caption_request.author,
"book_title": caption_request.book_title,
"image_description": caption_request.image_description,
},
)
request = OpenAiTextRequest(
model=self.model,
instructions="Erzeuge sachliche Instagram-Captions für einen deutschen E-Book-Verlag.",
input_text=input_text,
safety_identifier=self.safety_identifier,
max_output_tokens=500,
)
result = self.openai_client.create_text(request)
caption = result.text.strip()
hashtags = self.hashtag_generator.generate(caption_request.tags)
if len(hashtags) > 0:
caption = caption.rstrip() + "\n\n" + " ".join(hashtags)
self._validate(caption)
return caption
def _validate(self, caption: str) -> None:
if caption.strip() == "":
raise InstagramPublisherError("Instagram caption must not be empty.")
if len(caption) > 700:
raise InstagramPublisherError("Instagram caption exceeds internal limit.")
15.4 Bild hochladen
Der Upload erfolgt im Create-Dialog.
Typischer Ablauf:
Instagram öffnen
Create-Button klicken
Dateiinput finden
Bildpfad setzen
Weiter klicken
Caption einfügen
Dry-Run oder teilen
UI-Texte können je nach Sprache abweichen. Deshalb werden mehrere Varianten unterstützt:
class InstagramButtonClicker:
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(timeout=10_000)
return
for name in names:
locator = page.get_by_text(name, exact=True)
if locator.count() > 0:
locator.first.click(timeout=10_000)
return
raise InstagramPublisherError("No matching Instagram button found.")
Create öffnen:
button_clicker.click_first_matching_button(
page=page,
names=[
"Erstellen",
"Create",
"Neue Veröffentlichung",
"New post",
],
)
Datei setzen:
file_input = page.locator("input[type=file]").first
file_input.set_input_files(image_path)
Wenn Instagram statt sichtbarem Dateiinput einen FileChooser öffnet:
with page.expect_file_chooser() as file_chooser_info:
button_clicker.click_first_matching_button(
page=page,
names=[
"Vom Computer auswählen",
"Select from computer",
],
)
file_chooser = file_chooser_info.value
file_chooser.set_files(image_path)
Playwright dokumentiert dafür FileChooser.set_files(); bei direktem input[type=file] ist locator.set_input_files() meist einfacher. (Playwright)
15.5 Beitrag veröffentlichen
Für Instagram sind mehrere „Weiter“-Schritte üblich.
Ablauf im Adapter:
Upload
Weiter
optional Zuschnitt/Originalgröße
Weiter
Caption einfügen
Dry-Run-Screenshot
Teilen
Abschluss-Screenshot
Caption einfügen:
caption_box = page.get_by_role("textbox").first
caption_box.click(timeout=10_000)
page.keyboard.insert_text(caption)
Absenden:
button_clicker.click_first_matching_button(
page=page,
names=[
"Teilen",
"Share",
],
)
Nach dem Klick nicht blind wiederholen.
Wenn unklar ist, ob der Post veröffentlicht wurde:
Screenshot speichern
Status failed setzen
manuelle Prüfung
Dry-Run-Regel:
Dry-Run endet nach Caption-Eingabe vor „Teilen“.
15.6 Hashtags begrenzen
Hashtags werden regelbasiert aus Tags erzeugt.
Limit:
maximal 5 Hashtags
Generator:
class InstagramHashtagGenerator:
def generate(self, tags: list[str]) -> list[str]:
hashtags: list[str] = []
for tag in tags:
hashtag = self._normalize(tag)
if hashtag == "":
continue
if hashtag in hashtags:
continue
hashtags.append(hashtag)
if len(hashtags) >= 5:
break
return hashtags
def _normalize(self, tag: str) -> str:
cleaned_tag = tag.strip().lower()
if cleaned_tag == "":
return ""
replacements: dict[str, str] = {
"ä": "ae",
"ö": "oe",
"ü": "ue",
"ß": "ss",
"-": "",
" ": "",
}
for search, replacement in replacements.items():
cleaned_tag = cleaned_tag.replace(search, replacement)
allowed_characters = "abcdefghijklmnopqrstuvwxyz0123456789"
normalized = ""
for character in cleaned_tag:
if character in allowed_characters:
normalized = normalized + character
if normalized == "":
return ""
return "#" + normalized
Beispiel:
generator = InstagramHashtagGenerator()
hashtags = generator.generate(
[
"klassiker",
"Jane Austen",
"E-Book",
"gemeinfreie Literatur",
"Verlag",
"Buchmarketing",
]
)
print(hashtags)
Ausgabe:
['#klassiker', '#janeausten', '#ebook', '#gemeinfreieliteratur', '#verlag']
Verbotene Hashtags können ergänzt werden:
class InstagramHashtagPolicy:
def __init__(self) -> None:
self.blocked_hashtags = [
"#followforfollow",
"#likeforlike",
"#viral",
"#instagood",
]
def validate(self, hashtags: list[str]) -> None:
for hashtag in hashtags:
if hashtag.lower() in self.blocked_hashtags:
raise InstagramPublisherError("Blocked hashtag: " + hashtag)
15.7 Beispiel: instagram_publisher.py
Datei:
src/platforms/instagram_publisher.py
from dataclasses import dataclass
from pathlib import Path
from playwright.sync_api import Page
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
from playwright.sync_api import sync_playwright
class InstagramPublisherError(Exception):
pass
class InstagramBlockedError(InstagramPublisherError):
pass
@dataclass(frozen=True)
class InstagramPublishRequest:
id: str
caption: str
image_path: str
storage_state_path: Path
screenshot_dir: Path
dry_run: bool
@dataclass(frozen=True)
class InstagramPublishResult:
post_id: str
status: str
screenshot_path: str
platform_post_url: str | None
error_message: str | None
class InstagramScreenshotService:
def save(self, page: Page, screenshot_dir: Path, post_id: str, status: str) -> Path:
screenshot_dir.mkdir(parents=True, exist_ok=True)
screenshot_path = screenshot_dir / (post_id + "_" + status + ".png")
page.screenshot(
path=str(screenshot_path),
full_page=True,
)
return screenshot_path
class InstagramImageValidator:
def validate_required_file(self, image_path: str | None) -> None:
if image_path is None:
raise InstagramPublisherError("Instagram post requires image_path.")
path = Path(image_path)
if path.exists() is False:
raise InstagramPublisherError("Image file does not exist: " + image_path)
if path.is_file() is False:
raise InstagramPublisherError("Image path is not a file: " + image_path)
allowed_suffixes = [
".jpg",
".jpeg",
".png",
".webp",
]
if path.suffix.lower() not in allowed_suffixes:
raise InstagramPublisherError("Unsupported image file type: " + image_path)
class InstagramButtonClicker:
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(timeout=10_000)
return
for name in names:
locator = page.get_by_text(name, exact=True)
if locator.count() > 0:
locator.first.click(timeout=10_000)
return
raise InstagramPublisherError("No matching Instagram button found.")
class InstagramCaptionValidator:
def validate(self, caption: str) -> None:
cleaned_caption = caption.strip()
if cleaned_caption == "":
raise InstagramPublisherError("Instagram caption must not be empty.")
if len(cleaned_caption) > 700:
raise InstagramPublisherError("Instagram caption exceeds internal limit.")
blocked_fragments = [
"jetzt kaufen",
"unbedingt lesen",
"follow for follow",
"like for like",
]
lowered_caption = cleaned_caption.lower()
for blocked_fragment in blocked_fragments:
if blocked_fragment in lowered_caption:
raise InstagramPublisherError("Blocked caption fragment: " + blocked_fragment)
class InstagramTextInserter:
def insert(self, page: Page, text: str) -> None:
for character in text:
if character == "\n":
page.keyboard.press("Shift+Enter")
else:
page.keyboard.insert_text(character)
class InstagramPublisher:
def __init__(self) -> None:
self.screenshot_service = InstagramScreenshotService()
self.image_validator = InstagramImageValidator()
self.button_clicker = InstagramButtonClicker()
self.caption_validator = InstagramCaptionValidator()
self.text_inserter = InstagramTextInserter()
def publish(self, request: InstagramPublishRequest) -> InstagramPublishResult:
self._validate_request(request)
with sync_playwright() as playwright:
browser = playwright.chromium.launch(headless=True)
context = browser.new_context(storage_state=str(request.storage_state_path))
page = context.new_page()
page.set_default_timeout(10_000)
page.set_default_navigation_timeout(30_000)
try:
page.goto(
"https://www.instagram.com/",
wait_until="domcontentloaded",
timeout=30_000,
)
self._assert_logged_in(page)
self._open_create_dialog(page)
self._upload_image(page, request.image_path)
self._continue_after_upload(page)
self._insert_caption(page, request.caption)
if request.dry_run is True:
screenshot_path = self.screenshot_service.save(
page=page,
screenshot_dir=request.screenshot_dir,
post_id=request.id,
status="dry_run",
)
browser.close()
return InstagramPublishResult(
post_id=request.id,
status="dry_run",
screenshot_path=str(screenshot_path),
platform_post_url=None,
error_message=None,
)
self._share(page)
screenshot_path = self.screenshot_service.save(
page=page,
screenshot_dir=request.screenshot_dir,
post_id=request.id,
status="published",
)
browser.close()
return InstagramPublishResult(
post_id=request.id,
status="published",
screenshot_path=str(screenshot_path),
platform_post_url=None,
error_message=None,
)
except InstagramBlockedError as exception:
screenshot_path = self.screenshot_service.save(
page=page,
screenshot_dir=request.screenshot_dir,
post_id=request.id,
status="blocked",
)
browser.close()
return InstagramPublishResult(
post_id=request.id,
status="blocked",
screenshot_path=str(screenshot_path),
platform_post_url=None,
error_message=str(exception),
)
except PlaywrightTimeoutError as exception:
screenshot_path = self.screenshot_service.save(
page=page,
screenshot_dir=request.screenshot_dir,
post_id=request.id,
status="failed",
)
browser.close()
return InstagramPublishResult(
post_id=request.id,
status="failed",
screenshot_path=str(screenshot_path),
platform_post_url=None,
error_message=str(exception),
)
except InstagramPublisherError as exception:
screenshot_path = self.screenshot_service.save(
page=page,
screenshot_dir=request.screenshot_dir,
post_id=request.id,
status="failed",
)
browser.close()
return InstagramPublishResult(
post_id=request.id,
status="failed",
screenshot_path=str(screenshot_path),
platform_post_url=None,
error_message=str(exception),
)
def _validate_request(self, request: InstagramPublishRequest) -> None:
if request.storage_state_path.exists() is False:
raise InstagramPublisherError(
"Storage state file does not exist: "
+ str(request.storage_state_path)
)
self.image_validator.validate_required_file(request.image_path)
self.caption_validator.validate(request.caption)
def _assert_logged_in(self, page: Page) -> None:
current_url = page.url.lower()
if "login" in current_url:
raise InstagramBlockedError("Instagram login required.")
body_text = page.locator("body").inner_text(timeout=10_000).lower()
blocked_fragments = [
"captcha",
"security check",
"sicherheitsüberprüfung",
"unusual activity",
"verdächtige aktivität",
]
for blocked_fragment in blocked_fragments:
if blocked_fragment in body_text:
raise InstagramBlockedError(
"Blocked Instagram state detected: "
+ blocked_fragment
)
def _open_create_dialog(self, page: Page) -> None:
self.button_clicker.click_first_matching_button(
page=page,
names=[
"Erstellen",
"Create",
"Neue Veröffentlichung",
"New post",
],
)
def _upload_image(self, page: Page, image_path: str) -> None:
file_inputs = page.locator("input[type=file]")
if file_inputs.count() > 0:
file_inputs.first.set_input_files(image_path)
page.wait_for_timeout(3000)
return
with page.expect_file_chooser() as file_chooser_info:
self.button_clicker.click_first_matching_button(
page=page,
names=[
"Vom Computer auswählen",
"Select from computer",
],
)
file_chooser = file_chooser_info.value
file_chooser.set_files(image_path)
page.wait_for_timeout(3000)
def _continue_after_upload(self, page: Page) -> None:
self.button_clicker.click_first_matching_button(
page=page,
names=[
"Weiter",
"Next",
],
)
page.wait_for_timeout(1000)
next_buttons = page.get_by_text("Weiter", exact=True)
if next_buttons.count() > 0:
next_buttons.first.click(timeout=10_000)
page.wait_for_timeout(1000)
return
next_buttons_english = page.get_by_text("Next", exact=True)
if next_buttons_english.count() > 0:
next_buttons_english.first.click(timeout=10_000)
page.wait_for_timeout(1000)
return
def _insert_caption(self, page: Page, caption: str) -> None:
caption_boxes = page.get_by_role("textbox")
if caption_boxes.count() == 0:
raise InstagramPublisherError("Caption textbox not found.")
caption_box = caption_boxes.first
caption_box.click(timeout=10_000)
self.text_inserter.insert(page, caption)
def _share(self, page: Page) -> None:
self.button_clicker.click_first_matching_button(
page=page,
names=[
"Teilen",
"Share",
],
)
page.wait_for_timeout(5000)
Beispielaufruf
from pathlib import Path
from src.platforms.instagram_publisher import InstagramPublisher
from src.platforms.instagram_publisher import InstagramPublishRequest
def main() -> None:
root_dir = Path(__file__).resolve().parent.parent
request = InstagramPublishRequest(
id="draft_20260512_002",
caption=(
"Ein Klassiker lebt nicht nur vom Text, sondern auch von der Form, "
"in der er heute gelesen wird. Eine digitale Ausgabe braucht "
"Korrektur, Typografie und klare Metadaten.\n\n"
"#klassiker #ebook #verlag"
),
image_path=str(root_dir / "storage" / "images" / "stolz-und-vorurteil.jpg"),
storage_state_path=root_dir / "storage" / "sessions" / "instagram.json",
screenshot_dir=root_dir / "storage" / "screenshots" / "instagram",
dry_run=True,
)
publisher = InstagramPublisher()
result = publisher.publish(request)
print(result)
if __name__ == "__main__":
main()
Ergebnis dieses Kapitels
Der Instagram-Adapter kann damit:
gespeicherte Session verwenden
Login- und Sicherheitszustände erkennen
Bildpflicht prüfen
Caption validieren
Create-Dialog öffnen
Bilddatei hochladen
Caption einfügen
Hashtags begrenzen
Dry-Run vor dem Teilen abbrechen
Beitrag veröffentlichen
Screenshot speichern
Status zurückgeben
Instagram bleibt im MVP ein reiner Veröffentlichungskanal für eigene Bildbeiträge. Interaktionen, Likes, Follows, DMs, Stories und Reels werden nicht automatisiert.
0 Kommentare