Clojure APIクライアント開発

主にサービスのAPIを叩くクライアントプログラムのノウハウまとめ.

Clojure HTTP Client

主なクライアントライブラリにhttp-kitとclj-httpがある. httpリクエストに関してはどちらも同じことができる. 非同期発行も.

http-kitは2022年現在メンテナンス状況が怪しく見えた. そしてclj-httpは後発のalephやjava-http-cljと比べるともう古いように思える.

いずれにしろ, どれもClojureのWeb抽象であるClojure: Ringの仕様に従っている.

🔧dakrone/clj-http

https://github.com/dakrone/clj-http

Ring の影響を受けているため Request Mapを入力としてResponse Mapを返す.

以下小技. ライブラリのチューニングにはclj-httpというよりもHTTPプロトコルの一般的な理解や経験が生きてくる.

tips: debug 出力

{:debug true} をRequest Mapに含めると, 標準出力にRequest Mapの内容が表示される.

tips: clj-httpにおけるjsonの扱い

ClojureでJSONを扱うライブラリである cheshire をインストールすると, jsonとの連携が{:as :json} でできる. この指定がされると clj-httpはrepsonse dataをcheshire でjsonにパースする.

なお 単純に {:headers {:accept “application/json”}}を設定したいだけなら{:accept :json}を指定する.

ref: 💡Accept と Content-Typeの違い

howto: HTTPエラーの扱いと例外ハンドリング

clj-httpはHTTP Responseが正常終了以外の場合(200 201 202 203 204 205 206 207 300 301 302 303 304 307)はslingshotのインスタンスを返すため, slingshotに合わせたエラーハンドリングが期待される.

すなわち(try… (catch Exception ex))で補足するか, slingshotのtry+ blockを使うか. slingshotはclj-httpをインストールすると一緒についてくる.

howto: clj-httpのリクエストをproxy経由にするには?

Request Mapに proxy-host, prox-port, proxy-user, proxy-passを含める.

ref: clj-http: Proxies

howto: clj-httpで WARN log: Invalid ‘expires’ attribute

request mapのオプションに :cookie-policy :standard を追加.

ref: Invalid ‘expires’ attribute? - GitHub

howto: タイムアウト値を設定するには?

Request Mapに 値をmillsecondで設定.

{:socket-timeout 1000
 :connection-timeout 1000
 :connection-request-timeout 1000}

clj-httpは JavaのApache HttpClentのWrapperであり仕様はこれに従う.

SocketTimeout/Connection Timeout/Connection Request Timeout

:throw-exceptions falseによる例外抑止

Effective Java “Use Excetions only for exceptional conditions”に従うと, {:thrwo-exceptions false}にすることで例外抑止しつつ自分で例外処理をかくのが吉.

ページが存在するかチェックするには?

スクレイピング用途で利用するときに404がかかるとわかっているならばgetを投げないほうがいい. こんなときはhead requestで事前に存在確認したい.

ただしclj-httpではエラーコードは自動で例外を上げるため, それを防止して自分で中身のコードをチェックするときはRequest Mapに {:throw-exceptions false} を設定する.

(defn page-exists? [url]
  (if-let [resp (client/head url {:headers          headers
                                  :throw-exceptions false})]
    (= (:status resp) 200)
    false))

🔗References

🔧schmee/java-http-clj

clj-httpをベースにJava11と📝HTTP/2をサポート. clj-httpは2015に登場したHTTP/2をサポートしていない.

⚠クエリ文字列(query string)は指定できない

HttpClient APIも対応していないのでこれはライブラリの対応外という方針から. 自力でuriにつける.

✅非同期requestとManifoldを連結するには?

async APIはJavaの CompletableFutures を返すのでこれと📝Clojure: Manifoldの連携を考えるらしい. どうも現在進行系でPRが進んでいるようだ.

an easy way to adapt a deferred to Java CompleteabaleFuture · Issue #189 · clj-commons/manifold · GitHub

ただし, 同期Requestを投げてmanifoldのfutureをつかって待ち合わせもできる. d/let-flow内で非同期処理をして最後の戻り値でderefをすることによってrealizedする.

(defn fetch-ticker [this market]
  (let [query-params {"symbol" market}]
    @(d/let-flow [ticker (d/future (get-public-query-24-ticker this query-params))
                  orderbook (d/future (get-public-query-orderbook this query-params))
                  trades (d/future (get-public-query-trade this query-params))]
                 (let [ask (-> orderbook :orderbook-p :asks first first)
                       bid (-> orderbook :orderbook-p :bids first first)
                       ltp (-> trades :trades-p first)]
                   {:ask ask
                    :bid bid
                    :ltp nth ltp 2}))))

tip: SSL証明書チェックをスキップする

オプションとして自前のclientを作成してそれを渡すことでいろんな通信設定をする.

(defn accept-all-certificates []
  (let [trust-manager  (proxy [X509TrustManager] []
                         (getAcceptedIssuers [] (into-array java.security.cert.X509Certificate []))
                         (checkClientTrusted [chain auth-type] nil)
                         (checkServerTrusted [chain auth-type] nil))
        trust-managers (into-array X509TrustManager [trust-manager])
        ssl-context    (SSLContext/getInstance "TLSv1.2")]
    (.init ssl-context nil trust-managers nil)
    ssl-context))
 
(def opts
  {:client
   (http/build-client
    {:connect-timeout  10000 ;; 10s
     :ssl-context (accept-all-certificates)})})

🎨Clojure API Client ベストプラクティス

💡let or throw pattern on http request

ref. 🔎if-let は処理の結果による分岐でつかう

(let [resp (client/get xxx)]
  (-> resp
      :body
      xxx
      (or (throw (ex-info "Exception occured"
                          {:response resp})))))

💡Clojureでクエリ文字列を生成するには?

クエリ文字列をClojureで生成する方法

;; https://github.com/ring-clojure/ring-codec
 
(ring.util.codec/form-encode {:symbol "BTCUSDT"})
;; 記号も置き換えられる.
(ring.util.codec/url-encode {:symbol "BTCUSDT"})

ref. Clojure building of URL from constituent parts - Stack Overflow

💡pcoll pattern in clojure http request

http requestの呼び出しでよく使うと思う. applyがミソ.

pcoll pattern on Clojure

(defn request [req-fn & req-args]
  (try
    (when-let [resp (apply req-fn req-args)]
      resp)
    (catch Exception e [false e])))

Chunked request pattern

リストを指定するときに一回の数にlimitがある場合によく使うパターン. partition-allを使ってlistをchunkにする.

(defn get-bulk->p [& list]
  (p/let [results (p/all
                   (map (fn [chunked]
                          (apply get-reqest chunked))
                        (partition-all chunk-length list)))]
    (apply merge results)))

💡HTTP Requestのパラレル実行

スレッドを使って同時にhttp requestを出す.

例えばpmapをつかってマップに格納された処理を並列実行することができる.

ref. 💡Lazy Sequence + pmap + doallの並列スクレイピングが強い

httpkitやclj-httpにも並列実行をするoptionがある.

core.asyncは調査中…

Clojure WebSocket Client

aleph

Netty & Manifoldをbaseにしたclient lib.

📝Clojure: aleph

clj-socketio-client

https://github.com/ejschoen/clj-socketio-client

Socket.IOのClojureライブラリ. socket.ioの公式DocにはClojureクライアントライブラリは乗っていない. JavaはあるのでこのライブラリはJava版のWrapper.

https://github.com/socketio/socket.io-client-java

コードを覗くと短いコードなのでそれを参考にJava版のwrapperを自前実装してもいいかも.

このJava版は内部でThreadを生成して制御するようで, ライブラリの外からThreadの制御を出来るようなものではないようだ. メソッドコールのたびに, たとえばemitを呼ぶたびにRunnable()オブジェクトを作成してThreadPoolのThread上で処理をループさせている.

Clojure: sente

Clojureのためのリアルタイム通信ライブラリ.

https://github.com/ptaoussanis/sente


senteは日本語の囲碁の先手が由来とかwww先手を打つくらい爆速を目指す設計思想, 先手必勝!!! timbreと同じ作者. この人tempuraみたな名前のライブラリとかもつくってるので日本好きだな.


Or: We don’t need no Socket.IO

Or: core.async + Ajax + WebSockets = The Shiznizzle

Socket.IOがWebsocketの欠点を改良したものならば, senteも独自改良したらしい(auto-failback).

そしてもう一つの特徴は, 内部制御で📝Clojure core.async(go-loop)を使っていること.

https://github.com/ptaoussanis/sente/issues/227

他のライブラリはJavaの肩に乗っている影響でJavaのThread/ThreadPoolの仕組みに依存しているがcore.asyncには依存していない. Clojureのcore.asyncは普通のThreadとは違い軽量スレッドなため, そこがsente(先手)の意味だというのがわたしの自己解釈.

sente: Usage

Client Sideのコードを参考にする.

sente: References

Clojure Websocket Client Topics

✅ClojureでProducter-Consumer Patternをするにはcore.async

Producer-Consumer Patternはスレッド同士で情報をやり取りするための仕組み.

Clojureだと, 📝Clojure core.asyncを使える. core.asyncのチャネルの概念はキューの拡張.

別スレッドでwebsocket経由で情報を受信して, core.asyncのchannel経由でスレッド間通信によって情報を非同期で受け取る.

✅websocketのevent-handlerでmultimehodを利用する

Clojure: senteのexampleからのインサイト. websocket経由で受信したmessageをdefmultiで条件分岐する. これはmultimethodをつかったよい例.

https://github.com/ptaoussanis/sente/blob/master/example-project/src/example/client.cljs

ref. 📝Clojure マルチメソッド(multimethod)

✅handlerで受け取ったmessageを変換して保存するには?

サンプルコードはprintlnで標準出力するものばかり. そうではなく受信したデータを保存したい.

References

勉強の参考に目を通したもののブックマーク.