mike-neckのブログ

JavaかJavaFXかJavaEE(なんかJava8が多め)

Golang で XML をパースするために xsd から struct を作り出す

Go言語で XML をパースするメモ

f:id:mike_neck:20181121224358p:plain

とあるxmlgolangでパースしてデータを操作したいので、golangでxsdからstruct のコードを生成するツールを探したところ、次のようなのがあった

github.com

ところが、 リポジトリーの冒頭にも書いてあるように Stale since 2013 (= 2013 から古い)(stale には古い/新鮮ではないといった意味がある様子)とあり、最終更新が8ヶ月前なので、利用するのが少しためらわれた。

そこで、もう少し探してみたところ、次のようなライブラリーを見つけた。

github.com

更新も1ヶ月前なので多分大丈夫だろうということで、早速使ってみることにした。


インストール

go get aqwari.net/xml/...

コード生成

まず手元にパースしたいxmlのxsdを準備する

curl https://maven.apache.org/xsd/maven-4.0.0.xsd -o maven-4.0.0.xsd

次に xsdgen コマンドを呼び出す

xsdgen -o pom.go -pkg pom maven-4.0.0.xsd

なお、主なオプションは次の通り

$ xsdgen --help
Usage of xsdgen:
  -ns value
        target namespace(s) to generate types for
  -o string
        name of the output file (default "xsdgen_output.go")
  -pkg string
        name of the the generated package
  -r value
        replacement rule 'regex -> repl' (can be used multiple times)
  -v    print verbose output
  -vv
        print debug output

コードを生成すると次のようなgoコードが生成される

package pom

import "encoding/xml"

// 4.0.0+
//
// The conditions within the build runtime environment which will trigger the
// automatic inclusion of the build profile. Multiple conditions can be defined, which must
// be all satisfied to activate the profile.
type Activation struct {
    ActiveByDefault bool               `xml:"http://maven.apache.org/POM/4.0.0 activeByDefault,omitempty"`
    Jdk             string             `xml:"http://maven.apache.org/POM/4.0.0 jdk,omitempty"`
    Os              ActivationOS       `xml:"http://maven.apache.org/POM/4.0.0 os,omitempty"`
    Property        ActivationProperty `xml:"http://maven.apache.org/POM/4.0.0 property,omitempty"`
    File            ActivationFile     `xml:"http://maven.apache.org/POM/4.0.0 file,omitempty"`
}

func (t *Activation) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
    type T Activation
    var overlay struct {
        *T
        ActiveByDefault *bool `xml:"http://maven.apache.org/POM/4.0.0 activeByDefault,omitempty"`
    }
    overlay.T = (*T)(t)
    overlay.ActiveByDefault = (*bool)(&overlay.T.ActiveByDefault)
    return d.DecodeElement(&overlay, &start)
}

パースする

では生成されたコードを使って適当なxmlをパースする

とりあえず、ここでは junit-jupiter-api-5.0.0-M3.pom をパースしてみる

まず main 関数. ここは何もしていない

package main

import (
    "encoding/xml"
    "io"
    "log"
    "os"
    "reflect"
)

func main() {
    err := run("cmd/go-xml-analyze/testdata/junit-jupiter-api-5.0.0-M3.pom")
    if err != nil {
        log.Fatalln("error", err)
    }
}

run 関数は普通の xml の取り回し

func run(file string) error {
    log.Println("parse file", file)
    pomXml, err := os.Open(file)
    if err != nil {
        log.Println("failed to open pom file.", err)
        return err
    }
    defer pomXml.Close()

    decoder := xml.NewDecoder(pomXml)

    for {
        elem, err := read(decoder)
        if err != nil {
            return err
        }
        if elem.show() {
            return nil
        }
    }
}

どうハンドリングすればよいかよくわからなかったので適当に作った struct たち

// ただハンドルするだけ
// 終わったら true/終わってなければ false
type Elem interface {
    show() bool
}

// pom の project 要素のハンドル
// dependencies の dependency 要素を表示する
func (m pom.Model) show() bool {
    for _, dep := range m.Dependencies.Dependency {
        log.Println("group:", dep.GroupId, "artifact:", dep.ArtifactId, "version:", dep.Version, "scope:", dep.Scope)
    }
    return true
}

// project 要素が出る前のハンドル
type Another struct {
    token xml.Token
}

func (a *Another) show() bool {
    return false
}

読み取りの処理

func read(decoder *xml.Decoder) (Elem, error) {
    token, err := decoder.Token()
    if err != nil {
        log.Println("failed to get token", err)
        return nil, err
    }
    switch tokenType := token.(type) {
    case xml.StartElement:
        var project Model
        err := project.UnmarshalXML(decoder, tokenType)
        if err != nil {
            log.Println("failed to parse pom", err)
            return nil, err
        }
        return project, nil
    default:
        return &Another{token: tokenType}, nil
    }
}

ポイントとしては、 一番上の要素を宣言して、 xml.Decoderxml.StartElement をその Unmarshal メソッドに渡すとデータをすべてパースしてくれるらしい

というわけで実行結果は次の通り.

2018/11/21 22:39:33 group: org.opentest4j artifact: opentest4j version: 1.0.0-M1 scope: compile
2018/11/21 22:39:33 group: org.junit.platform artifact: junit-platform-commons version: 1.0.0-M3 scope: compile

必要そうなデータが読み取れたようだ