Phoenix で XML をパースして郵便番号検索 API を作る

Elixir で XML のパース処理を実装してみます。

今回は[郵便番号検索 API](http://zip.cgis.biz/) に問い合わせて、その返却結果（XML）を JSON に変換して返す API を作ってみます。
比較的軽めの XML パース処理...ということで上記の API を選びましたが、このテクニックを応用させれば Web スクレイピングなども実装可能と思います。

なお、今回は Erlang の `xmerl` という XML ライブラリを**直で使ってみます**。

Phoenix アプリケーションでの外部 API リクエストについては[コチラ](http://qiita.com/mserizawa/items/3ac483aca4f2926fa1a9)を参照してください。


# Phoenix アプリケーションをセットアップする

`xmlparse_sample` というアプリケーションを作り、mix.exs に以下のように追記しましょう。

```bash
$ mix phoenix.new xmlparse_sample
```

```ex:mix.exs
defmodule XmlparseSample.Mixfile do
  ...
  defp deps do
    [{:phoenix, "~> 0.17"},
     {:phoenix_ecto, "~> 1.1"},
     {:postgrex, ">= 0.0.0"},
     {:phoenix_html, "~> 2.1"},
     {:phoenix_live_reload, "~> 1.0", only: :dev},
     {:cowboy, "~> 1.0"},
     # HTTPoison を追記
     {:httpoison, "~> 0.7.2"}]
  end
end
```

忘れずにライブラリのダウンロードを行います。

```
$ mix deps.get
```

# 郵便番号検索 API に問い合わせる

一気に実装してしまいましょう。
まずはルーティングの設定からです。`/api/postal-code/xxxxxxx` で郵便番号が検索できるようにします。

```ex:router.ex
defmodule XmlparseSample.Router do
  ...
  scope "/api", XmlparseSample do
    pipe_through :api

    get "/postal-code/:code", PageController, :code_search
  end
  ...
end
```

次にコントローラです。受け取った `code` を元に API へ問い合わせます。

```ex:page_controller.ex
defmodule XmlparseSample.PageController do
  ...
  def code_search(conn, %{"code" => code}) do
    HTTPoison.start
    case HTTPoison.get! "http://zip.cgis.biz/xml/zip.php?zn=#{code}" do
      %{status_code: 200, body: body} -> json conn, %{succeed: 200}
      %{status_code: code} -> json conn, %{error: code}
    end
  end
  ...
end
```

特に変わったところはありませんね。

# パースする XML の構造を確認する

実装の前に、今回パースする XML（郵便番号検索 API の返却）の構造を確認しておきましょう。

```bash
$ curl -s "http://zip.cgis.biz/xml/zip.php?zn=1060032" | xmllint --format -
<?xml version="1.0" encoding="utf-8"?>
<ZIP_result>
  <result name="ZipSearchXML"/>
  <result version="1.01"/>
  <result request_url="http%3A%2F%2Fzip.cgis.biz%2Fxml%2Fzip.php%3Fzn%3D1060032"/>
  <result request_zip_num="1060032"/>
  <result request_zip_version="none"/>
  <result result_code="1"/>
  <result result_zip_num="1060032"/>
  <result result_zip_version="0"/>
  <result result_values_count="1"/>
  <ADDRESS_value>
    <value state_kana="ﾄｳｷｮｳﾄ"/>
    <value city_kana="ﾐﾅﾄｸ"/>
    <value address_kana="ﾛｯﾎﾟﾝｷﾞ(ﾂｷﾞﾉﾋﾞﾙｦﾉｿﾞｸ)"/>
    <value company_kana="none"/>
    <value state="東京都"/>
    <value city="港区"/>
    <value address="六本木（次のビルを除く）"/>
    <value company="none"/>
  </ADDRESS_value>
</ZIP_result>
```

郵便番号を投げると、対応する住所が返ってくる仕組みです。
今回は欲しいのは `ADDRESS_value` にある `state`, `city`, `address` とそれぞれに対応するルビです。

ちなみに、住所のような歴史の古いデータ構造には例外がツキモノです。
郵便番号周りでいうと、住所との紐づきが **1対1 ではなく 1対n** だったりします。

```bash
$ curl -s "http://zip.cgis.biz/xml/zip.php?zn=6048072" | xmllint --format -
<?xml version="1.0" encoding="utf-8"?>
<ZIP_result>
  <result name="ZipSearchXML"/>
  <result version="1.01"/>
  <result request_url="http%3A%2F%2Fzip.cgis.biz%2Fxml%2Fzip.php%3Fzn%3D6048072"/>
  <result request_zip_num="6048072"/>
  <result request_zip_version="none"/>
  <result result_code="1"/>
  <result result_zip_num="6048072"/>
  <result result_zip_version="0"/>
  <result result_values_count="2"/>
  <ADDRESS_value>
    <value state_kana="ｷｮｳﾄﾌ"/>
    <value city_kana="ｷｮｳﾄｼﾅｶｷﾞｮｳｸ"/>
    <value address_kana="ﾔｵﾔﾁｮｳ"/>
    <value company_kana="none"/>
    <value state="京都府"/>
    <value city="京都市中京区"/>
    <value address="八百屋町（六角通寺町西入、六角通御幸町東入、六角通御幸町西入、六角通麩"/>
    <value company="none"/>
  </ADDRESS_value>
  <ADDRESS_value>
    <value state_kana="ｷｮｳﾄﾌ"/>
    <value city_kana="ｷｮｳﾄｼﾅｶｷﾞｮｳｸ"/>
    <value address_kana="ﾔｵﾔﾁｮｳ"/>
    <value company_kana="none"/>
    <value state="京都府"/>
    <value city="京都市中京区"/>
    <value address="屋町東入、御幸町通麩屋町下る、麩屋町通六角下る）"/>
    <value company="none"/>
  </ADDRESS_value>
</ZIP_result>
```

つまり、返却する ADDRESS_value は Array にする必要があります。

# XML をパースする

いよいよ本題です。
Elixir では Erlang の XML ライブラリ `xmerl` が標準で利用できます。
流れとしては、「xmerl で XML をパース」 -> 「xpath を使って目的の値を取得」という感じです。

```ex:page_controller.ex
defmodule XmlparseSample.PageController do
  require Record
  use XmlparseSample.Web, :controller

  Record.defrecord :xmlElement, Record.extract(:xmlElement, from_lib: "xmerl/include/xmerl.hrl")
  Record.defrecord :xmlAttribute, Record.extract(:xmlAttribute, from_lib: "xmerl/include/xmerl.hrl")

  ...

  @targets [:state, :city, :address, :state_kana, :city_kana, :address_kana]

  def is_target?(attribute) do
    xmlAttribute(attribute, :name) in @targets
  end

  def parse_address(address_node) do
    attributes = :xmerl_xpath.string('//value', address_node)
      |> Enum.map(&(xmlElement(&1, :attributes)))
      |> Enum.map(fn elems ->
        elems
          |> Enum.filter(&is_target?/1)
      end)
      |> List.flatten

    keys = attributes
      |> Enum.map(&(xmlAttribute(&1, :name)))
    values = attributes
      |> Enum.map(&(to_string xmlAttribute(&1, :value)))

    Enum.into(List.zip([keys, values]), %{})
  end

  def parse_xml(body) do
    {document, _} = body
      |> :binary.bin_to_list
      |> :xmerl_scan.string

    parsed = :xmerl_xpath.string('//ADDRESS_value', document)
      |> Enum.map(&parse_address/1)
  end

  def code_search(conn, %{"code" => code}) do
    HTTPoison.start
    case HTTPoison.get! "http://zip.cgis.biz/xml/zip.php?zn=#{code}" do
      %{status_code: 200, body: body} -> json conn, parse_xml(body)
      %{status_code: code} -> json conn, %{error: code}
    end
  end
end
```

思いの外長くなってしまいました。

まず、`code_search/2` の HTTP リクエスト成功時（status_code=200）に、返却された XML 文字列を `parse_xml/1` に渡すようにします。

`pasrse_xml/1` では、渡された文字列を[文字リスト](http://elixir-ja.sena-net.works/getting_started/6.html#6.3-文字リスト---char-lists)へと変換して `:xmerl_scan/1` へと渡し、解析のベースとなる XML エレメント（中身は構造化されたタプル）を取得します。

ちなみに、この XML エレメントに対して xmerl の関数を使って操作していくわけですが、これらは Erlang のインターフェイスなので [Record](http://ns.maqcsa.org/elixir/docs/v1.0.1/elixir/Record.html) を使って事前にマクロを定義しておきます。
あまり深く考えず、Erlang の機能を使う際のオマジナイと捉えておきましょう。
今回は `:xmlElement` と `xmlAttribute` を使いたいので、ソース上部に宣言してあります。

```ex
require Record

Record.defrecord :xmlElement, Record.extract(:xmlElement, from_lib: "xmerl/include/xmerl.hrl")
Record.defrecord :xmlAttribute, Record.extract(:xmlAttribute, from_lib: "xmerl/include/xmerl.hrl")
```

XML エレメントに対して xpath を使った検索をかけるのが `:xmerl_xpath.string/2` です。
今回走査したい ADDRESS_value はルート直下にあるため、`//ADDRESS_value` と書くだけで OK です。

```ex
parsed = :xmerl_xpath.string('//ADDRESS_value', document)
```

※ xpath は文字リストで表現する点に注意してください。ダブルクオーテーションではありません。

`:xmerl_xpath` はリストを返すので、パイプライン演算子で各要素を `parse_address/1` へと渡します。

`parse_address/1` では、最初にまたしても `:xmerl_xpath` を使って要素の抽出を行っています。今回は ADDRESS_value の中にある value を抽出します。
この XML は「値を attribute で表現する」という構造をしているため、一旦 attribute を全て取得する必要があります。
`xmlElement/2` の第2引数に `:attributes` を指定することで、要素の attribute を取得できます。

```ex
|> Enum.map(&(xmlElement(&1, :attributes)))
```

取得された attribute には不要な項目や予期せぬ項目が含まれる可能性があるため、必要なもののみを抽出します。
これが `Enum.filter/2` のところなのですが、ちょっとデータ構造が複雑になってしまっている都合上、パイプライン演算子をネストしています（あまり良くない書き方かもしれません）
フィルタの条件は「定義済みの Key かどうか」で、Key の定義は[モジュールのアトリビュート](http://elixir-ja.sena-net.works/getting_started/14.html#14.2-定数として---as-constants)で表現しています。

必要な attribute が抽出できたら、最後に~~乱暴に~~List をフラット化します。
この段階で生成される `attributes` の内容は以下のようなイメージです。

```
[
  #xmlAttribute, #xmlAttribute, #xmlAttribute, ...
]
```

XML アトリビュートも、中身は構造化されたタプルで、`:xmlAttribute/2` を使うことで key, value を取得できます。key を取る場合は第2引数に `:name` を、value を取る場合は `:value` を指定します。
あとは、これらの XML アトリビュートから key, value を抜き出してマップに詰めればいいわけです。
方法はいろいろあると思いますが、今回は key と value をそれぞれ別個で抜き出して、最後に `List.zip/2` と `Enum.into/2` の組み合わせでマップに変換しています。

```ex
keys = attributes
  |> Enum.map(&(xmlAttribute(&1, :name)))
values = attributes
  |> Enum.map(&(to_string xmlAttribute(&1, :value)))

Enum.into(List.zip([keys, values]), %{})
```

この段階で生成されるマップは以下のような感じです。

```ex
%{
  "state" => "...",
  "state_ruby" => "...",
  "city" => "...",
  ...
}
```

`parse_xml/1` の返却は、これが ADDRESS_value の数だけ生成されて詰め込まれたリストです。
最後にそれを JSON に変換して完了です。

```ex
json conn, parse_xml(body)
```

# API を叩いてみる

まずは 1対1 のひも付きを持つ郵便番号を検索してみましょう。

```bash
$ curl -s "http://localhost:4000/api/postal-code/1060032" | jq .
[
  {
    "address": "六本木（次のビルを除く）",
    "address_kana": "ﾛｯﾎﾟﾝｷﾞ(ﾂｷﾞﾉﾋﾞﾙｦﾉｿﾞｸ)",
    "city": "港区",
    "city_kana": "ﾐﾅﾄｸ",
    "state": "東京都",
    "state_kana": "ﾄｳｷｮｳﾄ"
  }
]
```

いい感じですね。
次に 1対n のひも付きを持つ郵便番号を検索してみます。

```bash
$ curl -s "http://localhost:4000/api/postal-code/6048072" | jq .
[
  {
    "address": "八百屋町（六角通寺町西入、六角通御幸町東入、六角通御幸町西入、六角通麩",
    "address_kana": "ﾔｵﾔﾁｮｳ",
    "city": "京都市中京区",
    "city_kana": "ｷｮｳﾄｼﾅｶｷﾞｮｳｸ",
    "state": "京都府",
    "state_kana": "ｷｮｳﾄﾌ"
  },
  {
    "address": "屋町東入、御幸町通麩屋町下る、麩屋町通六角下る）",
    "address_kana": "ﾔｵﾔﾁｮｳ",
    "city": "京都市中京区",
    "city_kana": "ｷｮｳﾄｼﾅｶｷﾞｮｳｸ",
    "state": "京都府",
    "state_kana": "ｷｮｳﾄﾌ"
  }
]
```

これも意図通りです。

# 感想

* `xmerl` の使い勝手はあまり良いとは言えない
    * `xmlElement` や `xmlAttribute` といったヘルパーを使って値を取得していくのは、イマイチ直感的ではない...
* Erlang のインターフェイスを使うのは骨が折れる
    * （Erlang に詳しくないと）そもそもの使い方を調べるのに一苦労する
        * Elixir を突き詰めていくと結局 Erlang になるとはこの事か...
    * スタックトレースも省略されるため、デバッグがしづらい
* 結論、[Elixir 用にラップされたライブラリ](https://github.com/h4cc/awesome-elixir#xml)を使ったほうが無難
    * xpath が使えるものがあるかは未調査