F# Data: XML 型プロバイダー
このドキュメントではXML型プロバイダーを使用して、静的に型付けされた 方法でXMLドキュメントにアクセスする方法について説明します。 まず、XMLドキュメントの構造が推測される方法について説明した後に、 XML型プロバイダーを使用してRSSフィードを解析するデモを紹介します。
ML型プロバイダーを使用すると、静的に型付けされた方法でXMLドキュメントに アクセスできます。 このプロバイダーはサンプルとなるドキュメントを入力として受け取ります (あるいはサンプルとして使用されるような複数の子ノードを持った ルートXMLノードを含むドキュメントを受け取ります)。 そして生成された型を使用すると、サンプルドキュメントと同じ構造のファイルを 読み取ることができるようになります。 ファイルの構造がサンプルと異なる場合には実行時エラーが発生します (ただし存在しない要素にアクセスしようとした場合に限られます)。
プロバイダーの基本
型プロバイダーは FSharp.Data.dll
アセンブリ内にあります。
このアセンブリが ../../../../bin
ディレクトリにあると仮定すると、
F# Interactive内で読み込むためには以下のようにします
(なおこの型プロバイダーは内部で XDocument
を使用しているため、
System.Xml.Linq
への参照も必要になる点に注意してください):
1: 2: 3: |
#r "../../../../bin/FSharp.Data.dll" #r "System.Xml.Linq.dll" open FSharp.Data |
サンプルからの型の推論
XmlProvider<...>
には string
型の静的パラメータを1つ指定します。
このパラメータには サンプルとなるXML文字列 または
サンプルファイル のいずれかを指定します
(カレントディレクトリからの相対パスか、 http または https 経由で
アクセス可能なファイル名を指定します)。
パラメータの値があいまいで認識できないようなケースはほとんど無いでしょう。
以下のサンプルではルートノードに2つの属性を持ったXMLドキュメントを 読み取ることができるような型を生成しています:
1: 2: 3: 4: |
type Author = XmlProvider<"""<author name="Paul Feyerabend" born="1924" />"""> let sample = Author.Parse("""<author name="Karl Popper" born="1902" />""") printfn "%s (%d)" sample.Name sample.Born |
型プロバイダーによって生成された型 Author
には、XMLドキュメントの
ルート要素にある2つの属性と同じ名前のプロパティがあります。
プロパティの型はサンプルとして指定したドキュメントの値から推測されます。
今回の場合、 Name
プロパティは string
型で
Born
プロパティは int
型です。
XMLは非常に柔軟な形式なので、同じドキュメントを異なる形式で表現できます。
具体的には属性を使用する代わりに、値を直接含むようなネストされたノードとして
表現できます( <author>
以下に <name>
および <born>
をネストさせます)
1: 2: 3: 4: 5: |
type AuthorAlt = XmlProvider<"<author><name>Karl Popper</name><born>1902</born></author>"> let doc = "<author><name>Paul Feyerabend</name><born>1924</born></author>" let sampleAlt = AuthorAlt.Parse(doc) printfn "%s (%d)" sampleAlt.Name sampleAlt.Born |
生成された型を使用すると、同じ形式のドキュメントを読み取る場合には完全に同じAPIで
アクセスできるようになります(ただし1番目の形式を使用するサンプルを AuthorAlt
で解析することはできません。両者は単にpublic APIの形式が同じだけであって、
型の実装としてはそれぞれ別のものだからです)。
この型プロバイダーはノードにプリミティブな値だけが含まれていて、子ノードも属性も 持たないような場合に限って、ノードを単純に型付けされたプロパティへと変換します。
さらに複雑な構造を持つドキュメントに対する型
もう少し興味深い構造を持った例をいくつか見ていくことにしましょう。 まず、ノードが同じ値を持つものの、属性の値が異なる場合にはどうなるでしょうか?
1: 2: 3: 4: |
type Detailed = XmlProvider<"""<author><name full="true">Karl Popper</name></author>"""> let info = Detailed.Parse("""<author><name full="false">Thomas Kuhn</name></author>""") printfn "%s (full=%b)" info.Name.Value info.Name.Full |
ノードが( string
のような)単純型として表現できない場合、
この型プロバイダーは複数のプロパティを持つような型を新しく作成します。
今回の場合、(属性の名前を元にして) Full
というbool型のプロパティが生成されます。
次に要素のコンテンツを返すような、Value
という(特別な)名前のプロパティが
追加されます。
単純な要素を複数持つドキュメントに対する型
もう1つ興味深い例として、プリミティブ値しか持たないような複数のノードが存在する
場合を考えてみましょう。
以下の例ではルートノード以下に複数の <value>
ノードがあるドキュメントを
サンプルとして指定しています( Parse
メソッドにパラメータを指定しなかった場合、
スキーマ用に指定したものと同じテキストが実行時の値に反映されます)。
1: 2: 3: 4: |
type Test = XmlProvider<"<root><value>1</value><value>3</value></root>"> Test.GetSample().Values |> Seq.iter (printfn "%d") |
型パラメータは複数の値を配列として返すような Values
メソッドを生成します。
<value>
ノードは任意個の属性や子ノードを持つわけではないため、
それぞれ int
値となり、 Values
メソッドも単に int[]
を返すようになります!
哲学者たちをもてなす
このセクションでは特定の話題に関する著者のリストを含んだ、単純なドキュメントを
型プロバイダーで処理する方法についてのデモを紹介します。
サンプルとなるドキュメント data/Writers.xml
は
以下のようになっています:
<authors topic="Philosophy of Science">
<author name="Paul Feyerabend" born="1924" />
<author name="Thomas Kuhn" />
</authors>
実行時には型プロバイダーから生成された型を使用して、以下のような文字列を
解析します(構造としてはサンプルのドキュメントと同じですが、
author
ノードに died
属性が含まれているという違いがあります):
1: 2: 3: 4: 5: 6: |
let authors = """ <authors topic="Philosophy of Mathematics"> <author name="Bertrand Russell" /> <author name="Ludwig Wittgenstein" born="1889" /> <author name="Alfred North Whitehead" died="1947" /> </authors> """ |
XmlProvider
の初期化時にはファイル名またはWebのURLを指定できます。
Load
や AsyncLoad
メソッドを使用すると、
ファイルあるいはWeb上のリソースを読み取ることができます。
Parse
メソッドの場合はデータとして文字列を指定できるため、以下のようにすると
データ内の情報を表示することができます:
1: 2: 3: 4: 5: 6: 7: 8: |
type Authors = XmlProvider<"../../data/Writers.xml"> let topic = Authors.Parse(authors) printfn "%s" topic.Topic for author in topic.Authors do printf " - %s" author.Name author.Born |> Option.iter (printf " (%d)") printfn "" |
値 topic
には( string
型の) Topic
プロパティがあります。
このプロパティは同名の属性の値を返します。
また、すべての著者名をコレクションとして返すような Authors
メソッドもあります。
Born
プロパティはいくつかの著者では指定されていないため、
option<int>
型として定義されています。
そのため、 Option.iter
を使用して表示する必要があります。
died
属性はサンプルからの推論時には存在しないため、
静的に型付けられた方法でこの値を取得することはできません
(ただし author.XElement.Attribute(XName.Get("died"))
というコードを使用して
動的に取得することは可能です)。
グローバル推測モード
これまでの例では、同じ名前の要素を(再帰的に)含むような要素は
出てきませんでした(つまりたとえば <author>
以下に <author>
ノードは
決して現れないということです)。
しかしXHTMLファイルのようなドキュメントを扱う場合、こういった状況はよくあることです。
例として以下のようなサンプルドキュメントがあるとしましょう
(単純化したバージョンが data/HtmlBody.xml
にあります):
<div id="root">
<span>Main text</span>
<div id="first">
<div>Second text</div>
</div>
</div>
この例では <div>
要素内に <div>
要素がありますが、いずれも同じ型として
扱われるべきであることは明らかです。
<div>
要素を処理する再帰関数を作成できるようになっていてもらいたいはずです。
このような場合には引数 Global
に true
を指定します:
1: 2: |
type Html = XmlProvider<"../../data/HtmlBody.xml", Global=true> let html = Html.GetSample() |
引数 Global
を true
にすると、型プロバイダーは同名の要素すべてを 一元化 します。
つまりすべての <div>
要素が同じ型として扱われることになります
( <div>
に指定されたすべての属性をプロパティとして持ち、
サンプルドキュメント内で見つけられるすべての子要素の組み合わせが考慮されます)。
型は Html
以下に定義されます。
したがって、Html.Div
を引数にとり、
各 <div>
要素を処理するような printDiv
関数を以下のようにして作成できます:
1: 2: 3: 4: 5: 6: 7: 8: 9: |
/// <div> 要素のコンテンツを表示します let rec printDiv (div:Html.Div) = div.Spans |> Seq.iter (printfn "%s") div.Divs |> Seq.iter printDiv if div.Spans.Length = 0 && div.Divs.Length = 0 then div.Value |> Option.iter (printfn "%s") // すべての子要素と共にルートの <div> 要素を表示します printDiv html |
この関数はまず <span>
内のすべてのテキストを表示します
(今回の例の場合、属性が全く指定されていないため、
string
型として推論されます)。
次に、すべての <div>
要素を再帰的に表示します。
ネストされた要素が見つからない場合は Value
(インナーテキスト)を表示します。
RSSフィードの読み取り
今回の総まとめとして、もう少し実用的な例としてRSSフィードを解析してみましょう。 既に説明した通り、型プロバイダーには相対パスあるいはWebページのアドレスを指定できます:
1:
|
type Rss = XmlProvider<"http://tomasp.net/blog/rss.aspx"> |
このコードではRSSフィード(および http://tomasp.net
で使用されている機能)
を表す Rss
型を生成しています。
Rss
型にはこの型のインスタンスを生成するための機能として、staticメソッド
Parse
、 Load
、 AsyncLoad
が定義されています。
今回の場合、スキーマとして指定したものと同じURIを再利用したいので、
staticメソッド GetSample
を使用します:
1:
|
let blog = Rss.GetSample() |
ここまで来ればRSSフィードのタイトルと直近の投稿一覧を表示することは簡単です。
単に blog
に続けて .
と入力すれば、自動補完の候補一覧が確認できるでしょう。
コードとしては以下のようにします:
1: 2: 3: 4: 5: 6: |
// Title は文字列を返すプロパティです printfn "%s" blog.Channel.Title // すべてのitemノードを取得して、それぞれのタイトルとリンクを表示します for item in blog.Channel.Items do printfn " - %s (%s)" item.Title item.Link |
関連する記事
Full name: XmlProvider.Author
Full name: FSharp.Data.XmlProvider
<summary>Typed representation of a XML file.</summary>
<param name='Sample'>Location of a XML sample file or a string containing a sample XML document.</param>
<param name='SampleIsList'>If true, the children of the root in the sample document represent individual samples for the inference.</param>
<param name='Global'>If true, the inference unifies all XML elements with the same name.</param>
<param name='Culture'>The culture used for parsing numbers and dates.</param>
<param name='ResolutionFolder'>A directory that is used when resolving relative file references (at design time and in hosted execution).</param>
Full name: XmlProvider.sample
Parses the specified XML string
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
Full name: XmlProvider.AuthorAlt
Full name: XmlProvider.doc
Full name: XmlProvider.sampleAlt
Full name: XmlProvider.Detailed
Full name: XmlProvider.info
Full name: XmlProvider.Test
from Microsoft.FSharp.Collections
Full name: Microsoft.FSharp.Collections.Seq.iter
Full name: XmlProvider.authors
Full name: XmlProvider.Authors
Full name: XmlProvider.topic
Parses the specified XML string
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printf
from Microsoft.FSharp.Core
Full name: Microsoft.FSharp.Core.Option.iter
Full name: XmlProvider.Html
Full name: XmlProvider.html
Full name: XmlProvider.printDiv
<div> 要素のコンテンツを表示します
inherit XmlElement
member Divs : Div []
member Id : Option<string>
member Spans : string []
member Value : Option<string>
member _Print : string
Full name: FSharp.Data.XmlProvider,Sample="../../data/HtmlBody.xml",Global="True".Div
Full name: XmlProvider.Rss
Full name: XmlProvider.blog