見出し画像

FastAPI 超初心者向け入門 / チュートリアル2

FastAPIのチュートリアルの続きです。
前回はPython環境をつくり、最小のFastAPIプロジェクトを実行するところまでできました。
(前回の記事)

今回はWebAPIで一番大切なクライアントからのデータの受取りについて解説します。 具体的には文章を受け取り、要約ライブラリを利用して短くした文章をレスポンスするAPIサーバーを作成します。

完成画面

こちらが今回作った要約サイトでAPIを呼んで表示した画面です。 FastAPIの入門とこのページ作成含めて週末だけで作ったものです。
こうした単純なHTMLページを作るのはネットで調べればすぐできますが、サーバー側が…ということは多いと思います。 APIを自分でサクっと作れれば世界が広がるので、ぜひ今回のチュートリアルをお試しください。

APIが作成した要約文を、真ん中の(結果)のBOXに表示しています

※Webサイトも作るとAPI体験から脱線しすぎるので、この記事ではFastAPIのdocs画面からAPIを呼んで動作確認をします

用語 (記事で使う用語と意味)

API :   Web API 
client  (クライアント) :  server/client モデルのクライアントの意味で、ブラウザやアプリのことを指すものとします。
バリデーション: APIに渡した「パラメータ」などの値が正しいかチェックすること。

チュートリアル作成環境

前回作った環境をそのままつかって追記すればOKです。
- python 3.10 以上 , pip  が動く環境
- venv で仮想python環境
を想定しています。

ツール

ターミナル画面。 uvicorn を動かすのに使います。複数分割できるターミナルがオススメ

前回のに書き忘れましたが、ターミナルコンソール(黒いwinodowの画面)とテキストエディターを使います。すでにお使いのものがあればそれを、なければ以下がおすすめです。

テキストエディターの定番はVS-Code

VS-CodeをPythonに設定する記事

(mac 定番のターミナル, iTerm2)

Windowsではコマンドプロンプト


SumyAPIチュートリアル

1.パラーメータのあるAPIを作成

1−1.クエリパラメータとパスパラメータ

↓は本家のガイド

クエリパラメータはURLの後ろに ?id=123 などと 「?」から始まる文字列を付けてパラメータを渡す方法です。

パスパラメータはURLの中にパラメータを入れてしまう方法で、

https://myapi.com/item/123/

のように 「123」の部分がパラメータとして渡す方法です。
これらは混ぜて使うことができます。

こちらのサンプルコードは,item_id からアイテム情報を取得する,というよくあるAPIです。サンプルコードなので、実際のアイテム情報を得る処理は無く、 送られてきた item_id を jsonで返すだけの処理です。
※文章を読むより動かすほうが簡単に理解できるので、是非下記のコードを実行してdocsで結果を確認してください。

from fastapi import FastAPI
app = FastAPI()


@app.get("/items/{item_id}")                       # <-- (1)
async def get_item(item_id: str ):
    res = {"item_id": item_id, }                   # <-- (2)
    return res

(1)でパスパラメータを指定
(2)で受け取った値を返すための辞書データを作成

実行結果はこちら

パスパラメータにshoes26 として、同じ値が帰ってきています

よく見ると、item_id の横に赤い文字でrequired (必須)と書かれています。

これは コードの中で、

async def get_item(item_id: str ):

の item_id : str  にデフォルト値の指定がないためです。デフォルト値が書かれていると、自動的に任意入力のパラメータになる仕組みです。

この item_id は str と文字列型が指定されているので、数値だとエラーになります。
パラメータの値が正しいかチェックすることをvalidation (バリデーション)といいます。FastAPIでは型が正しいか、必須パラメータかを自動でバリデーションしてくれます。 (この辺りまで、上記の短いプログラムを書くだけで面倒を見てくれてしかも動作が高速、というのがFastAPIの良いところです。(

※実際にエラーになったときには自動でエラー情報をFastAPIがレスポンスしてくれます。
docsの画面のAPIの下に、422 Valication Error とレスポンスのサンプルが表示されていますので参考にしてください。

✪クライアントからAPIへクエリパラメータを渡す例

item情報のリストを アイテム種類がringで、最初のデータ25個はスキップして、最大50個ください、とリクエストする場合、

https://myapi.com/items/ring?skip=25&limit=50

これに対応するAPIのコードはこちら。

from fastapi import FastAPI
app = FastAPI()


@app.get("/items/{item_type}")                       # <-- (1)
async def read_user_item(item_type: str, skip:int=0, limit: int=10):
    res = { "item_type":item_type,  "skip":skip ,"limit": limit }    # <-- (2)
    return res


実行結果

1−2. Request bodyで渡す

パスパラメータやクエリではなく、もっと大きなデータが送れるPOSTのbodyのハンドリングには、専用の仕組みが用意されています。 これはFastAPIの心臓部ともいえるpydanticモジュールを使って実現されています。

POSTリクエストで受け取ったbodyにあるパラメータを、そのままレスポンスする例です。

from fastapi import FastAPI
from pydantic import BaseModel


class Item(BaseModel):          # 1) pydantic のBaseModelを継承
    name: str               #初期値なし
    description: str | None = None    #初期値はNone
    price: float
    tax: float | None = None


app = FastAPI()


@app.post("/items/")
async def create_item(item: Item):
    return item

大切なのは(1)からの部分で、 pydantic のBaseModelを継承したclassで、bodyで送信されるデータの定義をすると、自動で処理してくれます。

class Item の中で、 代わった記述があります。

tax: float | None = None

これは
- tax というパラメータはfloat (小数型)
- None という型でもOK
- デフォルトはNone型    (値が送信されなければNone型になる。Noneは「値を持たない」特別な型です。 Noneとするとことで型であり値でもあるのです。)

tax: float = 0.1

とすれば、デフォルト値は0.1 (10%)の税金、となります。

実際にAPIをdocs機能で呼んでみたのが下の画です。最後の

レスポンスではコードで書いたとおり、API呼び出しで設定した同じ値がjsonで帰ってきました。

↓は

{
  "name": "ring",
  "description": "string",
  "price": 10000,
  "tax": 80
}

Python 型ヒントと複数指定
tax : float
   という書き方を 「型ヒント (type hints) 」と呼びます。
これは Python3.5 から追加された機能で、プログラマが変数の型を明示できるようになりました。 さらにコードでは
tax: float | None = None  と記述されています。 とちゅうにある | はプログラムコードでは or と呼ばれる記号で、  "A | B" で "AかBどちらでも"、という意味です。
こちらはPython3.10から追加された機能なので、廃盤となった3.5の後の3.6~3.9では Union という別の機能を使うため、プログラムコードが若干かわります。

これで、自由にAPIサーバーに値を送る(受け取る)ことが出来るようになりました。
早速実用的なAPIを作ってみましょう。

2.sumyモジュールを使う

sumyは要約したい文章と要約後の行数、アルゴリズムを指定するだけで、長い文章を要約してくれるpython用モジュールです。

使い方

venv が activate されているプロジェクトのディレクトリでインストールします。

pip install sumy
pip install tinysegmenter
pip install spacy
pip install -U ginza ja-ginza

sumyは8つのアルゴリズムを選択して要約することができます。(あまり大きな差はないようでしたが。。。文章の種類によって使い分けるようです)

sumyUtil.py

# -*- coding: utf-8 -*-                                                                                                                     
from sumy.parsers.plaintext import PlaintextParser
from sumy.nlp.tokenizers import Tokenizer
from sumy.summarizers.lex_rank import LexRankSummarizer
from sumy.summarizers.lsa import LsaSummarizer
from sumy.summarizers.reduction import ReductionSummarizer
from sumy.summarizers.luhn import LuhnSummarizer
from sumy.summarizers.sum_basic import SumBasicSummarizer
from sumy.summarizers.kl import KLSummarizer
from sumy.summarizers.text_rank import TextRankSummarizer
from sumy.summarizers.edmundson import EdmundsonSummarizer
import spacy
import codecs
 
 
class DocSumy:
    def init( self, lines=10):
        self.sentences_count = lines
        self.text = None
        self.coupus = None
        self.originals = None
        self.parser = None
 
    def __init__(self, lines=10 ):
        self.algorithm = {
            'lex':LexRankSummarizer(),   #ok
            'txt':TextRankSummarizer(),  #ok
            'red':ReductionSummarizer(), #ok
            'luh':LuhnSummarizer(),      #ok
            'sum':SumBasicSummarizer(),  #ok
            'kls':KLSummarizer(),     #ok
            'lsa':LsaSummarizer(),    #ok
            'edm':EdmundsonSummarizer()   #ok
        }
        self.use_corpus = True
        self.sentences_count = lines 
        self.stop_words = ['']
        self.bonus_words = ['']
        self.stigma_words = ['']
        self.null_words = ['']
        self.text = None
        self.coupus = None
        self.originals = None
        self.parser = None
        self.init(lines)   
 
    def create_corpus(self,text):
        self.text = text;
        nlp = spacy.load('ja_ginza')
        corpus = []
        originals = []
        doc = nlp(text)
        for s in doc.sents:
            originals.append(s)
            tokens = []
            for t in s:
                tokens.append(t.lemma_)
            corpus.append(' '.join(tokens))
        return corpus,originals

     def read_text(self,text):
        if self.use_corpus:
            self.coupus,self.originals  = self.create_corpus(text.replace('\r','').replace('\n','').replace('『','「').replace('』','」'))
            self.parser = PlaintextParser.from_string(''.join(self.coupus), Tokenizer('japanese'))
        else:
            self.parser = PlaintextParser.from_string(self.text, Tokenizer('japanese'))
 
 
 def summarize(self,algo):
        #アルゴリズムの取得
        summarizer = self.algorithm[algo]
        if algo == 'edm':
            summarizer.bonus_words = ['']
            summarizer.stigma_words = ['']
            summarizer.null_words = ['']
        summarizer.stop_words = self.stop_words
        summary = summarizer(document=self.parser.document, sentences_count=self.sentences_count)
 
        #要約した結果をリストに格納
        res = []
        for sentence in summary:
            if self.use_corpus:
                #特定の文字列が
                if sentence.__str__() in self.coupus:
                    res.append(self.originals[self.coupus.index(sentence.__str__())])
            else:
                res.append(sentence.__str__())
        return '\n'.join([str(x) for x in res])

APIの作成から脱線して戻れなくなりそうなので、この部分は別のファイルにました。
この処理を書くのが今回の目的ではないので、、main.pyと同じディレクトリに作成すればOKです。

3.要約(サマライズ)APIの作成

APIのコードはこちら。

from .sumyUtil import DocSumy

class sumyParam(BaseModel):     # POSTで受け取るためのclass
    token: str
    text: str
    algo: str = "lex"              # APIで値が来なければ "lex" アルゴリズムを使う
    lines: int = 6                 # APIで値が来なければ 6行以下にサマライズ


@app.post("/sumy/")
async def sumy( body: sumyParam ):      # body という変数に自動で値が入ります
    doc = DocSumy(body.lines)          # 要約後の行数
    doc.read_text( body.text)           # テキストの読み込み
    res = doc.summarize( body.algo  );  # アルゴリズムを指定して呼び出し
    txt = ""
    for sentence in res:
        txt = txt + str(sentence) 
    
    return { "result": txt.split('\n') ,"lines":body.lines,  "original": len(body.text ), "short-len":len( txt ) } 
 


テスト例: Wikipediaのナポレオンから

ナポレオン=ボナパルトは、皇帝になる前から、自らの英雄的資質を人々に印象づけるための肖像画を制作さていた。次の図は、彼が、1800年にサン=ベルナール峠を越えて、北イタリアのマレンゴでオーストリア軍に大勝したときの、アルプス越えの様子を描いたものである。足元の岩には、ボナパルト、ハンニバル、カール大帝の名が刻まれており、ナポレオンが、古代カルタゴの名将ハンニバル、中世フランス王国のカール大帝に続く、近代のアルプス越えの英雄であることが示されている。このようなナポレオンの肖像画は、数多く作成され、フランス国内やナポレオン支配下の国々の宮殿を飾るのに用いられた。 統領政府は立法権も握り、フランス革命の成果を固定するための処置を進めた。1800年にはフランス銀行を設立して中央銀行の役割を与え、通貨の発行、金融・財政の整備、教育の統一など経済と社会の安定を図り、1801年にはローマ教皇との和解(コンコルダート)を実現(信教の自由は継承)した。外交面では1800年に再びアルプスを越えて北イタリアに入り、6月14日、マレンゴの戦いでオーストリア軍と戦い、勝敗はつかなかったがオーストリア軍は退却した。翌1801年のリュネヴィルの和約でライン左岸を獲得し、オーストリアはイタリアから排除された。一方、1802年3月にイギリスとのアミアン和約で当面の講和を実現した。政治の安定を受けて1802年に憲法を改正、終身統領制として自ら就任した。 1804年3月にはナポレオン法典を発布したが、それはナポレオン自身が編纂に参加したもので、法の下の平等、信仰や労働の自由、私的所有権の絶対と契約の自由など、フランス革命の成果を固定させる民法典となった。ハイチ独立を妨害 このころ、西インド諸島のフランス植民地ハイチの独立運動が始まっていた。運動を指導した黒人トゥーサン=ルヴェルチュールは1800年8月、独立を宣言し黒人奴隷を解放した。フランスの国民議会でも黒人奴隷制廃止を決議されていたが、ナポレオンは権力を握るとハイチ独立運動の弾圧に転じ、軍隊を派遣してトゥーサン=ルヴェルチュールを逮捕、本国に連行した。彼はフランスの獄中で死亡したが、ハイチでは独立軍がフランス軍を撃退し1804年、世界最初の黒人共和国として独立した。ハイチでのフランス軍の敗北はナポレオン全盛期の唯一の敗北であった。

結果)

{   
  "result": [     
     "次の図は、彼が、1800年にサン=ベルナール峠を越えて、北イタリアのマレンゴでオーストリア軍に大勝したときの、アルプス越えの様子を描いたものである。",
     "翌1801年のリュネヴィルの和約でライン左岸を獲得し、オーストリアはイタリアから排除された。",
     "フランスの国民議会でも黒人奴隷制廃止を決議されていたが、ナポレオンは権力を握るとハイチ独立運動の弾圧に転じ、軍隊を派遣してトゥーサン=ルヴェルチュールを逮捕、本国に連行した。",
     "彼はフランスの獄中で死亡したが、ハイチでは独立軍がフランス軍を撃退し1804年、世界最初の黒人共和国として独立した。"
   ],
   "lines": 4,
   "original": 990,
   "short-len": 268 
}

次の図は、彼が、1800年にサン=ベルナール峠を越えて、北イタリアのマレンゴでオーストリア軍に大勝したときの、アルプス越えの様子を描いたものである。

翌1801年のリュネヴィルの和約でライン左岸を獲得し、オーストリアはイタリアから排除された。

フランスの国民議会でも黒人奴隷制廃止を決議されていたが、ナポレオンは権力を握るとハイチ独立運動の弾圧に転じ、軍隊を派遣してトゥーサン=ルヴェルチュールを逮捕、本国に連行した。

彼はフランスの獄中で死亡したが、ハイチでは独立軍がフランス軍を撃退し1804年、世界最初の黒人共和国として独立した。

ブラウザからAPIをリクエストする例

こちらは vue3 + axios から呼んでいる例です。簡易的にTOKENを設定して、予め取得したTOKENのマジックコードが一致していればレスポンスする仕様となっています。

    summarize() {
      var URL = "http://localhost:8000/samy/";
      this.error = "";
      this.$store.commit("onClearResult");
      var lines = this.lines;
      var data = {
        token: samyAPI_TOKEN,
        text: this.prepro,
        algo: this.algo,
        lines: lines,
      };
      this.$store.commit("onAPI", false);
      this.axios
        .post(URL, data)
        .then((res) => {
          this.$store.commit("onSumy", res.data);
          this.$store.commit("onAPI", true);
        })
        .catch((err) => {
          this.$store.commit("onAPI", true);
          this.error =
            err.response.data.detail + "\n" + "APIエラーで実行できません";
        });
    },


謝辞

最後まで呼んでいただきありがとうございました。
簡単なチュートリアルにするつもりでしたが、色々と関連することが多く散らかってしまいました。 

駆け足での解説でしたので、いまいちピンとこないところや疑問も多々あるかと思いますが、まずは手にとってアプリを実行してAPIってなんだ?という感触を掴んでいただければと思います。

もっとFastAPIについて知りたい方は是非本家ガイドもご覧なってください。

(日本語版もありますが、中途半端なのとソースがpython3.6用しか乗ってないので英語版をchromeなどで翻訳してみるのが良いかも)

次回はもっと知りたい方のために、FastAPIの解説を備忘録かねて書こうと思います。

この記事が気に入ったらサポートをしてみませんか?