[XML] xsltprocを使って、XMLをCSVに変換する

2017年6月19日月曜日

bash XML シェルスクリプト

xsltproc

XMLをDBのテーブルに格納したい時はDBのツールを利用するために、一旦CSVに変換したくなる。
XMLからCSVへの変換っていうと、プログラムを組まなきゃって思うけど、xsltprocを使うとXSLTファイルで変換マッピングを作ってあげれば、シェルからでも変換処理ができる。

http://xmlsoft.org/XSLT/xsltproc.html

フラットな構造をCSVに展開する

まずはそのままCSVに展開できそうなフラットなデータ構造の場合を考えよう。

planets1.xml
<?xml version="1.0" encoding="utf-8"?>
<planets>
  <planet>
    <name>水星</name> 
    <diameter>4879.4</diameter> 
    <mass>3.301e+23</mass> 
    <epoch>2008-01-01</epoch> 
  </planet>
  <planet>
    <name>金星</name> 
    <diameter>12103.6</diameter> 
    <mass>4.869e+24</mass> 
    <epoch>2008-01-01</epoch> 
  </planet>
  <planet>
    <name>火星</name> 
    <diameter>6794.4</diameter> 
    <mass>6.4191e+23</mass> 
    <epoch>2008-01-01</epoch> 
  </planet>
  <planet>
    <name>木星</name> 
    <diameter>142984</diameter> 
    <mass>1.8986e+27</mass> 
    <epoch>2008-01-01</epoch> 
  </planet>
</planets>

これを planet 要素毎にCSVレコードにするXSLTは以下のようになる。

planets2csv1.xsl
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text" encoding="utf-8" />

  <xsl:param name="delim" select="','" />
  <xsl:param name="quote" select="'"'" />
  <xsl:param name="break" select="'
'" />

  <xsl:template match="/">
    <xsl:apply-templates select="planets/planet" />
  </xsl:template>

  <xsl:template match="planet">
    <xsl:apply-templates />
    <xsl:value-of select="$break" />
  </xsl:template>

  <xsl:template match="*">
    <xsl:value-of select="concat($quote, text(), $quote)" />
    <xsl:if test="following-sibling::*">
      <xsl:value-of select="$delim" />
    </xsl:if>
  </xsl:template>

  <xsl:template match="text()" />
</xsl:stylesheet>

$ xsltproc planets2csv1.xsl planets1.xml
"水星","4879.4","3.301e+23","2008-01-01"
"金星","12103.6","4.869e+24","2008-01-01"
"火星","6794.4","6.4191e+23","2008-01-01"
"木星","142984","1.8986e+27","2008-01-01"

各要素をダブルクォートで囲む必要がなければ、select="concat($quote, text(), $quote)" を select="text()" に変えればいい。

親要素があるような構造をCSVに展開する

今度は各データに共通な親要素があるようなデータ構造を考えよう。
内惑星と外惑星にカテゴリ分けしたXMLにしてみよう。

planets1.xml
<?xml version="1.0" encoding="utf-8"?>
<list>
  <orbits>
    <orbit>内惑星</orbit>
    <planets>
      <planet>
        <name>水星</name> 
        <diameter>4879.4</diameter> 
        <mass>3.301e+23</mass> 
        <epoch>2008-01-01</epoch> 
      </planet>
      <planet>
        <name>金星</name> 
        <diameter>12103.6</diameter> 
        <mass>4.869e+24</mass> 
        <epoch>2008-01-01</epoch> 
      </planet>
    </planets>
  </orbits>
  <orbits>
    <orbit>外惑星</orbit>
    <planets>
      <planet>
        <name>火星</name> 
        <diameter>6794.4</diameter> 
        <mass>6.4191e+23</mass> 
        <epoch>2008-01-01</epoch> 
      </planet>
      <planet>
        <name>木星</name> 
        <diameter>142984</diameter> 
        <mass>1.8986e+27</mass> 
        <epoch>2008-01-01</epoch> 
      </planet>
    </planets>
  </orbits>
</list>

これをCSVに変換するXSLTは以下のようになる。

planets2csv2.xsl
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text" encoding="utf-8" />

  <xsl:param name="delim" select="','" />
  <xsl:param name="quote" select="'"'" />
  <xsl:param name="break" select="'
'" />

  <xsl:template match="/">
    <xsl:apply-templates select="/list/orbits/planets" />
  </xsl:template>

  <xsl:template match="planets">
    <xsl:apply-templates select="planet" />
  </xsl:template>

  <xsl:template match="planet">
    <xsl:variable name="orbit" select="string(../../orbit/text())" />
    <xsl:value-of select="concat($quote, $orbit, $quote)" />
    <xsl:value-of select="$delim" />

    <xsl:apply-templates />
    <xsl:value-of select="$break" />
  </xsl:template>

  <xsl:template match="*">
    <xsl:value-of select="concat($quote, text(), $quote)" />

    <xsl:if test="following-sibling::*">
      <xsl:value-of select="$delim" />
    </xsl:if>
  </xsl:template>

  <xsl:template match="text()" />
</xsl:stylesheet>

ポイントは planet のテンプレート部分で親要素を参照しているところ。
相対パスで親要素を参照すれば、共通ヘッダの内容も繰り返して出力できる。
  <xsl:template match="planet">
    <xsl:variable name="orbit" select="string(../../orbit/text())" />
    <xsl:value-of select="concat($quote, $orbit, $quote)" />

$ xsltproc planets2csv2.xsl planets2.xml
"内惑星","水星","4879.4","3.301e+23","2008-01-01"
"内惑星","金星","12103.6","4.869e+24","2008-01-01"
"外惑星","火星","6794.4","6.4191e+23","2008-01-01"
"外惑星","木星","142984","1.8986e+27","2008-01-01"

参考サイト
https://stackoverflow.com/questions/365312/xml-to-csv-using-xslt