search
LoginSignup
4

More than 3 years have passed since last update.

posted at

updated at

Organization

今時XML使ってる気象庁から逃げるためにGoでJSONを再配布するAPIを作った

気象データ欲しい!

...とは言ったものの 気象庁 から配信されているデータはXMLなのでパースがめんどくさい。
そこでGoを使いXML(キー(タグ名)の重複が許されるデータ)をいかにJSON(重複が推奨されない)に変換するかを考えました。
もちろん、気象庁が公開している仕様に則って構造体を定義しそれを使ってUnmarshal, Marshalできたらそれに越したことはないのかもしれません。
しかし、多種多様にありまくる情報のフォーマットに対して構造体を定義するのがあまりに面倒だったのでゆるふわに変換できるようにしてみました。

今回の動いてるところ https://kakudo.app/kishow/
トップで提示されるUUIDを上記URLの後に付けると個々のデータにアクセスできます。

今回のリポジトリ https://github.com/kakudo415/kishow

ちなみにこの記事は 12/24 夜 ~ 12/25 深夜にかけて書かれたものです。
もう一度言います。クリスマスに書きました、はい。

方針

まずは、XMLを重複が許されるような構造体に変換した後重複しないようなJSONに変換します。

type Tag struct {
  Name     string
  Value    string
  Children []*Tag
}

お分かりの通り今回は属性は含めていません(いい扱い方を思いつかなかったため)。

XML を 構造体 に

上のような構造体を使ってJSONにする時のことはとりあえず考えずにXMLを構造体に変換していきます。

func (t *Tag) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
    t.Name = start.Name.Local
    for {
        token, err := d.Token()
        if err != nil {
            if err == io.EOF {
                return nil
            }
            return err
        }
        switch token.(type) {
        case xml.StartElement:
            tok := token.(xml.StartElement)
            var data *Tag
            if err := d.DecodeElement(&data, &tok); err != nil {
                return err
            }
            t.Children = append(t.Children, data)
        case xml.CharData:
            cd := string(token.(xml.CharData).Copy())
            t.Value = strings.Replace(cd, "\n", "", -1)
        }
    }
}

XMLのパースは Goで任意のXMLを処理する を参考に(写経)させていただきました。
タグの中がタグか文字情報かで分岐して(参考元では属性も扱っている)順に読んでいってるみたいです。

構造体 を JSON に

↑の構造体をそのまま json.Marshal() してしまえば必要な情報が正しいJSONで出力されるのですが、キー名を重複させないための構造になっているためとても冗長です。
なので、被っているキーの部分を配列にしてその中にそれぞれの被ったキーの中身をいれることにします。
つまり、こういう風にこのまま出力すると冗長なJSONになるところを

{
  "name": "TagName",
  "value": "",
  "children": [
    {
      "name": "Duplication",
      "value": "hoge",
      "children": null
    },
    {
      "name": "Duplication",
      "value": "fuga",
      "children": null
    }
  ]
}

このように重複したキーのデータを配列にまとめてしまうことでキー名が被らずかつ冗長さがなくなります(使う人は配列かどうかのチェックは必要)。

{
  "TagName": {
    "Duplication": [
      "hoge",
      "fuga"
    ]
  }
}

今回は重複したキーを親が判定してから子を再帰関数でキー名とデータのJSON文字列を返すようにしました。

func (t *Tag) MarshalJSON() ([]byte, error) {
    result, err := innerJSON(t.Name, []*Tag{t})
    if err != nil {
        return []byte{}, err
    }
    result = `{` + result + `}`
    return []byte(result), nil
}

呼び出すだけの MarshalJSON() はとても簡潔です(今回は気象庁XMLの仕様で根要素は被らないことが分かっているので重複キーを判定する処理を省いています)。

func innerJSON(name string, elms []*Tag) (string, error) {
    result := `"` + name + `":`
    if len(elms) >= 2 {
        result += `[`
    }
    for i, elm := range elms {
        if i >= 1 {
            result += `,`
        }
        if len(elm.Children) == 0 {
            result += `"` + elm.Value + `"`
        } else {
            result += `{`
            dupKeys := map[string]bool{}
            for i, ec := range elm.Children {
                if dupKeys[ec.Name] {
                    continue
                }
                inner, err := innerJSON(ec.Name, sameKeys(ec.Name, elm.Children))
                dupKeys[ec.Name] = true
                if err != nil {
                    return result, err
                }
                if i >= 1 {
                    result += `,`
                }
                result += inner
            }
            result += `}`
        }
    }
    if len(elms) >= 2 {
        result += `]`
    }
    return result, nil
}

キー名を出力した後に、まず同キー名の要素が複数ある場合は配列の中に入れ、その中で子要素に対して同じキー名の要素はあるかを判定後その結果とともに再帰させています。二回目に同じ名前の子要素に出会ったときは無視しています。
主な処理はそれだけです(残りは , とか [] とかをちょこちょこ出力してるだけです)。

最後に

属性をどう扱うかが大変悩ましいですが、現状は困っていないのでこのままいきます(含めるとなると今回のような簡潔さが失われそう)。
†さいきょうのでーたふぉーまっと† に出会いたい・・・むしろ覇権を取ってほしい。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
4