これは、Go Advent Calendar 2017 その1の17日目の記事です。

はじめに

GoでXMLを処理する際にシンプルなXMLファイルはともかく、入れ子になってたり、XMLがストリーミングで送られてきたりする場合にそれぞれ書き方の工夫が必要だったのでまとめました。
加えてXMLの各要素を構造体に紐付けるにあたって従うべき規則があることを学んだのでそちらも合わせて説明します。

今回やること

下記のXMLファイルに対して以下の2つの処理を行いながらXML要素を構造体に紐付けていきたいと思います。
1.入れ子になってるXMLファイルを処理する
2.↑をDecoderを用いて処理する

↓は今回使用するpost_review.xmlファイルです。

post_review.xml
<?xml version="1.0" encoding="utf-8"?>
<post id="1">
  <content>Final Fantasy XYW</content>
  <developer id="1">S●UARE ENIX</developer>
  <comments>
    <comment id="1">
      <content>It's very fun!</content>
      <review>★★★★★</review>
    </comment>
    <comment id="2">
      <content>So Crazy!</content>
      <review>★★★★</review>
    </comment>
  </comments>
</post>

1.入れ子になってるXMLファイルを処理する

処理自体は以下のコードになります。

xml.go
package main

import (
    "encoding/xml"
    "fmt"
    "io/ioutil"
    "os"
    "log"
)

type Post struct {
    XMLName     xml.Name  `xml:"post"`          //①XML要素名自体を取得
    Xml         string    `xml:",innerxml"`     //②生の(未処理の)XMLを取得
    Id          string    `xml:"id,attr"`      //③XML要素の属性を取得
    Content     string    `xml:"content"`
    Developer       Developer `xml:"developer"`
    Comments    []Comment `xml:"comments>comment"`  //④ネストされた下位要素を直接取得したい場合は構造タグ「a>b」を用いる
}

type Developer struct {
    Id   string `xml:"id,attr"`   
    Name string `xml:",chardata"`  //⑤XML要素の文字データを取得
}

type Comment struct {
    Id      string `xml:"id,attr"`
    Content string `xml:"content"`
    Review  string `xml:"review"`
}

func main() {
    xmlFile, err := os.Open("post_review.xml")
    if err != nil {
        log.Fatal(err)
        return
    }
    defer xmlFile.Close()
    xmlData, err := ioutil.ReadAll(xmlFile)
    if err != nil {
        log.Fatal(err)
        return
    }

    var post Post
    xml.Unmarshal(xmlData, &post)

    fmt.Println(post.XMLName.Local)
    fmt.Println(post.Xml)
    fmt.Println("postID: " + post.Id)
    fmt.Println(post.Content)
    fmt.Println(post.Developer.Id + ": " + post.Developer.Name)
    fmt.Println(post.Comments[0].Id + ": " + post.Comments[0].Content + ", Review: " + post.Comments[0].Review)
    fmt.Println(post.Comments[1].Id + ": " + post.Comments[1].Content + ", Review: " + post.Comments[1].Review)
}

①XML要素名自体を取得
XML要素名自体を保存する時に使います。(今回で言うとpostになります。)
XMLNameという名前でxml.Nameという型のフィールドを指定することで要素名が追加されます。
構造体タグはxml:"post"を指定します。
※構造体タグとは各フィールドの変数定義のあとに「` `」で囲まれている文字列のことです。ここではxmlをキー、postを値と呼び、 「キーと値」の組の文字列を指します。

②生の(未処理の)XMLを取得
xmlデータ自体を保存したい時に使います。
名前は任意のもので構わないですが、型のフィールドはxml:",innerxml"という構造体タグを指定します。

③XML要素の属性を取得
属性と同じ名前のフィールド名を定義して、xml:"<属性名>, attr"という構造体タグを指定します。(属性名はXML属性の名前で、ここでいうと<post id="1">id)

④ネストされた下位要素を直接取得
わざわざ構造体Commentsを作成しなくても、xml: comments>commentという下位層を指定する構造体タグを使うことで直接指定ができます。

⑤XML要素の文字データを取得
XML要素タグと同じ名前のフィールド名を定義(ここではName)し、構造タグはxml:",chardata"を指定します。

注)構造タグの定義については他にもドキュメントに記載されているので興味のある方はどうぞ
The Go Programming Language(package xml)

↓出力結果は以下になります。

post                                       
  <content>Final Fantasy XYW</content>
  <developer id="1">SQUARE ENIX</developer>
  <comments>
    <comment id="1">
      <content>It's very fun!</content>
      <review>★★★★★</review>
    </comment>
    <comment id="2">
      <content>So Crazy</content>
      <review>★★★★</review>
    </comment>
  </comments>
postID: 1
Final Fantasy XYW
1: S●UARE ENIX
1: It's very fun!, Review: ★★★★★
2: So Crazy, Review: ★★★★

2.入れ子になってるXMLファイルをDecoderを用いて処理する

構造体の指方法は上記で挙げたものと変わらないのでmain処理のみ載せます。

xml2.go
func main() {
    xmlFile, err := os.Open("post2.xml")
    if err != nil {
        log.Fatal(err)
        return
    }
    defer xmlFile.Close()

    decoder := xml.NewDecoder(xmlFile)      //①デコーダを作成
    for {                                   //②デコーダ内のXMLを順番に処理
        token, err := decoder.Token()   //③各処理においてTokenメソッドを用いて次のトークンを取得
        if err == io.EOF {
            break                   //④トークンを取り出し終わったら抜ける
        } 
                if err != nil {
            log.Fatal(err)
            return
        }
        switch se := token.(type) {     //⑤型チェック
        case xml.StartElement:
            if se.Name.Local == "comment" {  //⑥トークンがXML要素の開始タグである場合、それがコメント要素であるかを確認
                var comment Comment
                decoder.DecodeElement(&comment, &se)//⑦XMLデータを構造体にデコードする
                fmt.Println(comment)
            }
        }
    }
}

↓出力

{1 It's very fun! ★★★★★}
{2 So Crazy ★★★★}

正直デコーダを生成するのは手間ですが、ストリーミングや大きなXMLデータの場合は有効かと思われます。

終わりに

これまでXML処理の際はネストされている構造体への抵抗感がありましたが、定義に従って抜き出すことでそこまで負担を感じずに操作できました。
あとXML要素の属性や文字データを保存するといった操作があるということを知ってとても勉強になりました。
アプリレビューの情報をAppStoreから取るときなどsortBy=mostRecentされており最新の数百件しか取得できなかったりするのでバックアップの観点からもXML自体の保存は実用的かと思います。

参考資料

The Go Programming Language(package xml)
Goプログラミング実践入門