にわとりプログラマーの備忘録

覚えたことをすぐ忘れてしまう、自分のための備忘録ブログです。

Elixir + Phoenix でお手軽にJSONを返すWebAPIを構築

この記事はHamee Advent Calendar 2016の15日目の記事です。

今回は、ElixirのWebフレームワークPhoenixを使って、QiitaのAdvent Calendar 2016ランキングJSONで返すWebAPIを構築してみます。

調べているとDBを利用する場合の記事が多く、DBを利用しない場合に上手く動作しなかったので、DBを使わない場合のの実装方法をまとめておきます。

やること

前提条件

  • Elixirの環境構築済み

インストール

詳細は公式のインストールガイドをご参照ください。

Phoenixのインストール

以下のコマンドでPhoenixをインストールします。

$ mix archive.install https://github.com/phoenixframework/archives/raw/master/phoenix_new.ez

node.jsのインストール

Phoenixではjavascript, cssのcompileにbrunch.ioを利用しているため、node.js(>= 5.0.0)が必要になります。
macの場合はhomebrew経由でインストールできます。

$ brew install node

プロジェクトの作成

プロジェクトを作成していきましょう。
今回はスクレイピング結果をそのままJSON形式で返すAPIサーバーで、DBとViewは不要なので、オプションでその旨を指定しておきます。

$ mix phoenix.new --no-ecto --no-brunch --no-html web_api

ルーティングの設定

web/router.exに、ルーティングの設定を追記していきます。
get "/ranking", CalendarController, :indexが新しく追記した部分です。
/api/rankingにGETリクエストがあった際に、CalendarControllerindexアクションを実行します。

web/router.ex

defmodule WebApi.Router do
  use WebApi.Web, :router

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/api", WebApi do
    pipe_through :api

    get "/ranking", CalendarController, :index
  end

end

ライブラリのインストール

コントローラーの実装に入る前に、スクレイピングで使用するライブラリの依存関係をmix.exsに追記していきます。

mix.exs

defmodule WebApi.Mixfile do
  use Mix.Project

  def project do
    [app: :web_api,
     version: "0.0.1",
     elixir: "~> 1.2",
     elixirc_paths: elixirc_paths(Mix.env),
     compilers: [:phoenix, :gettext] ++ Mix.compilers,
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     deps: deps()]
  end

  # Configuration for the OTP application.
  #
  # Type `mix help compile.app` for more information.
  def application do
    [mod: {WebApi, []},
     applications: [:phoenix, :phoenix_pubsub, :cowboy, :logger, :gettext, :httpoison, :floki]]
  end

  # Specifies which paths to compile per environment.
  defp elixirc_paths(:test), do: ["lib", "web", "test/support"]
  defp elixirc_paths(_),     do: ["lib", "web"]

  # Specifies your project dependencies.
  #
  # Type `mix help deps` for examples and options.
  defp deps do
    [
     {:phoenix, "~> 1.2.1"},
     {:phoenix_pubsub, "~> 1.0"},
     {:gettext, "~> 0.11"},
     {:cowboy, "~> 1.0"},
     {:httpoison, "~> 0.10.0"},
     {:floki, "~> 0.11.0"}
    ]
  end
end

コントローラーの実装

最後にQiitaのAdvent Calendar 2016ランキングからデータを抽出して、JSON形式で返す処理を実装していきます。

JSON形式で返す処理はjson conn, calendarsの部分です。
このように記述することで、PhoenixJSON形式に変換してレスポンスを投げてくれます。

web/controllers/calendar_controller.ex

defmodule WebApi.CalendarController do
  use WebApi.Web, :controller

  def index(conn, _params) do
    calendars = ranking()
    json conn, calendars
  end

  # Advent Calendar 2016ランキングからランキング情報を抽出
  defp ranking() do
    url = "http://qiita.com/advent-calendar/2016/ranking/subscriptions"
    HTTPoison.start
    res = HTTPoison.get! url
    %HTTPoison.Response{status_code: 200, body: body} = res
    body
    |> Floki.find(".adventCalendarRankingListItem-top, .adventCalendarRankingListItem")
    |> Enum.map(&parse_item/1)
  end

  # DOM要素からランキング、タイトル、ページリンクを抽出
  defp parse_item(item) do
    rank  = Floki.find(item, ".adventCalendarRankingListItem_rank") |> Floki.text
    title_link = Floki.find(item, ".adventCalendarRankingListItem_name > a")
    title = Floki.text(title_link)
    url = "http://qiita.com#{Floki.attribute(title_link, "href")}"
    %{rank: rank, title: title, url: url}
  end

end

レスポンスを取得してみる

サーバーを起動して、レスポンスを取得してみます。

$ mix compile
$ mix phoenix.server
$ curl "http://localhost:4000/api/ranking" | jq
[
  {
    "url": "http://qiita.com/advent-calendar/2016/docker",
    "title": "Docker",
    "rank": "1"
  },
  {
    "url": "http://qiita.com/advent-calendar/2016/git",
    "title": "Git",
    "rank": "2"
  },
  {
    "url": "http://qiita.com/advent-calendar/2016/go",
    "title": "Go",
    "rank": "3"
  },
  {
    "url": "http://qiita.com/advent-calendar/2016/job",
    "title": "転職",
    "rank": "4"
  },
  {
    "url": "http://qiita.com/advent-calendar/2016/deeplearning",
    "title": "DeepLearning",
    "rank": "5"
  },
  {
    "url": "http://qiita.com/advent-calendar/2016/vim",
    "title": "Vim",
    "rank": "6"
  },
  {
    "url": "http://qiita.com/advent-calendar/2016/ruby",
    "title": "Ruby",
    "rank": "7"
  },
  {
    "url": "http://qiita.com/advent-calendar/2016/python",
    "title": "Python",
    "rank": "8"
  },
  {
    "url": "http://qiita.com/advent-calendar/2016/muscle",
    "title": "筋肉",
    "rank": "9"
  },
  {
    "url": "http://qiita.com/advent-calendar/2016/nodejs",
    "title": "Node.js",
    "rank": "10"
  },
  {
    "url": "http://qiita.com/advent-calendar/2016/go2",
    "title": "Go (その2)",
    "rank": "11"
  },
  {
    "url": "http://qiita.com/advent-calendar/2016/ouch-hack",
    "title": "おうちハック",
    "rank": "12"
  },
  {
    "url": "http://qiita.com/advent-calendar/2016/go3",
    "title": "Go (その3)",
    "rank": "13"
  },
  {
    "url": "http://qiita.com/advent-calendar/2016/vim2",
    "title": "Vim (その2)",
    "rank": "14"
  },
  {
    "url": "http://qiita.com/advent-calendar/2016/se",
    "title": "システムエンジニア",
    "rank": "15"
  },
  {
    "url": "http://qiita.com/advent-calendar/2016/python_python",
    "title": "Python",
    "rank": "16"
  },
  {
    "url": "http://qiita.com/advent-calendar/2016/docker2",
    "title": "Docker2",
    "rank": "17"
  },
  {
    "url": "http://qiita.com/advent-calendar/2016/tensorflow",
    "title": "TensorFlow",
    "rank": "18"
  },
  {
    "url": "http://qiita.com/advent-calendar/2016/swift",
    "title": "Swift",
    "rank": "19"
  },
  {
    "url": "http://qiita.com/advent-calendar/2016/job2",
    "title": "転職(その2)",
    "rank": "20"
  }
]