やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

【超簡単】PythonとResponderで爆速でAPIを立ち上げるまで

Responderについて

github.com

2018年の10月ぐらいに公開されたPythonのWebフレームワーク
当時からスター稼ぎまくりのおばけプロジェクトだった たまたま仕事で触る機会があったので自分のメモがてらまとめておく

Python界隈では有名な方が作成しており
FlaskとFalcon(これはやったことない)のいいとこ取りな上に
独自の設計思想が加えられて強靭無敵最強のフレームワークに出来上がっている

Responderの売りは恐ろしいぐらいのシンプルさ
Responderをimportするだけでもう使える

import responder

Elixirでのtrotでも同じようなことを言っていますが
今の流行りはシンプルさと制作がいかに早く行えるかということ
その点Responderは両方を兼ね備えている。うーん、良い
学習コストもかなり低く、1時間ぐらいドキュメントを見ただけで何と無く書けてしまう

responderのインストールとサーバーの立ち上げ

公式ではpipenvを使うことを推奨している
無くてもinstall & 使用は可能だけど、素直に従う
pipenvのinstallも簡単で以下を叩くのみ

> pip install pipenv

そしてpipenvを使ってresponderをinstallする
あと、ついでに後にjanomeを使うのでついでにinstall

> pipenv install responder --pre

#これはしなくてもok
> pipenv install janome

では早速、responderでAPIを作成する
その前にプロジェクト用のディレクトリを用意しておく

mkdir cool_api
cd cool_api

作成したファイルに以下を追加
./cool_api/server.py

import responder
api = responder.API()

if __name__ == '__main__':
  api.run()

pipenvを使って作成したserver.pyを実行する

> pipenv run python server.py
INFO: Started server process [62605]
INFO: Waiting for application startup.
INFO: Uvicorn running on http://127.0.0.1:5042 (Press CTRL+C to quit)

やったぜ。上手くserverが立ち上がった
デフォルトでは5042番ポートを使用してサーバーが立ち上がるが

api.run(port=8000)

とすることでポートを変更することが可能
ただ、この状態では一切のendpointが無いので無能of the無能なため
簡単なendpointを実装する

シンプルな実装例(GETとPOST)

まずはシンプルなテキストを返すGET(/)を実装する
./cool_api/server.py

@api.route("/") #endpointを設定
def first_api(req, resp): #引数は基本的に固定
  resp.text = "first success!!" #respに値を設定することで戻り値を作成

早速呼び出す

> curl http://localhost:5042/
first success!!%

上手く返ってきた
続いてjsonを返してみる
./cool_api/server.py

@api.route("/info")
def human_info(req, resp):
  #jsonを返したいときは.mediaを指定する
  resp.media = {"user": "okb"}
> curl http://localhost:5042/info
{"user": "okb"}%

いい感じっすわ

ページのレンダリングとbodyの値の取得

せっかくなので触れておく
まずはページのレンダリングから
server.pyの初回実行時に./cool_api の直下に
/static と /templates が作成されていると思う
/templates 直下に適当にindex.htmlを作成する
responderはデフォルトでjinja2をラップしているのでimport無しで使える
jinja2についての説明はカット

./cool_api/templates/index.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>Simple home</title>
  </head>
  <body>
    <h1>Welcome to my home</h1>
    <ul>
      <li>my name is okb</li>
      <li>I like elixir</li>
      <li>nice to meet you</li>
    </ul>
    {% block content %}{% endblock %}
  </body>
</html>

/main をcallした時にこのファイルを返すようにする
./cool_api/server.py

@api.route("/main")
def simple_homepage(req, resp):
  #.htmlファイルを返すときはresp.htmlを設定 & ファイル名を指定
  resp.html = api.template("index.html")

とりあえずcurlで呼び出す
./cool_api/templates/index.html

> curl http://localhost:5042/main
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>Simple home</title>
  </head>
  <body>
    <h1>Welcome to my home</h1>
    <ul>
      <li>my name is okb</li>
      <li>I like elixir</li>
      <li>nice to meet you</li>
    </ul>

  </body>
</html>%

ブラウザで見れば上手くレンダリングされてるはず

最後にPOSTで送られてきたbodyの値を受け取る
やり方さえ分かってしまえば簡単であとは今まで通り
Javascriptを触る方にはお馴染みだと思うasyncとawaitを使う
pythonでも使えるやってresponder使ってて知った
./cool_api/server.py

@api.route("/easy-post")
#asyncを指定する
async def easy_post(req, resp):
  data = await req.media() #ここで値を受け取る
  input_data = data.get("input", "None data..?")
  resp.text = f"fetch: {input_data}"

受け取った値をテキストで返す
では呼び出す

> url -H "Content-Type: application/json" -X POST -d '{"input": "apple"}' http://localhost:5042/easy-post
fetch: apple%

そこそこ便利なAPIを実装する

以前紹介&作成したjanomeを使用して日本語の形態素解析APIで行う
クラスメゾットに関しては使い回しですまぬ(若干アップグレードしてる
./cool_api/utils.py

import os
from janome.tokenizer import Tokenizer
import re

regex = r"(https?|ftp)(:\/\/[-_\.!~*\'()a-zA-Z0-9;\/?:\@&=\+\$,%#]+)"
pattern = re.compile(regex)

class JanomeSpliter():
  """return keywords(str) in array"""
  def __init__(self):
    self.t = Tokenizer()

  def parser(self, value_str, tag=u"名詞", validate=True):
    tokens = self.t.tokenize(str(value_str))
    if validate:
      res = [self.validate_str(token.surface) for token in tokens if token.part_of_speech.split(',')[0] == tag]
      res = list(filter(lambda x: x != None, res))
      return res
    else:
      res = [self.validate_str(token.surface) for token in tokens]
      res = filter(lambda x: x != None, res)
      return " ".join(res)

  def is_tag(self, value_str, tag):
    tokens = self.t.tokenize(str(value_str))
    res = [self.validate_str(token.surface) for token in tokens if token.part_of_speech.split(',')[0] == tag]
    return True if res else False

  def validate_str(self, value_str):
    if re.match('[あ-んア-ン一-鿐]+', value_str):
      return value_str

server.pyの上部に以下を追記
./cool_api/server.py

from utils import JanomeSpliter

j = JanomeSpliter()

APIを実装する。このAPIではレンダリングjsonの両方をサポートするようにする
URLの値を取得するには際にはendpointで実行する関数に引数を追加する
./cool_api/server.py

@api.route("/ja-parser/{mode}")
async def japanese_spliter(req, resp, *, mode):
  data = await req.media()
  user_input = data.get("input", False)
  if user_input:
    #受け取った値を形態素解析
    splited_input = j.parser(user_input, validate=False)
    if mode == "temp":
      #form.htmlに形態素解析結果を渡す
      resp.html = api.template("form.html", split_result=splited_input.split(" "))
    else:
      #jsonを返す
      resp.media = {"data": splited_input}

jsonを返すAPIを先にcallしておく

> curl -H "Content-Type: application/json" -X POST -d '{"input": "負けない思いを君に伝えたいよ"}' http://localhost:5042/ja-parser/api
{"data": "\u8ca0\u3051 \u306a\u3044 \u601d\u3044 \u3092 \u541b \u306b \u4f1d\u3048 \u305f\u3044 \u3088"}%

あ、何か文字化けしてますね
postman使って呼び出すときちんと形態素解析されている
postmanでの実行結果

{
    "data": "負け ない 思い を 君 に 伝え たい よ"
}

最後にレンダリング用に新規にform.htmlファイルを作成する
jinja2の構文についてはやっぱりカット

./cool_api/templates/form.html

{% extends "index.html" %}

{% block content %}
  <h2>parser form</h2>
  <!-- 送信先のendpointを指定(tempモード) --!>
  <form method="post" action="/ja-parser/temp">
    {{ form }}
    <!-- 受け取る際には"input"をkeyとして取得できる --!>
    <textarea name="input"></textarea>
    <button type="submit">execute</button>
  </form>
  {% if split_result %}
    <p>parse result</p>
    {% for token in split_result %}
      <p>{{ token }}</p>
    {% endfor %}
  {% endif %}
{% endblock %}

先ほどの/main でレンダリングするファイルをform.htmlに変更してブラウザから呼び出してみる
css当ててないのでデザインについてはお許しを...ッ!!

./cool_api/server.py

@api.route("/main")
def simple_homepage(req, resp):
  resp.html = api.template("form.html")

f:id:takamizawa46:20190519011954p:plain:w450

で、formに適当にテキストを入力してexecuteを押す
f:id:takamizawa46:20190519012124p:plain:w450

よし、上手く返ってきた。いいね

長々と書きましたがresponderの便利さが伝われば光栄です