Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
2
Help us understand the problem. What is going on with this article?
@mod_poppo

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

More than 1 year has passed since last update.

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

どうも、自分のブログの脱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)ファイルをパースする

2
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
mod_poppo
最近は浮動小数点数オタクをやっています。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
2
Help us understand the problem. What is going on with this article?