Der Blog von fm-ProductNode

Von BMEcat in das Semantische Web

Knöpfe
Verfasst von franz-mue am 3. Januar 2018

Vor einigen Jahren wurde das eCommerce-Vokabular GoodRelations entwickelt. Die Idee war insbesondere, Produktinformationen von Herstellern effektiver austauschen und im Internet präsentieren zu können. Im vorliegenden Beitrag zeige ich anhand eines Beispiels, wie eine Transformation von BMEcat nach GoodRelations mit Hilfe von fm-ProductNode vonstatten geht.

Motivation

Das GoodRelations-Vokabular wurde vor ein paar Jahren nach schema.org überführt. Die Online-Dokumentation von GoodRelations ist nach wie vor unvollständig und auch die GoodRelations-Validator-Seite führt ins Leere. Auf den ersten Blick gibt es also kaum einen Grund, sich weiter mit diesem Thema zu beschäftigen. Dennoch gibt es bei GoodRelations mindestens zwei relevante Aspekte für mich:

GoodRelations-Projektbeteiligte untersuchten zum einen, wie gut und vollständig Produktinformationen der Hersteller ihren Weg ins Web fanden - zum Beispiel auf Web-Seiten von Online Shops oder Marktplätzen. Das Ergebnis war ernüchternd; der größere Teil der Produktattribute blieb auf der Strecke. Die Idee, einen Brücke von den Produktstammdaten der Hersteller hin zur Präsentation und zum Verkauf im Web zu bauen, liegt auch fm-ProductNode zugrunde.

Zum zweiten wurde als konkrete Verbesserungsidee versucht, durch Konvertierung von BMEcat-Produktkatalogen in GoodRelations-Modelle die Hürde zu überbrücken. Als Notation zur Darstellung von GoodRelations-Modellen kommen Formate in Betracht, die auch in Web-Seiten als Microdaten eingebettet werden können, also zum Beispiel RDF beziehungsweise RDFa. Die GoodRelations-Leute bauten ein Werkzeug namens BMEcat2GoodRelations zur Konvertierung von BMEcat-Produktkatalogen nach GoodRelations. Zu ein paar Konvertierungen wurden Ausgangs-Katalog und die resultierenden Dateien im Detail angegeben. Wenn ich mich des selben Beispiels bediene, sind die Ergebnisdaten einfach zu vergleichen.

Ausgangssituation

Ich verwende also den selben BMEcat-Produktkatalog, der auch für die BMEcat2GoodRelations-Konvertierung zum Einsatz kam. Inwieweit der BMEcat-Katalog mit seinen nur drei enthaltenen Produkten praxisnah war, ist mir nicht bekannt; ich zweifle sogar daran.

Wichtig zu betonen ist mir, dass der Hintergrund beider Werkzeuge sehr verschieden ist: Bei BMEcat2GoodRelations geht es darum, möglichst robust und fehlertolerant zu arbeiten, und möglichst viele Inhalte in das Zielformat hinüber zu retten. Möglicherweise ist das der Grund, warum die BMEcat-Ausgangsdatei nicht fehlerfrei aufgebaut ist.

fm-ProductNode dagegen erwartet beim Einlesen einigermaßen korrekte BMEcat-Dateien. Je nach Einstellungen werden die einzulesenden Dateien mehr oder minder streng geprüft. Zur Vorbereitung für die Konvertierung korrigierte ich erst viele Fehler in der BMEcat-Datei.

Die Werkzeuge BMEcat2GoodRelations und fm-ProductNode unterscheiden sich in mancherlei Hinsicht. Während zum Beispiel das in Python geschriebene Werkzeug BMEcat2GoodRelations nur auf die Konvertierung nach GoodRelations abzielt, will fm-ProductNode die ganze Bandbreite möglicher Textformate unterstützen.

Bei den Eigenschaften des Konverters fallen bei BMEcat2GoodRelations die Begriffe "completeness" und "standards-compliance". Wie immer die Begriffe gemeint sind, deckt jedenfalls BMEcat2GoodRelations nur Teile der BMEcat-2005-Spezifikation ab. Die Themen Produktkonfiguration, Preisformeln und Integrated Procurement Point-Anwendungen werden von BMEcat2GoodRelations nicht berücksichtigt, obwohl sie Bestandteil der BMEcat-2005-Spezifikation sind. Mit dem Begriff Standardkonformität kann nicht gemeint sein, konform zur BMEcat-2005-Spezifikation zu agieren, denn das stark fehlerbehaftete Ausgangs-Beispiel wird von BMEcat2GoodRelations anstandslos akzeptiert und verarbeitet.

Da es mir nur darum geht, zu zeigen, wie eine Konvertierung mit Hilfe von fm-ProductNode grundsätzlich durchgeführt wird, beschränke ich meine Aktivitäten auf die Konvertierung der BMEcat-Kopfdaten zur GoodRelations-Klasse BusinessEntity, also zum Unternehmen, dem der Produktkatalog zuzuordnen ist.

Einen validen BMEcat-Ausgangskatalog erstellen

Zur Datenbereinigung beseitigte ich in dem BMEcat-Dokument etwa 40 Fehler. Die korrigierte Fassung ist unter meinem GitHub Repository einsehbar. Hier die wichtigsten korrigierten Fehler:

  • Es ist die BMEcat-Version 1.2 angegeben. Das ist aber nicht korrekt, da die Elemente PARTIES und SUPPLIER_IDREF erst in der späteren BMEcat-Version 2005 eingeführt wurden. Ebenso erlaubt BMEcat-1.2 nur einsprachige Produktkataloge.
  • Als zulässige Sprache ist im BMEcat-Header nur der Sprache-Code eng (für englisch) angegeben. Tatsächlich werden jedoch im restlichen Dokument in einigen Fällen auch Elemente in deutscher Sprache angegeben.
  • Es ist keine Default-Sprache eingestellt; andererseits ist nicht bei allen sprachabhängigen Elementen die Sprache angegeben.
  • Bei sprachabhängigen Elementen wird die Reihenfolge der Sprache nicht beibehalten.
  • In zwei Fällen wird zur Sprachangabe ein zweistelliger anstelle des dreistelligen Sprach-Codes verwendet.
  • Die vorgeschriebene Reihenfolge von Elementen wird in ein paar Fällen nicht eingehalten.
  • Verwendung nicht existenter Elementnamen (GROUP_ID statt CATALOG_GROUP_ID sowie FREF statt FT_IDREF)
  • Zu MIME-Elementen fehlt das umschließende MIME_INFO-Element.
  • Zu dem Element MIME_PURPOSE ist der Wert url nicht erlaubt (url darf nur beim Element MIME_TYPE verwendet werden).
  • Das Element REFERENCE_FEATURE_GROUP_NAME darf nur alternativ zum Element REFERENCE_FEATURE_GROUP_ID auftreten.
  • Das Element REFERENCE_FEATURE_SYSTEM_NAME darf nur auftreten, wenn zugleich auch das Unterelement FEATURE/FT_IDREF existiert.

Task Templates und FluidXML zur Konvertierung

Mit dem modifizierten BMEcat-Katalog nehme ich die geplante Transformation in Angriff.

Viele Wege führen nach Rom; fm-ProductNode ermöglicht unterschiedliche Ansätze zur Konvertierung in das gewünschte Zielformat. Im vorliegenden Beitrag setze ich Task Templates ein (also für die Zwecke von fm-ProductNode modifizierte Twig Templates) und verwende insbesondere die bereits in fm-ProductNode integrierte PHP-Komponente FluidXML.

Bevor die Konvertierung beginnt, ist der BMEcat-Katalog erst in die Datenhaltung von fm-ProductNode einzulesen. Damit verbunden ist Validierung des eingelesenen BMEcat-Katalogs. Der Aufruf zum Einlesen sieht etwa wie folgt aus:

$ php trans.php catalog.read --alias ikea workplace/bmecat/ikea_corrected.xml

Vorausgesetzt wird bei dem Aufruf, dass sich der BMEcat-Katalog in dem bmecat-Unterverzeichnis des workplace-Verzeichnisses befindet.

Die Konvertierungs-Aufgabe erledige ich mit drei Templates. Die Transformation wird durch Aufruf des Templates bmecat2GoodrelationsCompany angestoßen. Falls der Aufruf von der Kommandozeile geschieht, könnte er wie folgt lauten:

$ php trans.php task.process --alias ikea --template bmecat2GoodrelationsCompany

Das Template bmecat2GoodrelationsCompany wiederum lagert die Aufgabe in zwei Unter-Templates aus: Im ersten Schritt extrahiert die Template getBmecatMetadata die benötigten Kopfdaten aus dem BMEcat-Produktkatalog. Im zweiten Schritt erzeugt das Template createGoodrelationsCompany unter Verwendung der Kopfdaten die gewünschte RDF-Datei. Der Inhalt des Templates bmecat2GoodrelationsCompany sieht wie folgt aus:

set task = data.task

set language = language|default('eng')
do context.session.addItem('language', language)

set catalogViewer = repository.currentCatalog.viewer(language)

do task.setTemplateName('getBmecatMetadata').addContext('catalogViewer', catalogViewer).process

if context.session.item('meta.hasParty')
    do task.setTemplateName('createGoodrelationsCompany').process
endif

Falls beim Aufruf des Templates (wie das hier der Fall ist) keine Sprache angegeben ist, wird als Sprache Englisch (Sprach-Code eng) angenommen. Ein catalogViewer-Objekt zum Zugriff auf die Inhalte des BMEcat-Dokuments wird erzeugt und an das erste Sub Template beim Aufruf weitergegeben. Wenn in den Kopfdaten des BMEcat-Dokuments Angaben zu einem Unternehmen vorhanden sind (bei BMEcat als PARTY beziehungsweise Partei bezeichnet), wird als nächstes aus den Daten per Aufruf des Templates createGoodrelationsCompany eine RDF-Datei generiert.

Das Abgreifen der BMEcat-Metadaten

Das Template getBmecatMetadata greift aus den BMEcat-Kopfdaten die interessierenden Unternehmensdaten ab. Nach der Auflistung des Template-Inhalts gehe ich auf einige Aspekte näher ein.

set session = context.session
set log = utilities.log('INFO')
set metadata = catalogViewer.metadata

if not metadata.hasParties
  do session.addItem('meta.hasParty', false)
  do log.warn('No business entity data available.')
else
  set party = metadata.partyByIndex
  if not party.hasAddress
    do session.addItem('meta.hasParty', false)
    do log.warn('No address of the business entity available.')
  else
    do session.addItem('meta.hasParty', true)
    set address = party.address

    do session.addItem('meta.name', address.name|default('NAME_MISSING'))
    do session.addItem('meta.phone', address.phone|default('PHONE_MISSING'))
    do session.addItem('meta.fax', address.fax|default('FAX_MISSING'))
    do session.addItem('meta.postOfficeBox', address.boxNo|default('POST_OFFICE_BOX_MISSING'))
    do session.addItem('meta.zipCode', address.zip|default('ZIP_CODE_MISSING'))
    do session.addItem('meta.city', address.city|default('CITY_MISSING'))
    do session.addItem('meta.eMail', address.eMails|first|default('EMAIL_MISSING'))
    do session.addItem('meta.url', address.url|default('URL_MISSING'))
    do session.addItem('meta.duns', party.ids|first)

    set countryPlain = metadata.content('party.0.address.country')|first // concrete Task-API only exposes country code
    do session.addItem('meta.country', countryPlain|default('COUNTRY_MISSING'))

    do log.info('Catalog metadata grabbed.')
  endif
endif

Die Kopfdaten werden nur aus dem BMEcat-Dokument ausgelesen, wenn tatsächlich auch eine Partei (das heißt, ein Unternehmen) und zur Partei Adress-Angaben vorhanden sind.

Wenn dem so ist, werden die relevanten Daten dediziert abgefragt. Fehlt ein Attribut, wird es ersatzweise mit einem Vorgabe-Wert belegt. Die abgefragten Kopfdaten werden sofort, versehen mit dem Präfix "meta.", als Variablen des session-Objektes abgelegt. Über das session-Objekt sind die Kopfdaten in allen anderen Templates verfügbar.

Mit Hilfe des log-Objektes werden ein paar Meldungen zum Fortschritt der Konvertierung ausgegeben.

Das Erstellen der RDF-Datei

Der Template Code zur Generierung der RDF-Datei ist ein wenig umfangreicher. Daher splitte ich ihn hier in mehrere Blöcke auf.

Zu Beginn des createGoodrelationsCompany-Templates werden einige Vorbereitungen und Festlegungen getroffen:

set session = context.session
set xml = data.xml

set log = utilities.log('INFO')

set language = session.item('language')

set languageMap = [ { 'source': 'eng', 'destination': 'en' }, { other_destination: 'de' } ]
set beLanguage = language|map(languageMap)

set companyFileName = 'company.rdf'

Das session-Objekt wird benötigt, um auf die vorher im getBmecatMetadata Template zusammengetragenen BMEcat-Kopfdaten zuzugreifen.

Hinter dem xml-Objekt verbirgt sich die bereits weiter oben erwähnte PHP-Komponente FluidXML. Mit Hilfe dieser Komponente wird das RDF-Dokument erzeugt und zum Schluss in einer Datei abgespeichert.

Der Template-Filter map wird zur Abbildung der Sprach-Codes eingesetzt: Während BMEcat auf einen dreistelligen Sprach-Code setzt, verwendet GoodRelations einen zweistelligen Sprach-Code.

Als nächstes setze ich ein paar benötigte Zeichenketten zusammen:

set beStrippedName = session.item('meta.name')|replace({' ': ''})
set beId = session.item('meta.url') ~ '/data/rdf/' ~ companyFileName ~ '#be_' ~ beStrippedName
set beAddress =  session.item('meta.url') ~ '/data/rdf/' ~ companyFileName ~ '#address_' ~ beStrippedName

Bevor die RDF-Generierung richtig losgeht, sind einige Namensräume (namespaces) für die RDF Tags festzulegen:

set xmlString = 'http:\/\/www.w3.org/2001/XMLSchema#string'
set xmlNamespace = 'http:\/\/www.w3.org/2000/xmlns/'
set rdfNamespace = 'http:\/\/www.w3.org/1999/02/22-rdf-syntax-ns#'
set foafNamespace = 'http:\/\/xmlns.com/foaf/0.1/'
set vcardNamespace = 'http:\/\/www.w3.org/2006/vcard/ns#'
set goodrelationsNamespace = 'http:\/\/purl.org/goodrelations/v1#'

do xml.namespace('rdf', rdfNamespace)
do xml.namespace('foaf', foafNamespace)
do xml.namespace('vcard', vcardNamespace)
do xml.namespace('gr', goodrelationsNamespace)

set rdf = xml.addChild('rdf:RDF', true)
do rdf.0.setAttributeNS(xmlNamespace, 'xmlns:foaf', foafNamespace)
do rdf.0.setAttributeNS(xmlNamespace, 'xmlns:vcard', vcardNamespace)
do rdf.0.setAttributeNS(xmlNamespace, 'xmlns:gr', goodrelationsNamespace)

Dabei sind die letzten vier Anweisungen erforderlich, damit die zugehörigen Namensräume beim RDF Tag als Attribute erscheinen.

Nun ist die eigentliche Generierung der RDF Tags an der Reihe:

set be = rdf.addChild('gr:BusinessEntity', {'rdf:about' : beId}, true)

do be.addChild('vcard:tel', session.item('meta.phone'))
do be.addChild('vcard:fn', session.item('meta.name'), {'xml:lang' : beLanguage})
do be.addChild('vcard:fax', session.item('meta.fax'))
do be.addChild('foaf:page', {'rdf:resource' : session.item('meta.url')})
do be.addChild('gr:hasDUNS', session.item('meta.duns'), {'rdf:datatype' : xmlString})

set adr = be.addChild('vcard:adr', true)
set address = adr.addChild('vcard:Address', {'rdf:about' : beAddress}, true)
do address.addChild('vcard:country-name', session.item('meta.country'), {'xml:lang' : beLanguage})
do address.addChild('vcard:post-office-box', session.item('meta.postOfficeBox'), {'xml:lang' : beLanguage})
do address.addChild('vcard:postal-code', session.item('meta.zipCode'), {'rdf:datatype' : xmlString})
do address.addChild('vcard:locality', session.item('meta.city'), {'xml:lang' : beLanguage})

do be.addChild('vcard:email', session.item('meta.eMail'), {'rdf:datatype' : xmlString})
do be.addChild('gr:legalName', session.item('meta.name'), {'xml:lang' : beLanguage})
do be.addChild('vcard:url', {'rdf:resource' : session.item('meta.url')})

Zum Eintragen der Werte wird auf die vorgehaltenen session-Variablen zurückgegriffen.

Abschließend wird der RDF-Code in eine Datei abgespeichert und eine Protokollmeldung abgesetzt:

do xml.save(companyFileName)

do log.info('Catalog metadata converted to RDF company profile.')

Und hier noch das RDF-Resultat:

<?xml version="1.0" encoding="UTF-8"?>
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:foaf="http://xmlns.com/foaf/0.1/" xmlns:vcard="http://www.w3.org/2006/vcard/ns#" xmlns:gr="http://purl.org/goodrelations/v1#">
  <gr:BusinessEntity rdf:about="http://www.ikea.com/data/rdf/company.rdf#be_IKEAINTERNATIONAL">
    <vcard:tel>+46-42-267-100</vcard:tel>
    <vcard:fn xml:lang="en">IKEA INTERNATIONAL</vcard:fn>
    <vcard:fax>+46-42-132-805</vcard:fax>
    <foaf:page rdf:resource="http://www.ikea.com"/>
    <gr:hasDUNS rdf:datatype="http://www.w3.org/2001/XMLSchema#string">792837239</gr:hasDUNS>
    <vcard:adr>
      <vcard:Address rdf:about="http://www.ikea.com/data/rdf/company.rdf#address_IKEAINTERNATIONAL">
        <vcard:country-name xml:lang="en">Sweden</vcard:country-name>
        <vcard:post-office-box xml:lang="en">640</vcard:post-office-box>
        <vcard:postal-code rdf:datatype="http://www.w3.org/2001/XMLSchema#string">SE 25 106</vcard:postal-code>
        <vcard:locality xml:lang="en">Helsinborg</vcard:locality>
      </vcard:Address>
    </vcard:adr>
    <vcard:email rdf:datatype="http://www.w3.org/2001/XMLSchema#string">info@ikea.com</vcard:email>
    <gr:legalName xml:lang="en">IKEA INTERNATIONAL</gr:legalName>
    <vcard:url rdf:resource="http://www.ikea.com"/>
  </gr:BusinessEntity>
</rdf:RDF>

Wie ein Vergleich mit dem Beispiel-Ergebnis bei GoodRelations zeigt, bestehen bei den beiden Dateien keine signifikanten Unterschiede.

Zusammenfassung

Der vorliegende Beitrag zeigt anhand eines konkreten Beispiels, wie BMEcat-Dateien mit Hilfe von fm-ProductNode in XML-Dateien, insbesondere RDF-Dateien konvertiert werden können.

Der gesamte Template Code nebst korrigierter Ausgangs-BMEcat-Datei und Ergebnis-RDF-Datei liegt auf meinem GitHub Repository zur Einsicht vor.

Ausblick

Demnächst zeige ich in meinem Blog, wie BMEcat-Dateien unter Einsatz von fm-ProductNode in das Vokabular von schema.org überführt werden können.

schema.org besitzt große Relevanz und auch zunehmende Akzeptanz für unterschiedliche Anwendungs-Domänen, nicht nur für den eCommerce. Kein Wunder, denn dahinter stehen vor allem die Suchmaschinen-Giganten Google, Bing & Co. Die Möglichkeit, der Einbettung der schema.org-Klassen per leichtgewichtigen Microdata-Formaten in Web-Seiten, erleichtert den Suchmaschinen die Analyse von Web-Seiten, denn sie wird sowohl effektiver als auch effizienter.

Ich persönlich halte die schema.org-Aktivitäten für sehr nützlich. Allerdings meine ich, dass mit einem weitergehenden Ansatz noch ein wesentlich höherer Nutzen möglich ist. Doch auf diese Überlegungen werde ich erst in späteren Blog-Beiträgen detaillierter eingehen.

Tags: BMEcat, Konvertieren, Template, Release2.2, GoodRelations, SemanticWeb, rdf, Microdata
Foto: PublicDomainPictures / pixabay.com

« Neu: Das Release 2.2 von fm-ProductNode - Neu: Das Release 2.3 von fm-ProductNode »