2017-08-18

属性の階層構造の表現 - 構造化リスト, XML, JSON

一般的なGISデータフォーマットでは、地物の属性は表形式で格納されることが多いと思いますが、ひとつの属性項目が下位の複数の属性で構成される階層構造を持っていたり、同一の属性項目の複数の値が単一の地物に関連付けられたりするなど、フラットな表だけでは、本来の構造をストレートに表現するのが難しいケースもあります。

例えば、国土数値情報「バス停留所」

国土数値情報「バス停留所」データの製品仕様書によれば、バス停留所フィーチャーは次の属性を持つことになっています。

・バス停名
・バス路線情報(0個以上、個数上限なし)

このうち「バス路線情報」属性は単一の値ではなく、次の3つの下位属性で構成されています。

・バス区分: 運行形態の種類(路線バス(民間)、路線バス(公営)、コミュニティバスなど)を示すコード
・事業者名: 路線を運営する事業者の名称。
・バス系統: バスの系統番号または系統名。

GML形式のデータでは、属性本来の階層構造がXMLドキュメントツリーの構造に反映されています。

国土数値情報「バス停留所」GML形式: 地物XML要素の記述例
注1: 実データからの引用ですが、事業者名、地名等の固有名詞は英文字の記号に置き換えました。
柱2: コメント <!-- *** --> は、記述内容を分かりやすくするために追記したものです。
<ksj:BusStop gml:id="ED01_5">  <!-- バス停留所 -->
    <ksj:position xlink:href="#n5"/>  <!-- gml:Point 要素 (空間属性) 参照 -->
    <ksj:busStopName>図書館前</ksj:busStopName>  <!-- バス停名 -->
    <ksj:busRouteInformation>  <!-- バス路線情報 -->
        <ksj:BusRouteInformation>
            <ksj:busType>1</ksj:busType>  <!-- バス区分 -->
            <ksj:busOperationCompany>XXバス(株)</ksj:busOperationCompany>  <!-- 事業者名 -->
            <ksj:busLineName>AA01</ksj:busLineName>  <!-- バス系統 -->
        </ksj:BusRouteInformation>
    </ksj:busRouteInformation>
    <ksj:busRouteInformation>  <!-- バス路線情報 -->
        <ksj:BusRouteInformation>
            <ksj:busType>1</ksj:busType>  <!-- バス区分 -->
            <ksj:busOperationCompany>XXバス(株)</ksj:busOperationCompany>  <!-- 事業者名 -->
            <ksj:busLineName>深夜(AA01)</ksj:busLineName>  <!-- バス系統 -->
        </ksj:BusRouteInformation>
    </ksj:busRouteInformation>
    <ksj:busRouteInformation>  <!-- バス路線情報 -->
        <ksj:BusRouteInformation>
            <ksj:busType>3</ksj:busType>  <!-- バス区分 -->
            <ksj:busOperationCompany>YY市</ksj:busOperationCompany>  <!-- 事業者名 -->
            <ksj:busLineName>BB線(CC循環コース)</ksj:busLineName>  <!-- バス系統 -->
        </ksj:BusRouteInformation>
    </ksj:busRouteInformation>
</ksj:BusStop>

しかし、フラットな表ではこのような属性の階層構造を表現することはできません。そのため、Shapefile形式の国土数値情報「バス停留所」データでは、バス路線情報に属する下位の属性もそれぞれ単一の属性項目として扱い、複数の値がある場合は、それらをカンマ区切りで連結した文字列によって記録しています。

国土数値情報「バス停留所」Shapefile形式: 属性定義
属性名内容上記XML地物要素例に対応するレコードの値
P11_001バス停名図書館前
P11_002バス路線情報_バス区分1,1,3
P11_003_1バス路線情報_事業者名XXバス(株),XXバス(株),YY市
P11_003_2~19(同上 x 18列)(空欄 x 18列)
P11_004_1バス路線情報_バス系統AA01,深夜(AA01),BB線(CC循環コース)
P11_004_2~19(同上 x 18列)(空欄 x 18列)

事業者名、バス系統は、論理的にはひとつずつの属性項目ですが、表の物理構造としては、それぞれ19列ずつ値の格納領域が確保されています。これは、Shapefile形式の仕様上、1列に格納できるデータサイズの上限は255バイトまでであり、路線数が多いバス停留所では1列に収められないことがあるためと思われます。

この方式では、カンマで区切られた個別要素の並び順でみて同じ位置にあるバス区分、事業者名、バス系統によって、ひとつの「バス路線情報」が構成されます。つまり、上の例の「図書館前」バス停は、次の3つのバス路線情報を持っていることになります。

「図書館前」バス停のバス路線情報 (カンマ区切り文字列)
(並び順)バス区分事業者名バス系統
01XXバス(株)AA01
11XXバス(株)深夜(AA01)
23YY市BB線(CC循環コース)

以下、このShapefile形式の属性データを、階層構造の表現が可能な次の3種類のデータ構造に変換するワークスペース例を紹介します。

a. 構造化リスト (Structured List)
b. XML要素
c. JSONオブジェクト


FMEワークスペース例(FME 2017.1.0.0 build 17539)

ステップ 1: 共通の前処理

























AttributeManager: P11_001 (バス停名), P11_002 (バス区分) については属性名を変更し、P11_003_1~19 (事業者名 x 19列), P11_004_1~19 (バス系統 x 19列) については、各列の値をカンマをはさんで連結することによってそれぞれ単一のカンマ区切り文字列とし、新しい属性に格納します。

AttributeTrimmer: 事業者名、バス系統の2列目以降 (P11_003_2~, P11_004_2~) が空欄の場合、連結後の文字列の末尾には1個以上の余分なカンマが付加されるので、ここでそれらを削除します。

AttributeSplitter x 3個: カンマ区切りで列挙されている複数のバス区分、事業者名、バス系統をそれぞれカンマで分割し、分割後の個別のバス区分、事業者名、バス系統を要素とする次の3つのリスト属性を作成します。

type{}
company{}
lineName{}

「図書館前」バス停のバス路線情報 (リスト属性)
i (index)type{i}company{i}lineName{i}
01XXバス(株)AA01
11XXバス(株)深夜(AA01)
23YY市BB線(CC循環コース)

後続のワークフローで、これらの3つのリスト属性を構造化リスト、XML要素、または、JSONオブジェクトに変換します。


ステップ 2-a: 構造化リスト (Structured List) への変換











ステップ 1 で作成された3つのリスト属性から同一のインデクスの要素 - (バス区分, 事業者名, バス系統) を集めることによってひとつのバス路線情報を構成できますが、データ構造として、それらが関連づけられているわけではありません。

これらのリストを、ひとつの「構造化リスト(Structured List または 複合リスト: Complex List)」に変換することによって、共通の名前(リスト名)とインデクスで特定のバス路線情報 = (バス区分, 事業者名, バス系統) を識別できるようになります。

ステップ 2-a の BulkAttributeRenamer (Action: Regular Expression Replace) は、ステップ 1 で作成された3つのリストを、次のように3つのメンバーを持つひとつの構造化リストに変換します。

busRoute{}.type
busRoute{}.company
busRoute{}.lineName

これらの名前は自動的には Workbench のインターフェース上に現れないので、AttributeExposer を使って現しました。

「図書館前」バス停のバス路線情報 (構造化リスト属性)
i (index)busRoute{i}.typebusRoute{i}.companybusRoute{i}.lineName
01XXバス(株)AA01
11XXバス(株)深夜(AA01)
23YY市BB線(CC循環コース)

構造化リストは、ワークスペース内でのリストの操作において、リスト内の要素の位置(インデクス)がメンバー間で同期するという性質を持っています。例えば、ひとつのメンバーの値の昇順あるいは降順でリストの要素を並べ替えたときには他のメンバーも自動的に同じ順で並べ替えられ、個々の要素に属するメンバーの組み合わせは維持されます。

また、ListExploder によって構造化リストの要素ごとにフィーチャーを展開したときに、各要素のメンバーの値は、展開後の各フィーチャーではメンバー名と同じ名前の属性に格納されるということも重要な性質です。

ステップ 2-a のワークフローの後に ListExploder を追加して busRoute リストについてフィーチャーを展開すれば、展開後の各フィーチャーは、バス停名のほかに、ひとつのバス路線情報を構成する3つの属性: type (バス区分), company (事業者名), および lineName (バス系統) を持つことになります。元のデータセットが「バス停名」を主キーとしてそのポイントジオメトリを格納した空間データテーブルであると考えれば、この変換は、「バス停名」を外部キーとするバス路線情報テーブルを作成することに他なりません。

リスト属性については、「文字列の分割とリスト属性の展開」もご参照ください。


ステップ 2-b: XML要素への変換

















XML要素またはJSONオブジェクトならば、属性の階層構造をさらに直接的に表現することができます。

XML、JSON関係のいくつかのトランスフォーマーは、XQuery式の実行をサポートしており、また、リスト属性をシーケンス (Sequence) に変換するXQuery関数 "fme:get-list-attribute" も提供されているので、ステップ 1 で作成した3つのリスト属性に基づいて、属性の階層構造を表現するXML要素またはJSONオブジェクトを作成するのは容易です。

ステップ 2-b の XMLTemplater では、Template Expression パラメーターに次の式を設定することにより、バス停留所フィーチャーの属性セットをひとつのXML要素に変換し、「バス停留所」属性に格納しました。

XMLTempleter | Template Expression:
<busStop>
    <name>{fme:get-attribute("バス停名")}</name>
    <busRoutes>{
        let $companies := fme:get-list-attribute("company{}")
        let $lineNames := fme:get-list-attribute("lineName{}")
        for $type at $i in fme:get-list-attribute("type{}")
        return
        <route>
            <type>{$type}</type>
            <company>{$companies[$i]}</company>
            <lineName>{$lineNames[$i]}</lineName>
        </route>
    }</busRoutes>
</busStop>

XMLTemplater によって作成されたXML要素(「バス停留所」属性)の例
<?xml version="1.0" encoding="UTF-8"?>
<busStop>
    <name>図書館前</name>
    <busRoutes>
        <route>
            <type>1</type>
            <company>XXバス(株)</company>
            <lineName>AA01</lineName>
        </route>
        <route>
            <type>1</type>
            <company>XXバス(株)</company>
            <lineName>深夜(AA01)</lineName>
        </route>
        <route>
            <type>3</type>
            <company>YY市</company>
            <lineName>BB線(CC循環コース)</lineName>
        </route>
    </busRoutes>
</busStop>


ステップ 2-c: JSONオブジェクトへの変換
















ステップ 2-c の JSONTemplater では、Template Expression パラメーターに次の式を設定することにより、バス停留所フィーチャーの属性セットをひとつのJSONオブジェクトに変換し、「バス停留所」属性に格納ました。

JSONTemplater | Template Expression:
{
    "name": fme:get-attribute("バス停名"),
    "busRoutes": [
        let $companies := fme:get-list-attribute("company{}")
        let $lineNames := fme:get-list-attribute("lineName{}")
        for $type at $i in fme:get-list-attribute("type{}")
        return
        {
            "type": $type,
            "company": $companies[$i],
            "lineName": $lineNames[$i]
        }
    ]
}

JSONTemplater によって作成されたJSONオブジェクト(「バス停留所」属性)の例
{
   "name" : "図書館前",
   "busRoutes" : [
      {
         "type" : "1",
         "company" : "XXバス(株)",
         "lineName" : "AA01"
      },
      {
         "type" : "1",
         "company" : "XXバス(株)",
         "lineName" : "深夜(AA01)"
      },
      {
         "type" : "3",
         "company" : "YY市",
         "lineName" : "BB線(CC循環コース)"
      }
   ]
}

ステップ 2-a の構造化リストはFME固有の仕様であり、ソースデータセットから読み込んだフィーチャーのデータ構造を出力先のスキーマに合わせて変換する過程で、必要に応じて中間的なデータ構造としてのみ使えるのに対して、XMLとJSONは標準化されたデータフォーマットなので、出力先データセットに書き込む最終形にもなり得ます。

XMLTemplater, JSONTemplater は、一般に、フィーチャーの属性セット(リスト属性を含む)に基づいて任意のスキーマでXML要素、JSONオブジェクトを作成する場合に使うことができます。


補足: XQuery標準関数の利用

XMLTemplater, JSONTemplater など、XQuery式をサポートするトランスフォーマーでは、XQueryの標準関数もサポートされます。XQuery標準関数の中には、指定した区切り文字で文字列を分割してシーケンスを返す fn:tokenize 関数があるので、例えば、XML要素への変換については、次のXQuery式を XMLTemplater の Template Expression に設定すれば、ステップ 1 の3つの AttributeSplitter は不要になります。JSONTemplater についても同様です。

XMLTemplatere | Template Expression:
<busStop>
    <name>{fme:get-attribute("バス停名")}</name>
    <busRoutes>{
        let $companies := fn:tokenize(fme:get-attribute("事業者名"), ',')
        let $lineNames := fn:tokenize(fme:get-attribute("バス系統"), ',')
        for $type at $i in fn:tokenize(fme:get-attribute("バス区分"), ',')
        return
        <route>
            <type>{$type}</type>
            <company>{$companies[$i]}</company>
            <lineName>{$lineNames[$i]}</lineName>
        </route>
    }</busRoutes>
</busStop>