4.1.12  JSON mit XSLT 1.0 und Python lxml

Wenn man mit Python programmiert, hat man leider nicht Saxon als XSLT Prozessor zur verfügung, sondern muss sich mit dem XSLT Prozessor der lxml ↗ Bibliothek begnügen. Diese basiert auf der C Bibliothek libxslt ↗
Dabei steht nur XSLT 1.0 zur Verfügung mit den EXSLT ↗ Erweiterungen.
Unter diesen Bedingungen war es nicht ganz einfach eine Transformation zu erstellen, die XML als Eingabe nimmt und sauberes JSON als Ausgabe produziert. Sicherlich ist hierfür ein mehrstufiger Prozess erforderlich.
Um genau zu sein braucht man mindestens eine Vortransformation, die die JSON Elemente als XML erzeugt und ein Postprocessing, das möglichst generisch die spitzen Klammern auf geschweifte abbildet und einige Elementnamen verwirft.
Die folgenden JSON Strukturen lassen sich leicht identifizieren:
1.

Properties

Dabei kann der propertyValue vom Typ String, Number oder Boolean sein.
"propertyName" : propertyValue
2.

Benannte Objekte

"objektName" : { ... }
3.

Bennante Listen

"listenName" : [ { ... }, { ... }]
4.

Anonyme Objekte

{ }
In Objekten können Properties, benannte Objekte oder benannte Listen enthalten sein. In benannten Listen können nur anonyme Objekte enthalten sein. Diese Unterscheidung hat erst einmal für meinen Anwendungsfall ausgereicht.
Umgemünzt auf XSLT Regeln, kommt man auf vier Regeln die anhand einer Attributbelegung für @json matchen:
<xsl:template match="*[@json='anonymous-object']" mode="post">
  <xsl:text>{</xsl:text>
  <xsl:apply-templates select="node()|@*" mode="post"/>
  <xsl:text>}</xsl:text>
  <xsl:if test="following-sibling::*">,</xsl:if>
</xsl:template>

<xsl:template match="*[@json='named-list']" mode="post">
  <xsl:text>"</xsl:text>
  <xsl:value-of select="name()"/>
  <xsl:text>"</xsl:text>
  <xsl:text>:[</xsl:text>
  <xsl:apply-templates select="node()|@*" mode="post"/>
  <xsl:text>]</xsl:text>
  <xsl:if test="following-sibling::*">,</xsl:if>
</xsl:template>

<xsl:template match="*[@json='named-object']" mode="post">
  <xsl:text>"</xsl:text>
  <xsl:value-of select="name()"/>
  <xsl:text>"</xsl:text>
  <xsl:text>:{</xsl:text>
  <xsl:apply-templates select="node()|@*" mode="post"/>
  <xsl:text>}</xsl:text>
  <xsl:if test="following-sibling::*">,</xsl:if>
</xsl:template>

<xsl:template match="*[@json='property']" mode="post">
  <xsl:text>"</xsl:text>
  <xsl:value-of select="name()"/>
  <xsl:choose>
    <xsl:when test="@type='boolean' or @type='integer'">
      <xsl:text>":</xsl:text>
      <xsl:value-of select="."/>
    </xsl:when>
    <xsl:otherwise>
      <xsl:text>":"</xsl:text>
      <xsl:value-of select="."/>
      <xsl:text>"</xsl:text>
    </xsl:otherwise>
  </xsl:choose>
  <xsl:if test="following-sibling::*">,</xsl:if>
</xsl:template>
In einer Vortransformation in eine Variable pre werden die JSON Strukturen erstmal als XML aufgebaut und dann in einem Postprocessing Step post auf die geschweiften Klammern gemappt:
<xsl:template match="/">
  <root>
    <!-- first processing step -->
    <xsl:variable name="pre">
      <xsl:copy>
        <xsl:apply-templates/>
      </xsl:copy>
    </xsl:variable>
    <!-- second processing step -->
    <xsl:apply-templates select="exsl:node-set($pre)" mode="post"/>
  </root>
</xsl:template>
Das Aufbauen der XML-artigen JSON Struktur mit den annotierten @json Attributen, könnte z.B. so aussehen:
<xsl:template match="testsuite">
  <sarif-report json="anonymous-object">
    <runs json="named-list">
      <run json="anonymous-object">
        [...]
      </run>
    </runs>
    <version json="property">2.1.0</version>
  </sarif-report>
</xsl:template>
Mit folgender finaler Ausgabe:
{ "runs" : [{ ... }],
  "version": "2.1.0"
}
Da Python lxml bzw. libxslt anscheinend kein xsl:output method="text" beherscht, muss im Python Code der Textknoten eines Root-Elements ausgelesen werden. Wenn man diesen als JSON in ein Python dictionary liest und anschliessend wieder als JSON mit Indention herausschreibt, hat man auch gleich ein schönes Pretty-Printing der JSON Strukturen. Der relevante Python Code sieht dabei so aus:
with fileobj:
  # We register namespace functions that can be called inside the XSLT transformation
  register_stylesheet_functions()
  # Read the XSLT stylesheet
  xsl = etree.XML(open(SARIF_TRANSFORMATION_FILE, "r", encoding="utf-8").read())
  # Transform the XML to Sarif JSON and put it into the root text node
  transform = etree.XSLT(xsl)
  result = transform(tree)
  if INDENT:
    # Load into python dict and pretty print when writing back to JSON
    jsn = json.loads(result.getroot().text)
    fileobj.write(json.dumps(jsn, indent=2))
    else:
      fileobj.write(result.getroot().text)
Das vollständige Beispiel findet man in meinem Github Repo ↗.
Previous PageNext Page
Map Version: