F# Data


F# Data: JSON パーサーおよびリーダー

F#の JSON 型プロバイダー はF#で作成された 効率の良いJSONパーサーを元にしています。 このパーサーは F# 3.0 Sample Pack 内にあるJSONパーサーを元にしていますが、 F# Dataでは値を動的にアクセスできるようにするための単純なAPIが追加されています。

厳密に定義されたJSONドキュメントを処理する場合、 型プロバイダー を使うと簡単なのですが、 動的に処理するようなシナリオであったり、 単純なスクリプトを手軽に用意したいような場合には パーサーを使ったほうが簡単でしょう。

JSONドキュメントの読み取り

サンプルとなるJSONドキュメントを読み取るには (F# Interactiveの場合)FSharp.Data.dll ライブラリへの参照を追加するか、 プロジェクトで参照を追加します。

1: 
2: 
#r "../../../../bin/FSharp.Data.dll"
open FSharp.Data

FSharp.Data 名前空間にある JsonValue 型を使うと、 以下のようにしてJSON形式の文字列をパースできます:

1: 
2: 
3: 
4: 
let info =
  JsonValue.Parse(""" 
    { "name": "Tomas", "born": 1985,
      "siblings": [ "Jan", "Alexander" ] } """)

JsonValue 型は RecordCollection などのケースを持った判別共用体なので パターンマッチを使ってパース後の値の構造を調査することができます。

JSON用拡張機能を使用する

ここではすべての機能を紹介しません。 その代わり、 FSharp.Data.JsonExtensions 名前空間をオープンすることで 利用できるようになるいくつかの拡張機能について説明します。 この名前空間をオープンすると、以下のような記述ができるようになります:

  • value.AsBoolean() は値が true または false の場合にブール値を返します。
  • value.AsInteger() は値が数値型で、整数として変換可能であれば整数値を返します。 同様に value.AsInteger64() value.AsDecimal() value.AsFloat() といったものもあります。
  • value.AsString() は値を文字列として返します。
  • value.AsDateTime() は値を ISO 8601 か、 1970/1/1からのミリ秒を含んだJSON形式の \/Date(...)\/ でパースして DateTime を返します。
  • value.AsGuid() は値を Guid としてパースします。
  • value?childchild という名前のレコードメンバーを 取得するための動的演算子です。 あるいは value.GetProperty(child) やインデクサ value.[child] を使うこともできます。
  • value.TryGetProperty(child) はレコードメンバーを安全に取得できます (もしメンバーが値無しあるいはレコードではなかった場合、 TryGetPropertyNone を返します)。
  • [ for v in value -> v ] とすると value をコレクションとして扱い、 含まれている要素を走査します。 また、 value.AsArray() とすると、すべての要素を配列として取得できます。
  • value.Properties() はレコードノードの全プロパティのリストを返します。
  • value.InnerText() はすべてのテキストあるいは配列内のテキスト (たとえば複数行文字列を表すデータ)を連結します

数値または日付データとしてパースする( AsFloatAsDateTime などの)メソッドには 省略可能な引数としてカルチャを指定出来ます。

以下のコードはサンプルで指定したJSONの値を処理する方法の一例です:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
open FSharp.Data.JsonExtensions

// 名前と誕生日を表示
let n = info?name
printfn "%s (%d)" (info?name.AsString()) (info?born.AsInteger())

// 兄弟姉妹全員の名前を表示
for sib in info?siblings do
  printfn "%s" (sib.AsString())

JsonValue 型は実際には IEnumerable<'T> インターフェイスを 実装しているわけではありません(つまり Seq.xyz 関数に渡す事はできません)。 GetEnumerator だけが定義されているため、シーケンス式内や for ループで使うことができるというわけです。

WorldBankからのレスポンスをパースする

もう少し複雑な例として、WorldBankへのリクエストに対する レスポンスデータ data/WorldBank.json を サンプルドキュメントにしてみます。 (より便利な方法としては 型プロバイダー を使って WorldBankにアクセスすることもできます)。 このドキュメントは以下のようになっています:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
[ { "page": 1, "pages": 1, "total": 53 },
  [ { "indicator": {"value": "Central government debt, total (% of GDP)"},
      "country": {"id":"CZ","value":"Czech Republic"},
      "value":null,"decimal":"1","date":"2000"},
    { "indicator": {"value": "Central government debt, total (% of GDP)"},
      "country": {"id":"CZ","value":"Czech Republic"},
      "value":"16.6567773464055","decimal":"1","date":"2010"} ] ]

このように、全体としては配列になっていて、 1番目の要素にはレコード、2番目の要素にはデータ点のコレクションが 含まれた形式になっています。 このドキュメントは以下のようにして読み取りおよびパースできます:

1: 
let value = JsonValue.Load(__SOURCE_DIRECTORY__ + "../../../data/WorldBank.json")

なおWeb上から直接データを読み取ることもできます。 また、読み取りを非同期的に実行するバージョンもあります: *

1: 
let valueAsync = JsonValue.AsyncLoad("http://api.worldbank.org/country/cz/indicator/GC.DOD.TOTL.GD.ZS?format=json")

最上位の配列を1番目の(概要を含んだ)レコードとデータ点のコレクションに分けるためには value に対してパターンマッチを使って Jsonvalue.Array のコンストラクタとマッチするかどうか調べます:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
match value with
| JsonValue.Array [| info; data |] ->
    // 概要を表示
    let page, pages, total = info?page, info?pages, info?total
    printfn 
      "%d ページ中の %d ページ目を表示中。 全体のレコード数は %d" 
      (pages.AsInteger()) (page.AsInteger()) (total.AsInteger())
    
    // 非nullのデータ点をそれぞれ表示
    for record in data do
      if record?value <> JsonValue.Null then
        printfn "%d: %f" (record?date.AsInteger()) 
                         (record?value.AsFloat())
| _ -> printfn "失敗しました"

データ点の value プロパティは常に使用できるわけではありません。 直前で説明してある通り、この値は null になることがあります。 その場合にはデータ点をスキップします。 プロパティが null かどうか調べる場合は単に JsonValue.Null と 比較するだけです。

また datevalue のプロパティは元のファイルでは( 1990 のような)数値ではなく、 ( "1990" のような)文字列形式になっている点に注意してください。 この値を int または float として取得しようとすると、 JsonValue は自動的に文字列を特定の形式になるようにパースします。 一般的にはこのAPIがファイルをパースする場合、 できるだけ寛容に値を受け入れるようになっています。

関連する記事

Multiple items
namespace FSharp

--------------------
namespace Microsoft.FSharp
Multiple items
namespace FSharp.Data

--------------------
namespace Microsoft.FSharp.Data
val info : JsonValue

Full name: JsonValue.info
type JsonValue =
  | String of string
  | Number of decimal
  | Float of float
  | Record of properties: (string * JsonValue) []
  | Array of elements: JsonValue []
  | Boolean of bool
  | Null
  member Request : uri:string * ?httpMethod:string * ?headers:seq<string * string> -> HttpResponse
  member RequestAsync : uri:string * ?httpMethod:string * ?headers:seq<string * string> -> Async<HttpResponse>
  override ToString : unit -> string
  member ToString : saveOptions:JsonSaveOptions -> string
  member WriteTo : w:TextWriter * saveOptions:JsonSaveOptions -> unit
  static member AsyncLoad : uri:string * ?cultureInfo:CultureInfo -> Async<JsonValue>
  static member private JsonStringEncodeTo : w:TextWriter -> value:string -> unit
  static member Load : uri:string * ?cultureInfo:CultureInfo -> JsonValue
  static member Load : reader:TextReader * ?cultureInfo:CultureInfo -> JsonValue
  static member Load : stream:Stream * ?cultureInfo:CultureInfo -> JsonValue
  static member Parse : text:string * ?cultureInfo:CultureInfo -> JsonValue
  static member ParseMultiple : text:string * ?cultureInfo:CultureInfo -> seq<JsonValue>
  static member ParseSample : text:string * ?cultureInfo:CultureInfo -> JsonValue

Full name: FSharp.Data.JsonValue
static member JsonValue.Parse : text:string * ?cultureInfo:System.Globalization.CultureInfo -> JsonValue
Multiple items
module JsonExtensions

from FSharp.Data

--------------------
type JsonExtensions =
  static member AsArray : x:JsonValue -> JsonValue []
  static member AsBoolean : x:JsonValue -> bool
  static member AsDateTime : x:JsonValue * ?cultureInfo:CultureInfo -> DateTime
  static member AsDecimal : x:JsonValue * ?cultureInfo:CultureInfo -> decimal
  static member AsFloat : x:JsonValue * ?cultureInfo:CultureInfo * ?missingValues:string [] -> float
  static member AsGuid : x:JsonValue -> Guid
  static member AsInteger : x:JsonValue * ?cultureInfo:CultureInfo -> int
  static member AsInteger64 : x:JsonValue * ?cultureInfo:CultureInfo -> int64
  static member AsString : x:JsonValue * ?cultureInfo:CultureInfo -> string
  static member GetEnumerator : x:JsonValue -> IEnumerator
  ...

Full name: FSharp.Data.JsonExtensions
val n : JsonValue

Full name: JsonValue.n
val printfn : format:Printf.TextWriterFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
val sib : JsonValue
static member JsonExtensions.AsString : x:JsonValue * ?cultureInfo:System.Globalization.CultureInfo -> string
val value : JsonValue

Full name: JsonValue.value
static member JsonValue.Load : uri:string * ?cultureInfo:System.Globalization.CultureInfo -> JsonValue
static member JsonValue.Load : reader:System.IO.TextReader * ?cultureInfo:System.Globalization.CultureInfo -> JsonValue
static member JsonValue.Load : stream:System.IO.Stream * ?cultureInfo:System.Globalization.CultureInfo -> JsonValue
val valueAsync : Async<JsonValue>

Full name: JsonValue.valueAsync
static member JsonValue.AsyncLoad : uri:string * ?cultureInfo:System.Globalization.CultureInfo -> Async<JsonValue>
union case JsonValue.Array: elements: JsonValue [] -> JsonValue
val info : JsonValue
val data : JsonValue
val page : JsonValue
val pages : JsonValue
val total : JsonValue
static member JsonExtensions.AsInteger : x:JsonValue * ?cultureInfo:System.Globalization.CultureInfo -> int
val record : JsonValue
union case JsonValue.Null: JsonValue
static member JsonExtensions.AsFloat : x:JsonValue * ?cultureInfo:System.Globalization.CultureInfo * ?missingValues:string [] -> float
Fork me on GitHub