4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

HaskellからWordPressのREST APIを叩くメモ

Last updated at Posted at 2019-10-15

この記事はメモ書きです。

どうも、自分のブログの脱WordPress化を画策しているmod_poppoです。

今日は、WordPressのREST APIを叩いてデータを取ってみたいと思います。APIのマニュアルはこの辺にあり、例えば

$ curl https://example.com/wp-json/wp/v2/posts | jq

とすると直近10件くらいの投稿が取得できます。

JSON

WordPressのREST APIのレスポンスはJSONで帰ってきます。これをいい感じにHaskellのデータ型にマップしたいです。

最初は自分でデータ型を定義して GenericFromJSON / ToJSON のインスタンスを定義しようとしたのですが、フィールド名のアレが面倒なのでやめました。代わりに extensible を使います。

extensible を使うと、投稿を表すデータ型が次のように定義できます。各フィールドにアクセスするには Lens と OverloadedLabels を使って post ^. #id という風にすれば良いようです。

type Post = Record '[ "date" >: LocalTime
                    , "date_gmt" >: LocalTime
                    , "guid" >: JSON.Object
                    , "id" >: Int
                    , "link" >: T.Text
                    , "modified" >: LocalTime
                    , "modified_gmt" >: LocalTime
                    , "slug" >: T.Text
                    , "status" >: T.Text
                    , "type" >: T.Text
                    , "title" >: JSON.Object
                    , "content" >: JSON.Object
                    , "author" >: Int
                    , "excerpt" >: JSON.Object
                    , "featured_media" >: Int
                    , "comment_status" >: T.Text -- one of "open", "closed"
                    , "ping_status" >: T.Text -- one of "open", "closed"
                    , "format" >: T.Text -- one of "standard", "aside", "chat", "gallery", "link", "image", "quote", "status", "video", "audio"
                    , "meta" >: JSON.Value
                    , "sticky" >: Bool
                    , "template" >: T.Text
                    , "categories" >: [Int]
                    , "tags" >: [Int]
                    ]

ところで、TypeScriptでいうところのリテラル型のunion "open" | "closed" みたいなやつはHaskellでどう書いたらいいんでしょうか。もちろん自分でデータ型を定義すればいいんですけど、いちいち名前をつけるのが面倒です。わからん。

REST APIの定義

RESTのAPIを関数っぽい形で叩けると便利です。 GET /wp/v2/posts/123getPost 123 という具合です。

やり方は色々あると思いますが、ここでは Servant.Client を使ってみました。GET /wp/v2/postsGET /wp/v2/posts/:id の例は次のような感じになります:

type WordPressAPI = "wp" :> "v2" :> "posts" :> API.QueryParam "page" Int :> API.QueryParam "offset" Int :> API.Get '[JSON] [Post]
               :<|> "wp" :> "v2" :> "posts" :> API.Capture "id" Int :> API.Get '[JSON] Post

wp_api :: Proxy WordPressAPI
wp_api = Proxy

getPosts :: Maybe Int -> Maybe Int -> ClientM [Post]
getPost :: Int -> ClientM Post
getPosts :<|> getPost = Client.client wp_api

エンドポイントが対応している QueryParam を全部書くと getPosts の引数が大量発生して大変そうです。 GetPostsParam みたいなデータ型にまとめられないんでしょうか。よくわかりません。

ちなみにクエリパラメーター等の仕様は

$ curl -X OPTIONS https://blog.miz-ar.info/wp-json/wp/v2/posts | jq

みたいな感じで取ってこれるようです。

コード全体

動くコードを載せておきます。依存するパッケージは base 以外だと

  • aeson
  • extensible
  • http-client
  • http-client-tls
  • lens
  • lens-aeson
  • servant
  • servant-client
  • text
  • time

あたりになります。

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}

import           Control.Lens
import           Control.Monad
import qualified Data.Aeson as JSON
import           Data.Aeson.Lens
import           Data.Extensible
import qualified Data.Text as T
import qualified Data.Text.IO as T
import           Data.Time (LocalTime, UTCTime)
import           Network.HTTP.Client (newManager)
import           Network.HTTP.Client.TLS (tlsManagerSettings)
import           Servant.API hiding (Post)
import qualified Servant.API as API
import           Servant.Client as Client

type Post = Record '[ "date" >: LocalTime
                    , "date_gmt" >: LocalTime
                    , "guid" >: JSON.Object
                    , "id" >: Int
                    , "link" >: T.Text
                    , "modified" >: LocalTime
                    , "modified_gmt" >: LocalTime
                    , "slug" >: T.Text
                    , "status" >: T.Text
                    , "type" >: T.Text
                    , "title" >: JSON.Object
                    , "content" >: JSON.Object
                    , "author" >: Int
                    , "excerpt" >: JSON.Object
                    , "featured_media" >: Int
                    , "comment_status" >: T.Text -- one of "open", "closed"
                    , "ping_status" >: T.Text -- one of "open", "closed"
                    , "format" >: T.Text -- one of "standard", "aside", "chat", "gallery", "link", "image", "quote", "status", "video", "audio"
                    , "meta" >: JSON.Value
                    , "sticky" >: Bool
                    , "template" >: T.Text
                    , "categories" >: [Int]
                    , "tags" >: [Int]
                    ]

type WordPressAPI = "wp" :> "v2" :> "posts" :> API.QueryParam "page" Int :> API.QueryParam "offset" Int :> API.Get '[JSON] [Post]
               :<|> "wp" :> "v2" :> "posts" :> API.Capture "id" Int :> API.Get '[JSON] Post

wp_api :: Proxy WordPressAPI
wp_api = Proxy

getPosts :: Maybe Int -> Maybe Int -> ClientM [Post]
getPost :: Int -> ClientM Post
getPosts :<|> getPost = Client.client wp_api

main :: IO ()
main = do
  manager <- newManager tlsManagerSettings
  baseUrl <- Client.parseBaseUrl "https://example.com/wp-json/"
  let clientEnv = Client.mkClientEnv manager baseUrl
  result <- runClientM (getPosts Nothing Nothing) clientEnv
  case result of
    Left err -> print err
    Right result -> forM_ result $ \post -> case post ^? #title . ix "rendered" . _String of
                                              Just title -> T.putStrLn $ "[" <> T.pack (show (post ^. #date)) <> "] " <> title
                                              Nothing -> T.putStrLn $ "[" <> T.pack (show (post ^. #date)) <> "]: title not available"

私のブログについて実行すると

[2019-10-12 19:14:03] Haskellのscan系関数を使いこなす
[2019-10-01 22:47:30] 技術書典7の振り返り
[2019-09-19 22:55:03] 技術書典7に、Haskellで競技プログラミングをやる本を出します
[2019-08-16 21:33:35] TeXにとってやばい入力ファイル名
[2019-07-15 18:50:05] AtCoderで青色になった
[2019-06-30 22:43:59] Haskellでの浮動小数点数の方向付き丸めを考える
[2019-05-27 21:17:37] HaskellでAtCoderに参戦して水色になった
[2019-04-24 20:57:59] 技術書典6の振り返り
[2019-04-11 20:39:13] 技術書典6に、ClutTeX(LaTeX文書処理自動化ツール)の本を出します
[2019-03-25 22:35:55] AtCoder Beginner Contest 122 の D の別解 (ABC122-D)

という風な結果が得られました。やったね。

デフォルトだと直近10件の記事ですが、 offset または page を変えるともっと古い記事も得られます。

その他

移行などの目的で自分の管理しているWordPressからデータを取ってくるには管理画面の「エクスポート」を使って WordPress eXtended Rss という形式のファイルを吐かせるという方法もあるようです。その場合はXMLのパースが必要になります。

追記:WXRをパースする話を書きました→ HaskellでWordPress eXtended RSS (WXR)ファイルをパースする

4
3
0

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
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?