この記事について

PythonのWebアプリケーションフレームワーク「Django」についてのチュートリアル記事です。今回は「Django REST framework」を使ったWebAPIと、それを使った簡単なWebアプリケーションを構築します。具体的にはこちらの記事の初心者向け実践記事となります。

Djangoの機能の細かい説明は省略します。まずは実用的なアプリケーションを完成させ、疑問点等についてはWebや参考リンク等で各々確認するスタイルでお願いします。

執筆環境

OS Windows 7
Python 3.6.3
Django 2.0.5
django-rest-framework 0.1.0

この記事で作るもの

システム概要

image.png

  • IoTシステムをイメージしています。Raspberry Piのようなセンサーデバイスが気温と湿度の情報をWebAPIに送信します。
  • 集計情報をグラフ表示するWebアプリケーションを同じサーバで公開します。データの取得はWebAPI経由で行います。
  • WebAPI、WebアプリケーションへのアクセスはID/Passwordで認証させます。デバイスからWebAPIへの認証は簡易利用を想定してBasic認証とします。

画面イメージ

image.png

用語解説

※初心者向けの解説です。正確な内容については参考リンクで再確認してください。

WebAPI ってなに?

Webアプリケーションはブラウザからしか使えない。…そんなふうに考えていた時期が俺にもありました。

WebAPIとは、Webアプリケーションを「ブラウザ以外」からでも利用できるようにするための技術です。

通常のWebページは画面に表示するのが目的なので、本来の目的のデータに加えてレイアウト情報やアイコン画像など人間が理解しやすくするためのデータが大量に含まれます。WebAPIでは通信するデータを純粋にデータのみに限定し、表示や加工処理をクライアントのアプリケーションに任せます。

当初はGoogleマップのようにページ全体を再読み込みさせずに追加データを次々と表示させる技術として使われました。画面とデータを分けて管理できること、データの管理単位を分割し複数のWebAPIを組み合わせて利用できるメリットが評価され、現在ではスマホアプリやIoTデバイスのデータアクセス方法として利用され社会に欠かせないものになりました。

ExcelやFileMakerのようなアプリケーションの最新版にもWebAPI連携機能が搭載されており以前よりカジュアルな利用が可能になっています。

参考:WebAPIについての説明

参考:5分で分かるWebAPI | NTT Communications Developer Portal

REST ってなに?

WebAPIの共通仕様です。

WebAPIで公開するリソースは、エンドポイントと呼ばれるURLを持ちます。
エンドポイントに対して特定のHTTPメソッドでアクセスすると、リソースに対する特定の操作の意味を持ちます。例としては以下のような感じです。

# メソッド # エンドポイント
POST /users # 新規作成
GET  /users # ユーザ一覧の取得
GET  /users/1 # ユーザID = 1のユーザ情報を取得
PUT  /users/1 # ユーザID = 1のユーザ情報を更新
DELETE /users/1 # ユーザID = 1のユーザ情報を削除

通常のWebアプリケーションでは使わないPUT、DELETEを利用しているのがポイントです。送受信するデータのフォーマットはXMLとJSONが使えますが、現在ではJSONが主流になっています。

参考:REST入門 基礎知識

Django ってなに?

PythonのWebアプリケーションフレームワークです。
CakePHPやRuby on Rails のPython版と思ってください。

詳しくは前回のリンク集を参照してください。

Djangoでは「Django REST Framework」というパッケージが提供されています。
WebAPIを実際に作ろうとするとかなり手間がかかるのですが、このパッケージを利用することで素人でも簡単にRESTなWebAPIを公開することができます。

参考:Django REST Frameworkを使って爆速でAPIを実装する

サンプルコード

完成品のアプリケーションをGithubに公開しました。
https://github.com/okoppe8/Django-REST-Framework-easy-sample

Git-Cloneもしくはダウンロード後、プロジェクトのディレクトリに移動して以下のコマンドを順に実行してください。

※Windows用 Macでは適切に読み替えてください。

python -m venv env
env\Scripts\activate
pip install -r requirements.txt
manage.py migrate
manage.py prepare_dummydata
manage.py createsuperuser 
manage.py runserver

最後のrunserverでWebアプリケーションが起動するので、ブラウザよりhttp://localhost:8000にアクセスしてください。ログイン画面ではcreatesuperuserで入力したID/PASSを使います。

herokuへのデプロイ方法

サンプルコードはherokuにアップロードしてインターネット上で試すことができます。こちらに丁寧にやりかたが乗ってますので手順通りに進めてみましょう。

ただし以下の2点が異なるので適切に修正してください。

  • Procfile の「mysite」は自分で付けたプロジェクト名に変える。サンプルのままなら「project」となる。
Procfile
web: gunicorn project.wsgi
  • チュートリアル内のpsycopg2のバージョンは「2.5.4」を指定しているが、最新版「2.7.4」に置き換える。
requirements.txt
psycopg2==2.7.4

アプリケーションの作成

作業概要

プロジェクト名等について

サンプルコードではアプリケーションに以下のように名前を付けています。
流用する場合は具体的な名前に置き換えてください。

プロジェクト project
アプリケーション api
モデル Log

作業手順

この記事では以下の手順で作業をすすめます。

手順 作業内容 作業ファイル
1 プロジェクト作成
2 設定ファイル編集 project/settings.py
3 モデル作成 api/models.py
4 データベース作成
5 管理アプリによる動作確認 api/admin.py
6 ビュー作成 app/views.py
7 ルーティング設定編集 project/urls.py
8 テンプレート作成 templates/index.html
9 データ登録を検証

手順1.プロジェクト作成

pythonをインストールしたマシンの作業用ディレクトリで以下のコマンドを実行します。
※Windows用なのでMacOSでは適切に読み替えてください。

mkdir project
cd project
python -m venv env
env\Scripts\activate
pip install django django-rest-framework django-filter
django-admin startproject project .
manage.py startapp api

さらに手動でプロジェクト直下にtemplatesディレクトリを追加し、空ファイルindex.htmlを作成してください。実行後、ディレクトリは以下の状態になったはずです。

project
│  manage.py
├─app
│  │  __init__.py
│  │  admin.py
│  │  apps.py 
│  │  models.py
│  │  tests.py
│  │  views.py
│  ├─migrations
│    └__init__.py
├─project
│  │  __init__.py
│  │  settings.py
│  │  urls.py
│  └─ wsgi.py
└─templates   #追加
   └index.html #追加

この構成が最終形となります。自動出力されるファイルを除き、この後でファイルを作ることはありません。

追加パッケージの説明

pip install で django以外に追加パッケージをインストールしました。 各パッケージの内容は以下の通りです。

パッケージ名 説明
django-rest-framework DjangoでRestfulなWebAPIを構築する
django-filter WebAPIに検索機能を追加する

手順2.設定ファイル編集

プロジェクトの設定ファイル「project/settings.py」の編集を行います。
まずINSTALLED_APPSにパッケージとアプリケーションの設定を追加します。

project/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',  #追加
    'django_filters',  #追加
    'api',             #追加
]

TEMPLATESのDIRSに'templates'という文字列を追加します。先ほど追加した'templates'ディレクトリが認識されます。

project/settings.py
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': ['templates'], #変更
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

次にタイムゾーンの設定を探して、日本時間に置き換えてください。

project/settings.py
# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/

LANGUAGE_CODE = 'ja-JP' # 上書き

TIME_ZONE = 'Asia/Tokyo' # 上書き

さらにファイルの最後に以下の設定を追記してください。

project/settings.py
# 以下、追加

# rest_framework 設定
REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    )
}

# 管理画面のログインを通常ログインとして流用する
LOGIN_URL = '/admin/login/'
LOGOUT_REDIRECT_URL = '/'

以上で設定ファイルの編集は完了です。

手順3.モデル作成

データモデルを定義します。api/models.pyを開いて以下のコードを追加してください。

api/models.py
from django.db import models


# Create your models here.
class Log(models.Model):
    created_at = models.DateTimeField(
        verbose_name='登録時間',
        auto_now_add=True,
    )
    temperature = models.FloatField(
        verbose_name='気温',
        blank=True
    )
    humidity = models.FloatField(
        verbose_name='湿度',
        blank=True
    )

    # 以下は管理サイト上の表示設定
    def __str__(self):
        return self.created_at

    class Meta:
        verbose_name = '採取データ'
        verbose_name_plural = '採取データ'

シンプルに登録時間、気温、温度のみのモデルとしました。デバイスによってはクライアント側でタイムゾーンの指定をするのは面倒なことがあるので、登録時間はサーバ側で自動的につけるようにしています。

手順4.データベース作成

データベースを作成します。以下のマイグレーションコマンドを実行してください。デフォルトの設定ではプロジェクト直下にsqliteのファイルが作成されます。

manage.py makemigrations
manage.py migrate

★ ダミーデータの投入方法

サンプルコード内にダミーデータの投入スクリプトを用意しています。2018年5月1日~7日の東京の気温・湿度のデータを直近7日分のデータとして登録します。使い方は以下の通りです。

1.サンプルコード内のディレクトリapi/fixturesapi/managementをapiディレクトリ下にコピーする

2.以下のコマンドを実行する

manage.py prepare_dummydata

手順5.管理アプリの動作確認

Djangoのプロジェクトに付属する管理アプリケーションの設定を行います。

以下のファイルを編集してください。

api/admin.py
from django.contrib import admin

from .models import Log


@admin.register(Log)
class LogAdmin(admin.ModelAdmin):
    pass

コマンドラインより以下のコマンドを入力し管理者ユーザーを作成します。

manage.py createsuperuser

コマンドラインより以下のコマンドでアプリケーションを起動します。

manage.py runserver

起動後、ブラウザでhttp://localhost:8000/adminにアクセスします。
ログイン画面が表示されるので、createsuperuserで指定したIDとPasswordを使ってログインします。サイト管理アプリではユーザー管理のほか、データベースの直接参照・編集が可能です。先ほど投入したダミーデータを確認してみましょう。

image.png

管理アプリでは新規ユーザーの登録が可能です。実際に運用する場合はこちらからユーザーの追加をしてください。

ユーザー追加時の注意

このシステムではサイト管理者用のログイン機構を一般ユーザー用として流用しています。ユーザーを追加するときは「スタッフ権限」に必ずチェックを入れてください。入れないとログインできません。

image.png

手順6.ビュー作成

WebAPIとWebアプリの本体となるビューの設定を行います。api/views.pyを開いて以下のコードを入力してください。

api/views.py
import django_filters
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import TemplateView
from rest_framework import serializers
from rest_framework import viewsets
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.permissions import IsAuthenticated

from .models import Log


class LogSerializer(serializers.ModelSerializer):
    class Meta:
        model = Log
        fields = '__all__'


class LogFilter(django_filters.FilterSet):
    class Meta:
        model = Log
        fields = {'created_at': ['gte', ], }


class LogViewSet(viewsets.ModelViewSet):
    queryset = Log.objects.all().order_by("created_at")

    serializer_class = LogSerializer
    filter_class = LogFilter

    authentication_classes = (SessionAuthentication, BasicAuthentication)
    permission_classes = (IsAuthenticated,)


class MainView(LoginRequiredMixin, TemplateView):
    template_name = 'index.html'

各クラスの役割を簡単に説明します。

・Serializer(ModelSerializer)

入出力の定義。ModelSerializerでは指定したモデルでの出力、入力チェックを暗黙的に定義する。

・FilterSet

django-filterの設定クラス。検索キーと検索条件を指定する。この設定をすると以下のURLで「指定した時間以降に作られたデータ」という条件の検索ができる。

http://<endpoint>/?<fieldname>__gte=YYYY-MM-DD%20HH:mm:ss

・ViewSet(ModelViewSet)

WebAPIの定義本体。
ModelViewSetでは自身かSerializerで指定したモデルに対応するWebAPIが6種類同時に有効になる。

役割 エンドポイント HTTPメソッド
リソースの取得(複数) リソース名/ GET
リソースの作成 リソース名/ POST
リソースの取得(個別) リソース名/id/ GET
リソースの更新(全部) リソース名/id/ PUT
リソースの更新(一部) リソース名/id/ PATCH
リソースの削除 リソース名/id/ DELETE

・TemplateView

ブラウザで表示する画面の定義。LoginRequiredMixinを継承することでアクセス時に認証をかけている。

Serializer等の詳細な情報については以下のリンクを参照してください。

参考:[Django REST Framework] Serializer の 使い方 をまとめてみた

参考:Django REST Frameworkを使って爆速でAPIを実装する

参考:Django REST framework カスタマイズ方法 - チュートリアルの補足

参考:Django REST framework で django-filter を使う

手順7.ルーティング設定編集

作成したViewセットに対してルーティングの設定を行います。
project/urls.pyに以下のコードを入力してください。

project/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework import routers

from api.views import LogViewSet
from api.views import MainView

router = routers.DefaultRouter()
router.register(r'logs', LogViewSet)

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include(router.urls)),
    path('', MainView.as_view()),
]

# ログイン画面のタイトルを変更
admin.site.site_header = '気温と湿度の変化'

これでルートディレクトリでindex.htmlを公開、/api/logs/をエンドポイントとしてWebAPIが公開されます。

★ Browserble API でAPIの動作確認

Django REST Frameworkでは、WepAPIの動作をWeb画面から確認するためのツールが付属しています。runserver でアプリケーションを起動し、http://localhost:8000/api/logsにアクセスしてください。

image.png

Debug設定が有効の時にかぎりますが、このように公開されているAPIをWeb画面で動作確認することができます。Google Chrome拡張のPOSTMANと同等の機能です。現在公開されている機能がわかりやすいというメリットがあります。

Django-Filterが有効になっているので、画面上部に「フィルタ」ボタンが表示されています。

image.png

検索クエリも画面上から試すことができます。

image.png

手順8.テンプレート作成

テンプレートファイルtemplates/index.htmlにデータ閲覧用のアプリケーションを記述します。今回テンプレートの機能は殆ど利用していませんが、専用タグを埋め込みことでログインユーザー等の情報を表示させることができます。

templates/index.html
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>気温・湿度の変化</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
    <a class="navbar-brand" href="#">気温・湿度の変化</a>
    <button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#Navber" aria-controls="Navber"
            aria-expanded="false"
            aria-label="ナビゲーションの切替">
        <span class="navbar-toggler-icon"></span>
    </button>

    <div class="collapse navbar-collapse" id="Navber">
        <ul class="navbar-nav mr-auto">
            <li class="nav-item">
                <a class="nav-link" href="{% url 'admin:index' %}">管理サイト</a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="{% url 'admin:logout' %}">ログアウト</a>
            </li>
        </ul>
    </div>
    <!-- /.navbar-collapse -->
</nav>
<div class="container">
    <div class="container">
        <canvas id="myChart"></canvas>
    </div>
    <div class="row mt-3">
        <div class="col-12">
            <form>
                <fieldset class="form-group">
                    <legend class="text-center">表示期間</legend>
                    <div class="text-center">
                        <div class="form-check form-check-inline">
                            <label class="form-check-label">
                                <input type="radio" class="form-check-input" name="days" id="term_4"
                                       value="7">
                                1週間分
                            </label>
                        </div>
                        <div class="form-check form-check-inline">
                            <label class="form-check-label">
                                <input type="radio" class="form-check-input" name="days" id="term_3"
                                       value="3">
                                3日分
                            </label>
                        </div>
                        <div class="form-check form-check-inline">
                            <label class="form-check-label">
                                <input type="radio" class="form-check-input" name="days" id="term_1"
                                       value="1"
                                       checked>
                                1日分
                            </label>
                        </div>
                    </div>
                </fieldset>
            </form>
        </div>
    </div>
</div>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/locale/ja.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.0/underscore-min.js" charset="utf-8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.min.js" charset="utf-8"></script>
<script>

    var chart;

    // グラフ描画
    function drawChart(data) {
        var ctx = document.getElementById("myChart").getContext('2d');

        if (chart) {
            chart.destroy();
        }

        chart = new Chart(ctx, {
            type: 'line',
            data: {

                labels: data[0],
                datasets: [{
                    label: '気温',
                    data: data[1],
                    yAxisID: "y-axis-1",
                    fill: false,
                    backgroundColor: 'rgb(255, 99, 132)',
                    borderColor: 'rgb(255, 99, 132)',
                }, {
                    label: '湿度',
                    data: data[2],
                    yAxisID: "y-axis-2",
                    fill: false,
                    backgroundColor: "rgb(54, 162, 235)",
                    borderColor: "rgb(54, 162, 235)",
                },]
            },
            options: {
                scales: {
                    yAxes: [
                        {
                            // 気温軸(左)
                            id: "y-axis-1",
                            type: "linear",
                            position: "left",
                            ticks: {
                                max: 50,
                                min: -30,
                                stepSize: 10
                            },
                        }, {
                            // 湿度軸(右)
                            id: "y-axis-2",
                            type: "linear",
                            position: "right",
                            ticks: {
                                max: 100,
                                min: 0,
                                stepSize: 25
                            },
                        }],
                    xAxes: [{
                        type: 'time',
                        time: {
                            displayFormats: {
                                hour: 'D日H時'
                            }
                        }
                    }]
                },
                tooltips: {
                    callbacks: {
                        title: function (tooltipItem, data) {
                            return moment(tooltipItem[0].xLabel).format('YYYY-MM-DD HH:mm')
                        }
                    }
                }
            }
        })
    }

    // データ読込
    function reload() {

        // 指定時間を取得
        var days = $('input[name=days]:checked').val();
        // 1日、3日、7日前の時間を設定
        var param = moment().subtract(days, 'days').format('YYYY-MM-DD HH:mm:ss')

        $.getJSON('/api/logs?created_at__gte=' + param)
            .done(function (source) {
                let data = new Array(3);
                data[0] = _.pluck(source, 'created_at');
                data[1] = _.pluck(source, 'temperature');
                data[2] = _.pluck(source, 'humidity');
                drawChart(data);
            })
    };

    $(document).ready(function () {
        // ラジオボタンのイベントリスナー
        $('input[name="days"]:radio').on('change', function () {
            reload();
        });
        // 初期化
        reload();
    });

</script>
</body>
</html>

Webアプリケーションを起動してhttp://localhost:8000にアクセスしてください。グラフが表示されれば成功です。表示されているデータはJQueryのAjax処理にてWebAPIより取得されています。以下、利用したライブラリの参考リンク。

グラフ表示 Chart.js
参考:chart.js入門

配列処理 underscore.js
参考:遅すぎたUnderscore.js入門 - 全体像

時間処理 moment.js
参考:Moment.jsを使う

ファイルを更新したらmanage.py runserverで起動して、ブラウザでhttp://localhost:8000にアクセスしましょう。問題なければグラフが表示されます。

image.png

手順9.クライアント作成

データの参照の方が完了したので、登録の方を試してみたいと思います。
pythonで以下のサンプルコードを作りました。

IDとPasswordをcreatesuperuserで登録したものに変えて実行してください。
画面をリロードしてデータが追加されていれば成功です。

IDとパスワードは管理アプリケーションから追加したユーザーのものでもOKです。herokuなどで利用している場合はurlを変更してください。

data_post.py
import base64
import json
import urllib.request

# 変数は必要に応じて外部化してください。
url = "http://localhost:8000/api/logs/"
method = "POST"
headers = {"Content-Type": "application/json", }

user = "user"
password = "password"

temperature = 20
humidity = 80

# PythonオブジェクトをJSONに変換する
obj = {"temperature": temperature, "humidity": humidity, }
json_data = json.dumps(obj).encode("utf-8")

credentials = ('%s:%s' % (user, password))
encoded_credentials = base64.b64encode(credentials.encode('ascii'))

# httpリクエストを準備してPOST
request = urllib.request.Request(url, data=json_data, method=method, headers=headers)
request.add_header('Authorization', 'Basic %s' % encoded_credentials.decode("ascii"))

with urllib.request.urlopen(request) as response:
    response_body = response.read().decode("utf-8")

curlコマンドではこんな感じです。

curl -v -u user:password -H "Accept: application/json" -H "Content-type: application/json" -X POST -d "{"temperature":30, "humidity":54}" http://localhost:8000/api/logs/

※WindowsではJson内のダブルクォーテーションをエスケープする必要がある。

curl -v -u user:password -H "Accept: application/json" -H "Content-type: application/json" -X POST -d "{\"temperature\":30, \"humidity\":54}" http://localhost:8000/api/logs/

一応Rasbery Pi で動かすことを想定して pythonで作成してみましたが、Basic認証が使えるならどんなツールでも大丈夫のはずです。せっかく作ってみたので私もソニーのMESH 温度湿度タグでしばらく運用してみます。これは別記事としてまとめたいと思います。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.