Der Blog von fm-ProductNode

Einen Produktdaten-Feed aus einem BMEcat-Katalog erzeugen

Süßes
Verfasst von franz-mue am 16. August 2017

Der Beitrag zeigt, wie es mit fm-ProductNode gelingt, mit sehr überschaubarem Aufwand Produktdaten aus einem BMEcat-Produktkatalog herauszupflücken und in ein tabellarisches Textformat umzuwandeln. Derartige CSV-Daten finden insbesondere bei Produktdaten-Feeds Verwendung, die auf eCommerce-Marktplätzen hochzuladen sind.

Update

Der nachfolgende Beitrag bezieht sich auf das inzwischen nicht mehr aktuelle Release 2.0 von fm-ProductNode! Manche Aufrufe und Anweisungen haben sich in neueren Releases geändert.

Ausgangssituation und Vorgehen

Der Beitrag geht von einem mit Hilfe von fm-ProductNode validierten und eingelesenen BMEcat-Katalog aus. Weitere Einzelheiten zu diesem Thema habe ich bereits im vorausgehenden Beitrag beschrieben. Der Katalog soll wieder über das gleiche Alias, nämlich source1 identifizierbar sein.

Der Zugriff auf die benötigten Bestandteile des Produktkataloges steht im vorliegenden Beitrag nicht im Vordergrund; her verweise ich ebenfalls auf den vorausgehenden Beitrag sowie auf die zugehörige Schnittstellen-Dokumentation. Grundkenntnisse zur Twig Template Engine setze ich in dem Beitrag voraus.

Zu den Anforderungen an den Produktdaten-Feed orientiere ich mich grob und beispielhaft an den Vorgaben des Google Merchant Centers zur Produktdatenspezifizierung.

Das Template besorgt sich zu allen Produkten des BMEcat-Katalogs die Attribute und gibt sie in einer tabellarischen Datei aus. Pro Produkt wird der CSV-Datei eine weitere Zeile hinzugefügt.

Das Template aufrufen

Die Konvertierung wird entsprechend den Angaben eines Templates vonstatten gehen. Ich nenne das Template bmecat2Csv. Der Inhalt des Templates wird in der Folge zusammengestellt.

Das fertige Template kann zum Beispiel von der Konsole aus aufgerufen werden mit dem Shell-Kommando

$ php trans.php twig.render --alias source1 --template bmecat2Csv

Bei dem Aufruf wird gleich der zugrunde gelegte BMEcat-Katalog per Alias source1 angegeben.

Vorbereitungen im Template

Vorbereitend beginne ich im Template mit der Festlegung zweier Konstanten, die etwas später benötigt werden:

{% set imageLinkPrefix = 'https://www.huber-electronen.de/image/' %}{%
   set dataFeedPath = dataFeedPath|default('productDataFeed.txt') %}

imageLinkPrefix wird später verwendet, um  einen Bild-Dateinamen zu einer URL zu ergänzen. Das ist notwendig, da im Daten-Feed zu jedem Produkt eine URL zum Produktbild verlangt wird, im der vorliegenden BMEcat-Dokument aber nur der Dateiname gegeben ist.

Die Variable dataFeed gibt den Dateinamen an, unter dem die tabellarischen Produktdaten nach Fertigstellung abgespeichert werden. Bei Bedarf kann der Variablenwert beim Aufruf von der Konsole durch Übergabe einer gleichnamigen Kontext-Variablen überschrieben werden.

Den Zugriff auf die BMEcat-Produktdaten vorbereiten

set language = language|default('deu') %}{%
set viewer = repository.currentCatalog.viewer(language) %}{%

set lastProductIndex = viewer.productCount - 1 %}{%
if lastProductIndex >= 0 %}{%
  ... hier werden die Produktdaten gesammelt, aufbereitet,
      und in einer Tabelle ausgegeben ...
endif %}

Das viewer-Objekt ist der Startpunkt für Abfragen im BMEcat-Dokument.

Da es nur die Sicht in einer einzigen Sprache zulässt, ist vorher erst die Sprach-Variable language festzulegen. Wenn über den Template-Kontext dem Template keine Sprache mitgegeben wurde so wie es bei obigem Konsolen-Aufruf der Fall war, wird hier die Sprache deutsch angenommen (dem entspricht der Sprach-Code deu).

Die Idee ist, in einem Schleifenkonstrukt alle Produkte des Katalogs der Reihe nach auszuwählen; zu jedem Produkt werden die interessierenden Attribute abgefragt.

Bevor die Schleifendurchläufe beginnen, wird erst noch sichergestellt, dass die BMEcat-Datei überhaupt Produkte enthält. Das ist zwar in der Regel der Fall, aber die BMEcat-2005-Spezifikation erlaubt grundsätzlich auch Kataloge ohne Produkte.

Die Ausgabe in die CSV-Datei aufsetzen

{% set header = [
    'ID', 'Titel', 'Beschreibung', 'Link', 'Bildlink',
    'Verfügbarkeit', 'Preis', 'gtin',
    'Zustand', 'nicht_​jugendfrei', 'Versandkosten',
] %}{%
set writer = data.csv.writer(dataFeedPath) %}{%
set result = writer.setDelimiter("\t").setEnclosure('"') %}{%
set result = writer.insertOne(header) %}

Durch die Erweiterungen der in fm-ProductNode eingebetteten Twig Template Engine steht im Template ein Open Source-csv-Objekt zur Verfügung. Das csv-Objekt kümmert sich um die Verarbeitung von tabellarischen Daten. Zur Ausgabe in die Datei wird ein writer-Objekt verwendet, das über das csv-Objekt erhältlich ist.

Vor allen anderen Aktivitäten wird dem writer-Objekt erst einmal mitgeteilt, welches Trennzeichen es verwenden soll, und wie umfangreichere Felder einer Zeile zusammen gehalten werden.

Wenn diese Einstellungen durchgeführt wurden, wird erst einmal eine Kopfzeile als erste Zeile in die CSV-Datei eingefügt. Die Spaltennamen sind in der Variable header abgelegt. Die Spaltennamen entsprechen den Google-Vorgaben.

Die BMEcat-Produktdaten extrahieren und an die Anforderungen anpassen

Als nächstes steht das Durchlaufen aller Produkte im BMEcat-Dokument an. Am Beginn der Schleife wird das Produkt über den Schleifenindex ausgewählt. Zur Vereinfachung der nachfolgenden Befehle werden ein paar Variablen gesetzt:

{% for productIndex in 0..lastProductIndex %}{%
  set product = viewer.productByIndex(productIndex) %}{%
  set properties = product.properties %}{%
  set priceProperties = product.pricePropertiesByIndex.productPriceByIndex %}{%

  ... hier werden die Daten eines Produkts gesammelt, aufbereitet,
      und in einer Tabellenzeile ausgegeben ...
endfor %}

Als Produktattribute fallen die Artikelnummer sowie zwei Produkt-Beschreibungen an:

set id = product.supplierProductId|left(50) %}{%
set shortDescription = properties.shortDescription|left(150) %}{%
set longDescription = properties.longDescription|left(5000)|default('DESCRIPTION MISSING') %}{%

Die ermittelten Werte werden erst einmal durch einen left-Filter geschickt, der alles, was nach einer bestimmten Zeichenkettenlänge noch folgt, abschneidet.

Die Langbeschreibung ist bei BMEcat-2005 kein Pflichtelement; sie darf auch fehlen. Für diesen Fall wird im Wert der Variablen vermerkt, dass die Beschreibung fehlt. Ich mache es mir hier einfach; im Ernstfall sollte mehr Aufwand für die Fehlerbehandlung  spendiert werden.

In den nächsten beiden Tabellenspalten erwartet Google eine Url für eine Produkt-Landing Page sowie eine Url, die auf das Produkt-Foto zeigt:

set productLink = product.mimeInfoByIndex(3).source|left(2000) %}{%
if productLink is not valid_url %}{%
  set productLink = 'INVALID LINK' %}{%
endif %}{%

set imageLink = product.mimeInfoByIndex.source %}{%
set imageLink = imageLinkPrefix ~ imageLink|left(2000) %}{%
if not imageLink matches '/\.(gif|jpg|jpeg|png|bmp|tif|tiff)$/' %}{%
  set imageLink = 'INVALID IMAGE' %}{%
endif %}{%

Die benötigten Informationen sind in den BMEcat-Katalogen innerhalb eines MIME_INFO-Bereiches zu finden. Um die genaue Lage zu bestimmen, ist eventuell vorab eine Analyse des Katalogs erforderlich. Zu diesem Thema verweise ich wieder auf meinen vorausgehenden Beitrag zur BMEcat-Analyse an der Konsole.

Da im BMEcat-Katalog nur der Dateiname zu einem Produkt angegeben ist, aber keine Url, ergänze ich der Einfachheit halber den Dateinamen am Anfang mit einem Präfix namens imageLinkPrefix.

Erwähnenswert ist, dass der Wert der Produkt-Url nach der Entnahme aus dem Katalog überprüft wird: Wenn keine gültige Url angegeben ist, ersetze ich die Variable wie oben mit einem Fehlerhinweis. Der Foto-Link wird auf eine zugelassene Endung des Dateinamens geprüft.

Als nächstes ist der Produktpreis an der Reihe:

set price = priceProperties.amount %}{%
set currency = priceProperties.currency %}{%
set priceType = priceProperties.type %}{%
if priceType == 'net_list' %}{%
  set taxFactor = priceProperties.taxPropertiesByIndex.taxFactor %}{%
  set price = price * (1 + taxFactor) %}{%
endif %}{%
set price = price|number_format(2, '.', ',') ~ " " ~ currency %}{%

Beim Preis erwartet Google, dass der Bruttopreis angegeben wird. Falls im Katalog ein Nettopreis steht (das heißt, als Preistyp net_list angegeben ist), wird noch der Steuersatz aus dem Katalog ermittelt und zum Preis aufaddiert. Mit Hilfe des Filters number_format wird der Preis in die verlangte Form gebracht: Dezimalstelle ist das Komma (","), dahinter folgen noch zwei Stellen. Auch die Währung ist noch aus dem BMEcat-Katalog zu ermitteln und an den Preis anzuhängen.

Zum Schluss darf die eindeutige GTIN-Nummer nicht vergessen werden:

set gtin = properties.internationalPids|first %}{%
set pidType = properties.internationalPidType(gtin) %}{%
if pidType not in [ 'gtin', 'ean' ] %}{%
  set gtin = properties.manufacturerPid|default('GTIN MISSING') %}{%
endif %}{%

In BMEcat-Katalogen sind Produkt-Identifikatoren als internationale Artikelnummern (INTERNATIONAL_PID-Elemente) abgelegt und jeweils mit einem Typ versehen. Die Vorabanalyse des BMEcat-Dokuments zeigt, dass zu einem Produkt nur ein Identifikator angegeben ist. Es reicht also, sich mit dem Filter first auf das erste angegebene Element zu konzentrieren. Da BMEcat auch andere Identifikatoren zulässt, wird hier noch geprüft, ob es sich bei der ermittelten internationalen Artikelnummer tatsächlich um eine GTIN-Nummer handelt. Sicherheitshalber wird auch geprüft, ob das Element im Katalog als EAN-Nummer vermerkt ist. Als Vorläufer der GTIN zählt EAN natürlich auch als geeignete Identifikation mit. Sollte im Katalog keine gültige GTIN-Nummer hinterlegt sein, wird im Sinne von Google ersatzweise die Hersteller-Artikelnummer herangezogen. Ist die Prüfung hier ebenfalls nicht erfolgreich, wird wieder ein Fehlervermerk ausgegeben.

Die ermittelten Werte werden abschließend in eine Liste zusammengeführt und als Zeile in die CSV-Datei ausgegeben:

set productAttributes = [
  id, shortDescription, longDescription,
  productLink, imageLink,
  'in_stock', price,
  gtin, 'neu', 'nein', 'DE::10 EUR',
] %}{%
set result = writer.insertOne(productAttributes) %}{%

Beim genaueren Hinsehen wird offenbar, dass in der auszugebenden Liste noch vier Konstanten ergänzt wurden. Einige der von Google erwarteten Angaben waren in der BMEcat-Datei nicht enthalten. Daher muss eine andere Lösung gefunden werden. In der Praxis wird es wohl nicht immer mit der Angabe derartiger Konstanten getan sein...

Das komplette Template

Da alle Code-Fragmente des Templates inzwischen erstellt sind, gebe ich nachfolgend das resultierende Template als Ganzes an:

{%
set imageLinkPrefix = 'https://www.huber-electronen.de/image/' %}{%
set dataFeedPath = dataFeedPath|default('productDataFeed.txt') %}{%

set language = language|default('deu') %}{%
set viewer = repository.currentCatalog.viewer(language) %}{%
set lastProductIndex = viewer.productCount - 1 %}{%
if lastProductIndex >= 0 %}{%
  set header = [
    'ID', 'Titel', 'Beschreibung', 'Link', 'Bildlink',
    'Verfügbarkeit', 'Preis', 'gtin',
    'Zustand', 'nicht_​jugendfrei', 'Versandkosten',
  ] %}{%
  set writer = data.csv.writer(dataFeedPath) %}{%
  set result = writer.setDelimiter("\t").setEnclosure('"') %}{%
  set result = writer.insertOne(header) %}{%

  for productIndex in 0..lastProductIndex %}{%
    set product = viewer.productByIndex(productIndex) %}{%
    set properties = product.properties %}{%
    set priceProperties = product.pricePropertiesByIndex.productPriceByIndex %}{%

    set id = product.supplierProductId|left(50) %}{%
    set shortDescription = properties.shortDescription|left(150) %}{%
    set longDescription = properties.longDescription|left(5000)|default('DESCRIPTION MISSING') %}{%

    set productLink = product.mimeInfoByIndex(3).source|left(2000) %}{%
    if productLink is not valid_url %}{%
      set productLink = 'INVALID LINK' %}{%
    endif %}{%

    set imageLink = product.mimeInfoByIndex.source %}{%
    set imageLink = imageLinkPrefix ~ imageLink|left(2000) %}{%
    if not imageLink matches '/\.(gif|jpg|jpeg|png|bmp|tif|tiff)$/' %}{%
      set imageLink = 'INVALID IMAGE' %}{%
    endif %}{%

    set price = priceProperties.amount %}{%
    set currency = priceProperties.currency %}{%
    set priceType = priceProperties.type %}{%
    if priceType == 'net_list' %}{%
      set taxFactor = priceProperties.taxPropertiesByIndex.taxFactor %}{%
      set price = price * (1 + taxFactor) %}{%
    endif %}{%
    set price = price|number_format(2, '.', ',') ~ " " ~ currency %}{%

    set gtin = properties.internationalPids|first %}{%
    set pidType = properties.internationalPidType(gtin) %}{%
    if pidType not in [ 'gtin', 'ean' ] %}{%
      set gtin = properties.manufacturerPid|default('GTIN MISSING') %}{%
    endif %}{%

    set productAttributes = [
      id, shortDescription, longDescription,
      productLink, imageLink,
      'in_stock', price,
      gtin, 'neu', 'nein', 'DE::10 EUR',
    ] %}{%
    set result = writer.insertOne(productAttributes) %}{%
  endfor %}{%
endif %}

Die CSV-Datei

Das Result der Template-Abarbeitung sieht etwa wie folgt aus:

ID    Titel    Beschreibung    Link    Bildlink    Verfügbarkeit    Preis    gtin    Zustand    nicht_​jugendfrei    Versandkosten
LSS100200    spaceLYnk    "Logik Controller für die Erstellung ..."    http://www.Huber-electronen.de/download_reach.html    https://www.huber-electronen.de/image/00138874_1.jpg    in_stock    "1,761.20 EUR"    3606480715723    neu    nein    "DE::10 EUR"
LSS100100    homeLYnk    "Logik Controller für die Steuerung..."    http://www.Huber-electronen.de/download_reach.html    https://www.huber-electronen.de/image/00138862_1.jpg    in_stock    "1,404.20 EUR"    3606480595998    neu    nein    "DE::10 EUR"
CCT15838    "IHP+ 1C 18 mm, 1-Kanal"    "Digitale Zeitschaltuhr mit ..."    http://www.Huber-electronen.de/download_reach.html    https://www.huber-electronen.de/image/00143438_0.jpg    in_stock    "185.64 EUR"    3606480769238    neu    nein    "DE::10 EUR"
... uns so weiter ...

Anzumerken ist, dass ich hier Felder in der Breite gekürzt, außerdem Zeilen ausgelassen habe. Doch mir geht es hier nur um das Prinzip.

Fazit und Ausblick

Der Beitrag führt anhand eines Praxisbeispiels die Konvertierung von BMEcat-Produktkatalogdaten in einen CSV-formatierten Produktdaten-Feed vor. Aufgrund der Mächtigkeit der verfügbaren BMEcat- und CSV-Werkzeuge gelingt die Abbildung vom Quellformat ins Zielformat ohne Schwierigkeiten.

Der Beitrag konzentriert sich auf die Konvertierung zwischen den Formaten. In komplexeren Alltagssituationen kann es erforderlich sein, mehrere Datenquellen zusammenzuführen und den Ablauf besser zu strukturieren. Auch hierfür bietet fm-ProductNode Unterstützung. Bereits jetzt sind innerhalb der Templates Komponenten zur Verarbeitung verschiedenartiger Formate vorhanden (zum Beispiel XML, JSON, YAML, Markdown). Und für das kommende geplante Release 2.1 von fm-ProductNode wird die verfügbare Funktionalität für die Templates weiter ausgebaut. Insbesondere ist jedoch geplant, das Arbeiten in den Templates mit mehr Komfort auszustatten.

Tags: BMEcat, CSV, Konvertieren, Produktkatalog, Release2.0, Template, Twig, ProduktdatenFeed
Foto: PublicDomainPictures / pixabay.com

« Einen BMEcat-Produktkatalog an der Konsole analysieren - Neu: Das Release 2.1 von fm-ProductNode »