Google Shopping Feed für JTL optimieren – Ein Lösungsansatz für große Datenmengen

von | März 18, 2025 | JTL | 0 Kommentare

Einleitung – Warum diese Lösung notwendig ist

Viele Shopbetreiber, die JTL Wawi und den JTL Shop nutzen, stoßen irgendwann auf erhebliche Probleme mit der Google Shopping Feed-Erstellung. Während der integrierte Feed-Generator für kleinere Shops ausreichend ist, kommt er bei größeren Produktmengen schnell an seine Grenzen. Besonders bei 100.000+ Artikeln treten folgende Herausforderungen auf:

Cronjob-Probleme im JTL Shop – Nach einiger Zeit funktioniert der automatische Feed-Export nicht mehr zuverlässig.

Langsame Feederstellung – Die Performance der JTL-internen Lösung ist nicht für große Datenmengen optimiert.

Unvollständige oder veraltete Daten – Der Shop-Feed wird nur in bestimmten Intervallen aktualisiert und ist daher nie ganz aktuell.

Eingeschränkte Steuerungsmöglichkeiten – Es gibt keine Option, gezielt zu definieren, welche Produkte für Google Ads (Shopping-Anzeigen) und welche für kostenlose Einträge genutzt werden sollen.

Kein Zugriff auf wichtige Kennzahlen – Der JTL-Feed enthält standardmäßig viele wichtige Felder nicht, beispielsweise Felder wie Cost of Goods Sold (COGS), die für profitables Bieten in Google Ads entscheidend sind.

Die Lösung:

Anstatt den JTL-internen Feed zu nutzen, greifen wir direkt auf die JTL Wawi-Datenbank zu und erstellen unseren eigenen maßgeschneiderten Shopping-Feed. Damit haben wir volle Kontrolle über die Daten, die in Google Shopping eingespeist werden – und vermeiden Performance-Probleme.

In diesem Artikel zeige ich unseren individuellen Lösungsansatz:

  • Wie wir die Daten direkt aus der Wawi abrufen
  • Welche SQL-Tabellen notwendig sind
  • Wie wir Berechnungen für Google Ads optimieren
  • Wie wir den Feed in mehrere Dateien splitten
  • Wie der gesamte Prozess automatisiert wird

Dieser Guide richtet sich an Entwickler oder technisch versierte Shopbetreiber, die eine eigene Lösung umsetzen möchten. Wer das nicht selbst realisieren kann oder will, kann sich an uns wenden – wir setzen das individuell um. Neben der Feederstellung bieten wir auch weitere Custom-Code-Lösungen und Automatisierungen für JTL Wawi & Shop an.

Vorbereitung – So baust du deinen eigenen Google Shopping Feed-Prototyp

Bevor wir direkt mit der Feederstellung beginnen, sollten wir zunächst einen Prototypen in Einzelskripten erstellen. Gerade für Anfänger ist es hilfreich, die einzelnen Schritte zuerst in modularen Skripten zu testen, bevor alles in einem großen Automatisierungsskript zusammengeführt wird.

Warum mit Anaconda arbeiten?

Für die Entwicklung und das Testen der Einzelskripte eignet sich Anaconda hervorragend.

Einfache Verwaltung von Python-Paketen – Alle benötigten Bibliotheken wie psycopg2 für die Datenbankanbindung oder requests für das Sitemap-Scraping lassen sich über conda oder pip schnell installieren.
Integrierte Entwicklungsumgebung (Jupyter, Spyder) – Perfekt zum Debuggen einzelner Code-Blöcke.
Virtuelle Umgebungen – Du kannst verschiedene Python-Versionen und Abhängigkeiten verwalten, ohne dein System zu „zumüllen“.

Download & Installation:
Anaconda kann kostenlos unter https://www.anaconda.com heruntergeladen werden. Nach der Installation kannst du entweder mit Jupyter Notebook, Spyder oder direkt aus der Anaconda Prompt heraus arbeiten.

Warum eine externe Datenbank und kein Eingriff in die JTL Wawi-Datenbank?

JTL Wawi verwendet Microsoft SQL Server (MSSQL) als Datenbank. Obwohl wir darauf zugreifen können, ist es riskant, direkt dort Änderungen vorzunehmen:

Die Wawi-Datenbank ist geschäftskritisch – Fehlerhafte Änderungen können zu unerwarteten Problemen führen.
Kein direkter Zugriff für Web-Apps – Falls du später APIs oder Dashboards auf Basis deines Feeds bauen willst, ist MSSQL nicht die flexibelste Wahl.
Bessere Performance & Skalierbarkeit – Durch eine externe Datenbank kann der Feed unabhängig von JTL generiert werden, ohne die Shop- bzw. Wawi-Performance zu belasten.

Deshalb nutzen wir eine externe Datenbank, um dort unseren eigenen optimierten Feed aufzubauen. Wir kopieren nur die relevanten Daten aus der JTL Wawi-Datenbank und verarbeiten sie weiter.

Warum PostgreSQL als externe Datenbank?

PostgreSQL ist eine leistungsstarke Open-Source-Datenbank, die sich hervorragend für große Datenmengen eignet.

Schnell & effizient bei großen Datensätzen
Unterstützt komplexe SQL-Abfragen & JSON-Felder
Leicht in Python und andere Systeme integrierbar
Gute Unterstützung für Skalierung & Automatisierung

Alternativen zu PostgreSQL:

  • MySQL/MariaDB – Ebenfalls Open Source und weit verbreitet
  • SQLite – Super für kleine Projekte, aber nicht ideal für große Feeds
  • Microsoft SQL Server Express – Falls man in der MSSQL-Welt bleiben will

Download & Installation:
PostgreSQL gibt es kostenlos unter https://www.postgresql.org/download/.
Nach der Installation ist das Tool pgAdmin enthalten – eine grafische Benutzeroberfläche zur Verwaltung der Datenbank.

pgAdmin – Das grafische Tool für PostgreSQL

pgAdmin ist das Standard-Management-Tool für PostgreSQL und erleichtert:

Datenbank- und Tabellenverwaltung
Schreiben & Testen von SQL-Abfragen
Import & Export von Daten

Tipp:
Wer lieber mit der Kommandozeile arbeitet, kann psql nutzen – das ist das Terminal-Tool für PostgreSQL.

Den perfekten Google-Shopping-Feed erstellen

Welche Felder sind für den Feed wichtig und welche holen wir aus der JTL Wawi?

Damit unser Google Shopping Feed optimal funktioniert, benötigen wir eine Mischung aus Pflichtfeldern (erforderlich für die Google Produktlistung) und optionalen Feldern, die uns helfen, die Performance und Steuerung zu verbessern.

Die vollständige Liste der Google Produktanforderungen kann unter Google Shopping Produktdatenspezifikation für das Merchant Center eingesehen werden. Die in unserem Blogbeitrag erforderlichen Felder und Zuordnungen können von deinen Bedürfnissen abweichen.

Erforderliche Felder für den Google Shopping Feed

Google Feed-Feld Bedeutung Woher kommt es? (JTL Wawi)
ID [id] Eindeutige Produktkennung (meist Artikelnummer) tartikel.cartnr
Titel [title] Name des Produkts tartikelbeschreibung.cname
Beschreibung [description] Produktbeschreibung tartikelbeschreibung.cbeschreibung
Link [link] URL zur Produktseite sitemap_scraping.productlink
Bildlink [image_link] URL zum Produktbild sitemap_scraping.imagelink
Verfügbarkeit [availability] Ob das Produkt auf Lager ist tlagerbestand.fverfuegbar + tartikel.clagerkleinernull
Preis [price] Verkaufspreis inkl. MwSt. tartikel.fvknetto + tartikel.ksteuerklasse
Marke [brand] Hersteller oder Marke thersteller.cname
GTIN [gtin] EAN oder Barcode tartikel.cbarcode
MPN [mpn] Hersteller-Artikelnummer tartikel.chan
Zustand [condition] Zustand des Produkts (Immer „new“) Statisch „new“

Empfohlene & optionale Felder für bessere Performance

Diese Felder sind nicht zwingend notwendig, aber empfohlen, da sie helfen, die Sichtbarkeit und Steuerung der Produkte zu verbessern.

Google Feed-Feld Bedeutung Woher kommt es? (JTL Wawi)
Google-Produktkategorie [google_product_category] Klassifizierung nach Google-Taxonomie tkategorieartikel.kkategorie Mapping
Sonderangebotspreis [sale_price] Rabattierter Preis tsonderpreise.fnettopreis (falls aktiv)
Sonderangebotszeitraum [sale_price_effective_date] Gültigkeitszeitraum des Sonderpreises tartikelsonderpreis.dstart, dende
Cost of Goods Sold [cost_of_goods_sold] Selbstkosten (Einkaufspreis + Fixkosten) tliefartikel.fEKNetto (nur Standardlieferant)
Versandkosten [shipping] Versandkosten je nach Versandklasse tartikel.kversandklasse
Versandgewicht [shipping_weight] Gewicht des Produkts tartikel.fgewicht
Kostenloser Versand ab [free_shipping_threshold] Schwellenwert für gratis Versand Nur für relevante Klasse kversandklasse = ?

Welche Tabellen & Felder holen wir uns aus der JTL Wawi?

Wir kopieren aus der JTL Wawi nur die relevanten Daten in unsere externe PostgreSQL-Datenbank, um Performance-Probleme zu vermeiden.

Tabellen und Felder, die für den Feed infrage kommen (je nach Bedarf können es auch andere Felder sein):

tartikel – Hauptartikel-Tabelle

Enthält Basisinformationen zu jedem Produkt.

  • cartnr Artikelnummer (ID im Feed)
  • fvknetto Netto-Verkaufspreis
  • ksteuerklasse Steuerklasse (1 = 19%, 2 = 7%) Achtung: Kann abweichen!
  • caktiv Ist das Produkt aktiv?
  • cbarcode GTIN / EAN
  • fgewicht Gewicht für den Versand
  • clagerkleinernull Erlaubt das Produkt Überverkäufe?
  • kversandklasse Versandkosten-Kategorie
  • chan Hersteller-Artikelnummer (MPN)
  • khersteller Hersteller-ID (Verknüpfung zu thersteller)

tartikelbeschreibung – Name & Beschreibung

  • cname Titel
  • cbeschreibung Produktbeschreibung

tlagerbestand – Verfügbarkeitsdaten

  • fverfuegbar Aktueller Lagerbestand

thersteller – Herstellerdaten

  • cname Hersteller-Name (Brand)

tliefartikel – Lieferanten-Preise (EK & COGS)

  • fEKNetto Einkaufspreis netto
  • nstandard Markiert den bevorzugten Lieferanten

tsonderpreise & tartikelsonderpreis – Sonderpreise & Rabattaktionen

  • fnettopreis Sonderpreis netto
  • kkundengruppe, kshop Muss 1 sein (relevant für Endkunden)
  • naktiv Ist der Sonderpreis aktiv?
  • dstart, dende Start- und Enddatum des Sonderpreises
  • nistanzahl, nanzahl Einschränkungen nach Lagerbestand

tkategorieartikel, tkategorie & tkategoriesprache – Kategorisierung für Google

  • kkategorie Kategorie-ID des Artikels
  • koberkategorie Falls das eine Unterkategorie ist, Verknüpfung zur Hauptkategorie
  • cname (in tkategoriesprache) Kategoriename für Mapping zu Google

Der Scraper – So holen wir uns die JTL-Daten

Nachdem wir die grundlegenden Tabellen und deren Struktur definiert haben, geht es nun darum, die relevanten Daten aus der JTL-Wawi in eine externe PostgreSQL-Datenbank zu übertragen. Dies ermöglicht uns eine performante Weiterverarbeitung und volle Kontrolle über den Feed-Aufbau.

Warum brauchen wir einen separaten Scraper?

JTL-Wawi bietet zwar Exportmöglichkeiten, aber diese sind oft langsam, unflexibel und gerade für große Produktmengen (100.000+ Artikel) unpraktikabel. Zudem ist der JTL-Shop-Feed-Generator in größeren Shops fehleranfällig. Ein eigener Scraper ermöglicht uns:

  • Schnellere Verarbeitung: Direktzugriff auf die Datenbank ohne Shop-Export.
  • Flexibilität: Auswahl der relevanten Felder und Berechnungen (z. B. Verkaufspreis mit USt).
  • Bessere Skalierbarkeit: Verarbeitung in optimierten Batches (z. B. 50.000 Datensätze pro Durchlauf).
  • Vollständige Kontrolle: Wir bestimmen, welche Daten in den Feed gelangen.

Grundstruktur des Scrapers

Wir unterteilen den Scraper in mehrere Module:

  1. Datenbankverbindungen – Verbindung zu MSSQL (JTL-Wawi) und PostgreSQL.
  2. Datenabruf aus JTL – Abfrage der benötigten Daten aus der Wawi.
  3. Übertragung in PostgreSQL – Speichern der Daten zur weiteren Verarbeitung.
  4. Optimierung für große Datenmengen – Verarbeitung in Batches.

1. Verbindung zur JTL-Wawi und PostgreSQL

Der erste Schritt ist die Herstellung der Verbindung zu beiden Datenbanken. Wir verwenden:

  • pyodbc für die Verbindung zur MSSQL-Datenbank (JTL-Wawi).
  • psycopg2 für die Verbindung zu PostgreSQL.

Hier ist die Basis unserer Datenbankverbindungen:

import psycopg2
import pyodbc
import os
from dotenv import load_dotenv

# Lade Umgebungsvariablen aus einer .env-Datei
load_dotenv()

# Verbindung zur JTL-Datenbank (MSSQL)
def connect_jtl():
try:
jtl_conn = pyodbc.connect(
'DRIVER={SQL Server};'
'SERVER=<DEIN-SQL-SERVER>;', # Ersetze mit deinem SQL-Servernamen
'DATABASE=<DEINE-DATENBANK>;', # Ersetze mit dem Namen deiner JTL-Datenbank
'UID=<DEIN-BENUTZERNAME>;', # Ersetze mit deinem Datenbank-Benutzernamen
'PWD=<DEIN-PASSWORT>;' # Ersetze mit deinem Passwort
)
jtl_cursor = jtl_conn.cursor()
return jtl_conn, jtl_cursor
except Exception as e:
print(f"Fehler bei der Verbindung zu JTL: {e}")
exit()

# Verbindung zur PostgreSQL-Datenbank
def connect_postgres():
try:
db_params = {
"dbname": os.getenv("PG_DB"), # Daten aus der .env-Datei
"user": os.getenv("PG_USER"),
"password": os.getenv("PG_PASSWORD"),
"host": os.getenv("PG_HOST"),
"port": os.getenv("PG_PORT")
}
pg_conn = psycopg2.connect(**db_params)
pg_cursor = pg_conn.cursor()
return pg_conn, pg_cursor
except Exception as e:
print(f"Fehler bei der Verbindung zu PostgreSQL: {e}")
exit()

2. Abruf der Daten aus der JTL-Datenbank

Wir benötigen verschiedene Tabellen aus der JTL-Datenbank. Dazu gehört unter anderem:

  • tartikel Artikelinformationen wie Preis, Bestand, Gewicht, Hersteller.
  • tartikelbeschreibung Name und Beschreibung der Produkte.
  • tlagerbestand Lagerbestände, um die Verfügbarkeit zu bestimmen.
  • tliefartikel Lieferanteninformationen inklusive Einkaufspreis.
  • tsonderpreise & tartikelsonderpreis Sonderpreise und deren Gültigkeitszeiträume.
  • tkategorieartikel, tkategorie, tkategoriesprache Kategorien für Google Shopping.

Hier ein Beispiel, wie wir die Artikeldaten abrufen:

</pre>
def transfer_tartikel():
jtl_conn, jtl_cursor = connect_jtl()
pg_conn, pg_cursor = connect_postgres()

batch_size = 50000
offset = 0

while True:
query_jtl = f"""
SELECT
kArtikel, cArtNr, fVKNetto,
CASE WHEN cAktiv = 'Y' THEN 1 ELSE 0 END,
nLagerbestand, cBarcode, fGewicht,
CASE WHEN cLagerKleinerNull = 'Y' THEN 1 ELSE 0 END,
fEKNetto, kSteuerklasse, kVersandklasse, cHAN, kHersteller
FROM tArtikel
ORDER BY kArtikel
OFFSET {offset} ROWS FETCH NEXT {batch_size} ROWS ONLY;
"""

jtl_cursor.execute(query_jtl)
data = jtl_cursor.fetchall()

if not data:
break

insert_query_pg = """
INSERT INTO tartikel (kartikel, cartnr, fvknetto, caktiv, nlagerbestand, cbarcode,
fgewicht, clagerkleinernull, feknetto, ksteuerklasse,
kversandklasse, chan, khersteller)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (cartnr) DO UPDATE
SET fvknetto = EXCLUDED.fvknetto,
nlagerbestand = EXCLUDED.nlagerbestand,
cbarcode = EXCLUDED.cbarcode,
fgewicht = EXCLUDED.fgewicht,
feknetto = EXCLUDED.feknetto;
"""

pg_cursor.executemany(insert_query_pg, data)
pg_conn.commit()
offset += batch_size

jtl_cursor.close()
jtl_conn.close()
pg_cursor.close()
pg_conn.close()
<pre>

3. Optimierung für große Datenmengen

JTL-Wawi enthält oft hunderttausende Artikel. Um die Daten effizient zu verarbeiten, nutzen wir:

  • Batch-Verarbeitung Daten werden in Blöcken von 50.000 Zeilen übertragen.
  • ON CONFLICT (cartnr) DO UPDATE Falls der Artikel schon existiert, wird er aktualisiert.
  • Indexes auf PostgreSQL-Seite Performance-Optimierung für spätere Abfragen.

Hier ein Beispiel, wie wir die Sonderpreise aus JTL holen:

</pre>
def transfer_tsonderpreise():
jtl_conn, jtl_cursor = connect_jtl()
pg_conn, pg_cursor = connect_postgres()

query_jtl = """
SELECT kArtikelSonderpreis, kKundenGruppe, kShop, fNettoPreis
FROM tSonderpreise
"""

jtl_cursor.execute(query_jtl)
data = jtl_cursor.fetchall()

insert_query_pg = """
INSERT INTO tsonderpreise (kartikelsonderpreis, kkundengruppe, kshop, fnettopreis)
VALUES (%s, %s, %s, %s)
ON CONFLICT (kartikelsonderpreis, kkundengruppe, kshop) DO UPDATE
SET fnettopreis = EXCLUDED.fnettopreis;
"""

pg_cursor.executemany(insert_query_pg, data)
pg_conn.commit()

jtl_cursor.close()
jtl_conn.close()
pg_cursor.close()
pg_conn.close()
<pre>

Was haben wir bis hierhin erreicht?

Mit diesem Scraper haben wir: Eine performante, skalierbare Lösung zur Datenübertragung. Vollen Zugriff auf alle relevanten Felder für den Google Shopping Feed. Eine separate PostgreSQL-Datenbank als Basis für die Feed-Erstellung.

Im nächsten Schritt kümmern wir uns um die Aufbereitung der Daten für den Google Shopping Feed. Wir definieren die Feed-Struktur und bereiten die Daten so auf, dass sie von Google akzeptiert werden.

Warum ein individueller Feed Vorteile bringt

Feed-Erstellung: Google Shopping optimiert aus der Datenbank generieren

Nachdem wir nun unsere Produktdaten aus der JTL-Datenbank in eine externe PostgreSQL-Datenbank überführt haben, können wir diese in ein Google-konformes Feed-Format umwandeln. Hierbei greifen wir auf berechnete Werte, Filtermechanismen und Kategorisierungen zurück, um den Feed optimal für Google Shopping Ads und kostenlose Einträge zu gestalten.

Warum ein individueller Feed?

  • Standard-Feeds aus JTL sind oft langsam und unflexibel.
  • Wir können alle relevanten Felder aus der Wawi nutzen (inkl. Cost of Goods Sold für profitbasierte Google Ads).
  • Wir entscheiden, welche Produkte in bezahlte Werbung und welche nur in kostenlose Listings kommen.
  • Sonderpreise, Versandkosten, Verfügbarkeiten und Margen lassen sich granular steuern.

Datenbank-Setup für den Feed

Für die Feed-Erstellung haben wir eine eigene Tabelle google_shopping_feed, in die wir regelmäßig aktuelle Daten importieren. Die wichtigsten Spalten sind:

Spalte Beschreibung
id Artikelnummer (SKU)
title Produktname aus tartikelbeschreibung
description Artikelbeschreibung, für Google optimiert
productlink Direkter Link zum Produkt
imagelink Produktbild (aus der Datenbank, nicht JTL-Standard)
price Verkaufspreis inkl. MwSt.
availability Lagerbestand & Überverkauf geprüft
cost_of_goods_sold Einkaufspreis + Nebenkosten
sale_price Falls ein aktiver Sonderpreis existiert
shipping Berechnete Versandkosten und Lieferzeit
included_destination Wo wird das Produkt beworben?
excluded_destination Falls bestimmte Ads ausgeschlossen werden sollen

Google erwartet ein genau definiertes Format für den Feed. Eine vollständige Liste der Pflichtfelder gibt es in der Google Shopping Produktdatenspezifikation.

1. Berechnung von Preis & Verfügbarkeit

Wir müssen den Verkaufspreis aus der JTL-Datenbank nehmen und die MwSt. korrekt aufschlagen:

ROUND(a.fvknetto * (CASE WHEN a.ksteuerklasse = 1 THEN 1.19 ELSE 1.07 END), 2) || ' EUR' AS price

Die Verfügbarkeit wird aus zwei Faktoren bestimmt (Achtung: hier kann es je nach eurer Lagerheatung andere Methoden geben):

CASE 
WHEN a.clagerkleinernull = 1 OR lb.fverfuegbar > 0 THEN 'in_stock'
ELSE 'out_of_stock'
END AS availability

2. Berechnung der Selbstkosten (Cost of Goods Sold)

Einer der Hauptvorteile dieses Feeds ist, dass wir Gewinnmargen in Google Ads nutzen können, anstatt nur auf Umsatz zu optimieren.

CASE 
WHEN la.fEKNetto IS NOT NULL AND la.fEKNetto > 0 THEN 
ROUND(
(la.fEKNetto * (CASE WHEN a.ksteuerklasse = 1 THEN 1.19 ELSE 1.07 END)) + 
(CASE 
WHEN (a.fvknetto * (CASE WHEN a.ksteuerklasse = 1 THEN 1.19 ELSE 1.07 END)) <= 89.99 
THEN 2.98 
ELSE 8.93 
END)
, 2) || ' EUR'
ELSE NULL
END AS cost_of_goods_sold
  • Falls es einen Einkaufspreis (fEKNetto) gibt, wird er übernommen.
  • Auf den EK wird die richtige MwSt. aufgeschlagen.
  • Zusätzlich werden fixe Kosten für Lagerhaltung & Verpackung berechnet:
    • 2,98 EUR bei günstigen Artikeln
    • 8,93 EUR bei teureren Produkten.

Falls fEKNetto = 0, wird die Spalte nicht ausgegeben, da Google cost_of_goods_sold optional behandelt.

3. Sonderpreise & Aktionszeiträume

Falls ein Produkt einen gültigen Sonderpreis hat, wird er in sale_price ausgegeben.

CASE 
WHEN sp.fnettopreis IS NOT NULL THEN 
ROUND(sp.fnettopreis * (CASE WHEN a.ksteuerklasse = 1 THEN 1.19 ELSE 1.07 END), 2) || ' EUR'
ELSE NULL
END AS sale_price
  • Wir prüfen, ob fnettopreis existiert und rechnen die MwSt. auf.
  • Falls kein Sonderpreis existiert, bleibt das Feld leer.

Falls der Sonderpreis nur für einen begrenzten Zeitraum gilt, setzen wir das sale_price_effective_date:

CASE 
WHEN sp.fnettopreis IS NOT NULL AND sp.nistdatum = 1 
AND sp.dstart IS NOT NULL AND sp.dende IS NOT NULL
THEN TO_CHAR(sp.dstart, 'YYYY-MM-DD"T"HH24:MI"+0100"') || ' / ' || TO_CHAR(sp.dende, 'YYYY-MM-DD"T"HH24:MI"+0100"')
ELSE NULL
END AS sale_price_effective_date
  • Falls nistdatum = 1, wird geprüft, ob ein Start- und Enddatum existiert.
  • Die Zeiten werden ISO 8601-konform ausgegeben (YYYY-MM-DDThh:mmZ).

4. Steuerung: Paid Ads oder nur kostenlose Listings?

Google erlaubt es, Produkte nur in kostenlosen Einträgen anzuzeigen oder sie aktiv in Shopping Ads zu bewerben.

Wir steuern dies mit den Spalten included_destination & excluded_destination:

def determine_destinations(price, availability, clagerkleinernull):
included = ['Free_listings', 'Free_local_listings']
excluded = []

if price < 10 or (clagerkleinernull == '0' and availability == 'out_of_stock'):
excluded.extend(['Shopping_ads', 'Display_ads'])
else:
included.extend(['Shopping_ads', 'Display_ads'])

return included, excluded
  • Produkte unter X € oder nicht auf Lager ➝ werden nicht in Ads ausgespielt.
  • Alle anderen landen in Shopping Ads & Display Ads.

In der SQL-Query setzen wir das dann so:

determine_destinations(float(row[A].replace(' EUR', '').strip()), row[X], row[Y])[Z] AS included_destination,
determine_destinations(float(row[A].replace(' EUR', '').strip()), row[X], row[Y])[Z] AS excluded_destination

Wo stehen wir?

Wir haben nun alle wichtigen Produktdaten strukturiert & optimiert und in die google_shopping_feed-Tabelle importiert.
Damit können wir jetzt den Feed als XML generieren, was wir im nächsten Abschnitt behandeln.

Google Shopping Feed als XML generieren

Nachdem wir unsere Produktdaten in der PostgreSQL-Datenbank gespeichert haben, müssen wir daraus eine XML-Datei erstellen, die dem Google Merchant Center-Format entspricht.

Ein paar wichtige Anforderungen von Google:

  • Der Feed muss im XML-Format sein, mit spezifischen <g:>-Tags für jedes Feld.
  • Es sollten im besten Fall maximal 50.000 Produkte pro Datei enthalten sein.
  • CDATA-Tags müssen verwendet werden, um Probleme mit Sonderzeichen zu vermeiden.
  • Die Datei wird am Ende gzip-komprimiert, um Speicherplatz zu sparen.

Das folgende Skript erledigt genau das.

1. XML-Feed Struktur aufbauen

Ein Google Shopping Feed folgt immer einer klaren Struktur:

<rss xmlns:g="http://base.google.com/ns/1.0" version="2.0">
<channel>
<item>
<g:id><![CDATA[12345]]></g:id>
<title><![CDATA[Produktname]]></title>
<description><![CDATA[Produktbeschreibung]]></description>
<link><![CDATA[https://example.com/product12345?tracking=12345]]></link>
<g:image_link><![CDATA[https://example.com/image.jpg]]></g:image_link>
<g:price><![CDATA[19.99 EUR]]></g:price>
<g:availability><![CDATA[in_stock]]></g:availability>
</item>
</channel>
</rss>

Jeder Artikel wird in einem <item>-Tag gespeichert, mit allen notwendigen Google-Tags.

2. Daten aus PostgreSQL abrufen

Zunächst holen wir alle Daten aus unserer google_shopping_feed-Tabelle:

def generate_feeds():
"""Generiert XML-Feeds mit jeweils 50.000 Produkten und speichert sie."""
pg_conn, pg_cursor = connect_postgres()

print(" Hole Daten aus `google_shopping_feed`...")
pg_cursor.execute("SELECT * FROM google_shopping_feed")
products = pg_cursor.fetchall()

print(f" Gesamtanzahl Produkte: {len(products)}")

if not products:
print(" Keine Produkte gefunden. Beende Skript.")
return

3. XML-Dokument erstellen

Das folgende Snippet erzeugt das XML-Grundgerüst:

def create_xml_feed(products, file_index):
"""Erstellt eine XML-Feed-Datei mit maximal 50.000 Produkten pro Datei."""
root = Element("rss", {
"xmlns:g": "http://base.google.com/ns/1.0",
"version": "2.0"
})
channel = SubElement(root, "channel")

Hier wird die rss-Struktur mit dem Google-Namespace (xmlns:g) aufgebaut.

4. Produkte in den Feed einfügen

Nun fügen wir die einzelnen Artikel mit CDATA-Schutz hinzu:

def add_cdata(parent, tag, text):
"""Fügt ein XML-Element mit einem CDATA-Block hinzu, wenn Text vorhanden ist."""
if text:
element = SubElement(parent, tag)
element.text = f"<![CDATA[{text}]]>"
  • Sicherstellung, dass Sonderzeichen (z. B. & oder ") korrekt in Google gelesen werden.
  • Umgeht XML-Parsing-Probleme, indem alles in <![CDATA[]]> gepackt wird.

Dann setzen wir die Produktdaten ein:

for product in products:
item = SubElement(channel, "item")

add_cdata(item, "g:id", product[0])
add_cdata(item, "title", product[2])
add_cdata(item, "description", product[3])

tracked_link = f"{product[4]}{TRACKING_SUFFIX}{product[1]}"
add_cdata(item, "link", tracked_link)

add_cdata(item, "g:image_link", product[5])
add_cdata(item, "g:availability", product[6])
add_cdata(item, "g:price", product[8])

if product[9]:
add_cdata(item, "g:sale_price", product[9])

if product[10]:
add_cdata(item, "g:sale_price_effective_date", product[10])

if product[11]:
add_cdata(item, "g:google_product_category", product[11])

add_cdata(item, "g:brand", product[12])
add_cdata(item, "g:gtin", product[13])
add_cdata(item, "g:mpn", product[14])
add_cdata(item, "g:condition", "new")

Besonderheiten:

  • Die Produkt-URL bekommt einen Tracking-Parameter hinzugefügt.
  • Falls optionale Werte (z. B. sale_price) fehlen, werden sie übersprungen.
  • Alle Google-relevanten Tags werden gesetzt.

5. Versandinformationen & Kategorien

Google erwartet, dass Versandkosten & Lieferzeiten mitgeliefert werden:

add_cdata(item, "g:shipping", product[16])
if product[17]:
add_cdata(item, "g:shipping_weight", product[17]) 
if product[18]:
add_cdata(item, "g:free_shipping_threshold", product[18]) 
  • g:shipping: Enthält Versandkosten & Lieferzeit.
  • g:shipping_weight: Gewicht in Kilogramm.
  • g:free_shipping_threshold: Mindestbestellwert für kostenlosen Versand.

6. Steuerung, wo das Produkt angezeigt wird

Wir können festlegen, ob das Produkt in bezahlten Anzeigen oder nur in kostenlosen Listings erscheint:

if product[20] and isinstance(product[20], list):
for dest in product[20]:
add_cdata(item, "g:included_destination", dest.strip())

if product[19] and isinstance(product[19], list):
for dest in product[19]:
add_cdata(item, "g:excluded_destination", dest.strip())
  • g:included_destination: Standardmäßig Free_listings, kann aber Shopping_ads enthalten.
  • g:excluded_destination: Falls das Produkt nicht in Google Ads auftauchen soll.

7. XML sauber formatieren & speichern

Google erfordert ein sauber formatiertes XML, das wir mit minidom und gzip speichern:

xml_str = minidom.parseString(tostring(root, encoding="unicode")).toprettyxml(indent=" ")

# Fix für CDATA-Fehler
xml_str = xml_str.replace("&lt;![CDATA[", "<![CDATA[").replace("]]&gt;", "]]>")

file_name = f"lmn360_googleshopping_{file_index}.xml"
zip_file_name = f"{file_name}.gz"

with open(file_name, "w", encoding="utf-8") as f:
f.write(xml_str)

with gzip.open(zip_file_name, "wt", encoding="utf-8") as f:
f.write(xml_str)

print(f" Datei erstellt: {zip_file_name}")
  • minidom.parseString(...).toprettyxml() sorgt für lesbares XML.
  • CDATA wird nachträglich korrigiert, um Encoding-Probleme zu vermeiden.
  • Die Datei wird in .xml gespeichert und anschließend gzip-komprimiert.

8. Automatische Erstellung für große Feeds

Falls mehr als 50.000 Produkte im Feed sind, wird er aufgeteilt:

batch_size = 50000
file_index = 1
created_files = []

for i in range(0, len(products), batch_size):
batch = products[i:i + batch_size]
file_name = create_xml_feed(batch, file_index)
created_files.append(file_name)
file_index += 1

print(f" Generierung abgeschlossen. Erstellt: {len(created_files)} Dateien.")
  • Falls mehr als 50.000 Artikel vorhanden sind, wird ein neuer Feed gestartet.
  • Am Ende wird angezeigt, wie viele Dateien generiert wurden.

Fast geschafft!

Mit diesem Skript generieren wir einen sauberen & optimierten Google Shopping Feed. Die Daten sind: Optimiert für Google Ads & kostenlose Listings
Komprimiert für schnellen Upload
Skalierbar für 100.000+ Produkte

Nächster Schritt? Feed hochladen!

Automatisierter Feed-Import ins Google Merchant Center

Damit Google immer die aktuellsten Produktdaten erhält, können wir den generierten Feed direkt automatisch abrufen lassen. Hier gibt es verschiedene Möglichkeiten, je nachdem, welche Infrastruktur man nutzt.

Welche Möglichkeiten gibt es für den Import?

Google Sheets:
Man kann Produkte in einer Google-Tabelle pflegen. Das ist gut für kleine Shops mit wenigen Änderungen, aber für große Shops mit 100.000+ Artikeln ungeeignet.

Content API für Shopping:
Für Händler mit sehr großen Produktkatalogen und häufigen Änderungen bietet Google eine API-Schnittstelle, mit der sich Daten automatisiert übermitteln lassen. Dies erfordert allerdings eine eigene Entwicklung.

Geplanter Abruf per URL (empfohlen für große Shops!)
Das ist die beste Lösung für unsere Feed-Generierung:

  • Der Feed wird auf einem Webserver gespeichert
  • Google ruft die Datei täglich automatisch ab
  • Keine manuelle Arbeit nötig

SFTP oder Google Cloud Storage:
Alternativ kann man die Datei per SFTP oder Google Cloud Storage bereitstellen. Das ist besonders dann sinnvoll, wenn man bereits eine entsprechende Infrastruktur hat.

Wie wir Google für den geplanten Abruf einrichten

Damit Google die Datei automatisch verarbeitet, gehen wir so vor:

Feed in einem öffentlich abrufbaren Webverzeichnis speichern

  • Beispiel: https://www.meinshop.de/google_shopping_feed.xml.gz

Im Google Merchant Center hinterlegen

  • Google Merchant Center öffnen
  • Feeds Neuen Feed hinzufügen
  • „Geplanter Abruf“ wählen
  • Die URL zur Feed-Datei eingeben
  • Abrufhäufigkeit auf täglich setzen

Google holt sich den Feed automatisch

  • Alle 24 Stunden werden die Daten aktualisiert
  • Keine manuelle Arbeit nötig
  • Immer aktuelle Bestände & Preise in Google Shopping

Wir haben es geschafft…

Der geplante Abruf per URL ist die einfachste & effizienteste Methode
Google lädt den Feed automatisch jeden Tag neu
Kein manueller Upload mehr nötig

Fazit – Mehr als nur ein Google Shopping Feed

Mit dieser Anleitung haben wir gezeigt, wie man einen hochperformanten, automatisierten Google Shopping Feed für JTL erstellt – und das komplett unabhängig von den JTL-Shop-Feed-Funktionen.

Das gesamte Skript folgt einer modularen Architektur, in der alle Bausteine sauber in ein Main-Skript integriert werden. So bleibt der Code flexibel und kann jederzeit erweitert werden.

Unendliche Möglichkeiten mit Custom Feeds

Doch das ist erst der Anfang!
Mit dieser Technik lassen sich viele weitere Möglichkeiten umsetzen:

Detaillierte Produktvarianten: Neben Standard-Attributen wie Titel & Preis lassen sich auch Farben, Größen oder besondere Eigenschaften integrieren.

Serverseitiges Einzelprodukt-Tracking:

  • Normalerweise trackt Google Shopping Kampagnen grob auf Anzeigengruppen-Ebene
  • Mit individuellem Tracking auf Artikelebene kann man präzise analysieren, welche Produkte wirklich performen
  • Statt sich nur auf Google Ads Conversiontracking zu verlassen, kann der gesamte Kaufprozess serverseitig erfasst werden – ohne Cookies

Dynamische Preisoptimierung:

  • In Kombination mit Verkaufshistorie, Lagerbestand & Margen könnte man Preise automatisch anpassen
  • Besonders interessant für automatische Rabattaktionen oder Wettbewerbsanalysen

📢 Deine Meinung ist gefragt!

💬 Welche weiteren Anleitungen würdest du dir wünschen?
Wir planen, noch viele weitere Guides zu veröffentlichen – schreib uns deine Wünsche in die Kommentare!

Du willst das nicht selbst umsetzen?
Für alle, die keine Zeit oder Lust haben, das Skript selbst aufzubauen, steht unsere Agentur für Custom Code-Lösungen rund um JTL zur Verfügung. Wir übernehmen die gesamte Einrichtung, Automatisierung und Optimierung für dich!

Kontaktiere uns für eine individuelle Lösung!

0 Kommentare

Einen Kommentar abschicken

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

WordPress Kosten: Was kostet eine WordPress-Website wirklich?

WordPress ist eines der beliebtesten Content-Management-Systeme der Welt – flexibel, leistungsstark und (auf den ersten Blick) kostenlos. Doch was kostet eine WordPress-Website wirklich, wenn man alle Aspekte berücksichtigt? In diesem Artikel zeigen wir dir...

Was ist WordPress? CMS erklärt + Vorteile, Funktionen & Einsatzmöglichkeiten

Wenn du darüber nachdenkst, eine eigene Webseite oder einen Blog zu starten, stolperst du früher oder später über den Begriff WordPress. Aber was ist WordPress eigentlich genau? Und warum nutzen es Millionen Menschen weltweit? In diesem Artikel bekommst du WP einfach...