Der Blog von fm-ProductNode

Produktdaten-Feed - ein zweiter Anlauf

Lego-Bausteine
Verfasst von franz-mue am 31. Oktober 2017

Bereits in einem früheren Beitrag habe ich ausgeführt, wie aus einem BMEcat-Katalog eine CSV-Datei generiert werden kann. Nun greife ich das Thema erneut auf: Nach dem Erscheinen des Releases 2.1 von fm-ProductNode bieten sich Anwendern neue und bessere Möglichkeiten zum Schreiben von Templates; einige davon möchte ich hier aufzeigen.

Ausgangssituation

Es geht darum, aus einem gegebenen Beispiel-BMEcat-2005-Katalog einen Produktdaten-Feed im CSV-Format zu erzeugen; der Produktdaten-Feed orientiert sich an den Vorgaben des Google Merchant Centers. Hier gehe ich davon aus, dass der BMEcat-Katalog bereits in die Datenhaltung von fm-ProductNode eingelesen wurde, und zur weiteren Verarbeitung bereitsteht. Mit Hilfe von Templates sind nun die Attribute der Katalogprodukte zu ermitteln und zu tabellarischen Daten zusammenzustellen.

Ausgangssituation und Konvertierungs-Ziel haben sich gegenüber dem früheren Beitrag nicht geändert. Daher verweise ich für weitere Details auf den alten Beitrag.

Wesentliche Verbesserungen im Vergleich zum ersten Anlauf

Der erste augenscheinliche Unterschied gegenüber früher ist die Verwendung von Task Templates anstelle von Twig Templates. Der Tausch bietet sich an, da hier Anweisungsfolgen in den Templates dominieren, die Datenausgabe jedoch nicht im Template selbst erfolgt. Das erspart vor allem die lästigen öffnenden und schließenden Klammerpaare in den (Twig) Templates.

Der zweite wichtige Punkt, ist das einfachere Aufteilen von Template-Anweisungen in mehrere Templates. Das erleichtert die bessere Strukturierung des Template Codes. Das resultierende Template des früheren Beitrags wurde bereits recht länglich und unübersichtlich. Mit dem Release 2.1 von fm-ProductNode sind Template-Bruchstücke einfach in weitere Templates auszulagern.

In Verbindung mit dem zweiten Punkt ist ein dritter wichtiger Punkt zu nennen: Das Release 2.1 erlaubt die einfache Synchronisierung von Templates - und zwar in beide Richtungen. Insbesondere wird hier das neue session-Objekt eingesetzt. Damit teilen sich Templates untereinander Speicherbereiche für Variablen.

Das Ausgangs-Template

Zu Beginn steht ein Template mit dem gleichen Namen bmecat2csv wie bisher. Der Aufruf von der Kommandozeile unterscheidet sich nur geringfügig zu bisher:

$ php trans.php task.process --alias source1 --template bmecat2Csv

Da die Template Engines ausgetauscht werden, lautet  das Kommando nicht mehr

twig.render

sondern

task.process

Das Template ist kürzer, da es Teilaufgaben in weitere Templates auslagert:

set language = language|default('deu')
set viewer = repository.currentCatalog.viewer(language)
set lastProductIndex = viewer.productCount - 1

if lastProductIndex >= 0
  set dataFeedPath = dataFeedPath|default('productDataFeed.txt')

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

  for productIndex in 0..lastProductIndex
    set product = viewer.productByIndex(productIndex)
    do context.session.addItem('product', product)
        
    do data.task.setTemplateName('getProductAttributes').process

    set productAttributes = context.session.item('attributes')
    do writer.insertOne(productAttributes)
  endfor
endif

In einer Schleife werden alle Katalogprodukte abgehandelt. Für jedes Produkt werden die Produktattribute in einem separaten Template namens getProductAttributes ermittelt. Die beiden Templates tauschen Daten über das session-Objekt aus. Vor dem Aufruf von getProductAttributes wird das aktuelle product-Objekt im session-Objekt abgelegt. getProductAttributes greift über das session-Objekt auf das product-Objekt zu. Wenn getProductAttributes alle Produktattribute zusammengestellt hat, legt es die Attribut-Liste im session-Objekt ab. Kommt anschließend wieder das Ausgangs-Template zum Zuge, holt es sich die Produktattribute aus dem session-Objekt.

Die Anweisungen zum Aufsetzen des writer-Objekts der CSV-Komponente wurden im Template belassen. Auch die Anweisung zur zeilenweise CSV-Ausgabe der Produktattribute befindet sich weiterhin im Template.

Ein kleiner Nachteil der Syntax des Task Templates sei erwähnt: Eine Anweisung endet immer in der gleichen Zeile. Die Zuweisung der Listenelemente zur Variable header führt deshalb zu einer langen Anweisungszeile.

Erwähnenswert ist noch, dass sich der Aufruf des currentCatalog-Objekts im Vergleich zur Vorversion von fm-ProductNode geändert hat: currentCatalog ist ab Release 2.1 nicht mehr global ansprechbar, sondern über das Objekt repository aufzurufen.

Anreicherung mit Protokollierungs-Ausgaben

Um einen Eindruck der Fähigkeiten der Template Engines zu geben, wird das Template bmecat2Csv um Protokollierungs-Ausgaben ergänzt. Dazu steht bei fm-ProductNode das log-Objekt zur Verfügung. Da beim Aufruf des log-Objektes keine weiteren Pfadangaben gemacht werden, werden Protokollierungsmeldungen per Voreinstellung in eine Datei namens fmpn.log geschrieben (fmpn ist dabei einfach eine Abkürzung für fm-ProductNode); die Datei befindet sich im Verzeichnis workplace/log von fm-ProductNode.

set log = utilities.log('INFO')
do context.session.addItem('log', log)

set language = language|default('deu')
set viewer = repository.currentCatalog.viewer(language)
set lastProductIndex = viewer.productCount - 1

if lastProductIndex < 0
  do log.warn('Der Produktkatalog enthält keine Produkte!')
else
  do log.info('Der Produktkatalog enthält ' ~ viewer.productCount ~ ' Produkte.')

  set dataFeedPath = dataFeedPath|default('productDataFeed.txt')

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

  for productIndex in 0..lastProductIndex
    set product = viewer.productByIndex(productIndex)
    do context.session.addItem('product', product)
        
    do data.task.setTemplateName('getProductAttributes').process

    set productAttributes = context.session.item('attributes')
    do writer.insertOne(productAttributes)

    do log.info('Produkt Nr. ' ~ (productIndex + 1) ~ ' in CSV-Datei geschrieben!')
  endfor

  do log.info('CSV-Datei erstellt.')
endif

Das log-Objekt wird zu Beginn im session-Objekt abgelegt. Auf diese Weise können auch die übrigen Templates auf dasselbe Objekt zur Protokollierung zugreifen.

Zusammenstellen der Produktattribute

Die Anweisungen zur Ermittlung der Attribute zu den Katalogprodukten haben sich gegenüber dem früheren Release nicht geändert. Aus Gründen der Übersichtlichkeit werden sie hier in separate Task Templates ausgelagert.

Im Ausgangs-Template wird über die Anweisung

do data.task.setTemplateName('getProductAttributes').process

das Task Template getProductAttributes ausgeführt. Der Inhalt des Templates sieht wie folgt aus:

set task = data.task

set product = context.session.item('product')

set properties = product.properties
do context.session.addItem('properties', properties)

set id = product.supplierProductId|left(50)

set shortDescription = properties.shortDescription|left(150)
set longDescription = properties.longDescription|left(5000)|default('DESCRIPTION MISSING')

set productLink = task.setTemplateName('getProductLink').process

set imageLink = task.setTemplateName('getImageLink').process

set price = task.setTemplateName('getPrice').process

set gtin = task.setTemplateName('getGtin').process

set productAttributes = [ id, shortDescription, longDescription, productLink, imageLink, 'in_stock', price, gtin, 'neu', 'nein', 'DE::10 EUR' ]

do context.session.addItem('attributes', productAttributes)

Im getProductAttributes Template wird anfangs das aktuelle Katalogprodukt aus dem session-Objekt entnommen. Ein Teil der Produktattribute ist über das properties-Objekt erreichbar. Da dieser Teil der Attribute noch mehrmals Verwendung findet, wird er einer Variablen gleichen Namens zugewiesen. Nachdem später das getGtin Template auf diese Variable zugreifen wird, wird das properties-Objekt gleich im session-Objekt abgelegt.

In der Folge werden die Attribute aus dem BMEcat-Katalog zusammengesucht. Die Attribute id, shortDescription und longDescription werden direkt abgefragt. Die Abfrage der weiteren Attribute productLink, imageLink, price und gtin erfolgt dagegen in separaten Task Templates. Der zusätzliche Aufwand wird spendiert, da nicht nur der Attributwert abgefragt wird, sondern jeweils weitere Prüfungen oder Modifikationen an den Attributwerten vorgenommen werden.

Erwähnenswert ist bei den letzten vier Attributen der Rückgabemechanismus für den Attributwert: Bei den Werten handelt es sich um Zeichenketten. Zeichenketten können von aufgerufenen Templates unmittelbar zurückgegeben und einer Variable zugewiesen werden. Der Umweg über das session-Objekt ist dementsprechend nicht erforderlich.

Templates zur Abfrage einzelner Produktattribute

Der Vollständigkeit halber gebe ich die Inhalte der restlichen verwendeten Templates an.

Zuerst das Template getProductLink:

set product = context.session.item('product')
set log = context.session.item('log')

set productLink = product.mimeInfoByIndex(3).source|left(2000)
if productLink is not valid_url
  set productLink = 'INVALID LINK'
  do log.error('Es konnte kein gültiger Produkt-Link ermittelt werden.')
endif

print productLink

Als zweites das Template getImageLink:

set product = context.session.item('product')
set log = context.session.item('log')

set imageLinkPrefix = 'https:\/\/www.huber-electronen.de/image/'

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'
  do log.error('Es konnte kein gültiger Produktbild-Link ermittelt werden.')
endif

print imageLink

Als drittes das Template getPrice:

set product = context.session.item('product')
set log = context.session.item('log')

set priceProperties = product.pricePropertiesByIndex.productPriceByIndex

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)
  do log.info('Zum Nettopreis des Produkts werden Steuern addiert.')
endif

print price|number_format(2, '.', ',') ~ " " ~ currency

Und zu guter letzt das Template getGtin:

set properties = context.session.item('properties')
set log = context.session.item('log')

set gtin = properties.internationalPids|first
set pidType = properties.internationalPidType(gtin)
if pidType not in [ 'gtin', 'ean' ]
  set gtin = properties.manufacturerPid|default('GTIN MISSING')
  do log.error('Es konnte keine gültige GTIN oder EAN ermittelt werden.')
endif

print gtin

Die vier vorausgegangenen Task Templates geben den Ergebnis-Wert über die print-Anweisung zurück. Nur so kann in Task Templates eine Zeichenkette an den Aufrufer zurückgegeben werden. Das Verhalten der Task Templates unterscheidet sich in diesem Punkt von Twig Templates. Bei Twig Templates wird (etwas vereinfacht formuliert) der gesamte Template-Inhalt außerhalb von Klammerpaaren zurückgegeben.

Eine Besonderheit von Task Templates ist im getImageLink Template zu sehen. Task Templates werten den doppelten Vorwärtsschrägstrich // als Kommentar-Anfang (so wie es auch in der Programmiersprache PHP der Fall ist, jedoch abweichend zur Twig Template Engine). Die Task Template Engine entfernt vor der weiteren Verarbeitung einer Template-Zeile alle Zeichen vom Kommentaranfang bis zum Zeilenende. Im Falle einer Internet-Adresse wird jedoch der doppelte Schrägstrich zur Angabe des Übertragungsprotokolls benötigt: "https://...". Um zu verhindern, dass die Task Template Engine an einer solchen Stelle nicht fälschlicherweise den Beginn eines Kommentars interpretiert, sind die Schrägstriche mit dem üblichen Mechanismus zu entwerten, also durch Einfügen von Rückwärtsschrägstrichen: "https:\/\/...".

Beachtenswert ist hier noch der Umgang mit dem log-Objekt. Durch die Ablage im session-Objekt steht es allen aufgerufenen Templates zur Nutzung zur Verfügung. Der Zugriff ist also nicht auf das unmittelbar nächste aufgerufene Template beschränkt, sondern kann in einer etwaigen längeren Template-Aufrufkaskade auch von später aufgerufenen Templates eingesetzt werden.

Zusammenfassung

Mit dem neuen Release 2.1 von fm-ProductNode lassen sich Template-basierte Datentransformationen und Abläufe wesentlich bequemer und übersichtlicher durchführen als im Vorgänger-Release 2.0, das sich noch voll auf die Twig Template Engine stützte.

Übrigens: Die Task Templates sind ebenfalls in meinem GitHub Repository unter dem examples/productdatafeed-Unterverzeichnis abgelegt.

Tags: BMEcat, CSV, Konvertieren, ProduktdatenFeed, Produktkatalog, Release2.1, Template
Foto: Efraimstochter / pixabay.com

« Neu: Das Release 2.1 von fm-ProductNode - Excel-Tabellen aus BMEcat generieren »