クラウドの開発・公開はAzureのWebappsを、無償である事に感謝しつつ、常時接続ではない事やメモリ等の制限(1G)などからレスポンスは悪く、デバッグにはストレスが募っていました。そこでお盆休みを利用してGoogle App Engineにトライしました。
※本番デプロイができなかったのでDemoはありません。Azure版はこちら。
##構築手順
更新が早い為か、公式サイトでも自分でgoogleコマンド等いくつもインストールしなければならないように書かれているものもあったりしますが、以下の手順で簡単に設定できました。
1.Googleの検索サイトの右上のログインからGOOGLEアカウントにログイン。
2.Google Cloud Platformにアクセス。
3.無料トライアルをクリックし、同意事項に同意して進む。
4.クレジット情報などお客様情報を入力すればようこそが表示。
5.シェルを選択。
##ここまでで以下の環境ができあがります。
1.My First Projectというプロジェクトが初期生成されます。python2.7、openjdk1.8.0_222、node10.14.2(他にGOも)がライブラリのパスが切ってあり動作します。
2.pythonのライブラリを確認すると全部で93フォルダ、主なものとして以下がインストールされていました。
google-apiコマンド各種
google-cloudコマンド各種
bigquery
Keras-Applications
numpy
tensorflow
Flask,Django,sklearn,matlib,pandas,Pillowはありません。他のライブラリを追加する為には仮想環境を作る必要があるようです。仮想環境にしないままpip installしても追加されませんでした。これらのライブラリは他のAIプラットフォームなどを動かす際に使うのでしょう。
##Python開発環境の構築
1.コンソール画面の右上部の>_マークをクリックして、コンソールを立ち上げ、コンソール右の鉛筆マークをクリックするとエディタが立ち上がります。xxx_folderのところは、Linuxのホームディレクトリ「/home/ユーザー名」になります。
2.仮想環境を作成します。コンソール画面から「virtualenv <任意のフォルダ名>」。バージョン無指定だと、最新3.7がインストールされます(2019/08現在)。Googleクラウドのコマンドは2.7でしか動きませんので、利用する場合は「--python=/usr/bin/python2.7」を付けてバージョン指定します。仮想環境を作った段階では、ライブラリはpip・setuptools・wheelだけが設定されます。
3.「cd <任意のフォルダ名>」で変更して、「source bin/activate」で仮想環境を起動します。(終了は「deactivate」。)
4.必要なライブラリをインストールします。(今回のサンプルを動かすには以下の通り。)
pip install Django
pip install sklearn
pandas
matplotlib
numpy
dtreeplt
pip install Pillow
5.ソースを配置します。
6.「python manage.py runserver 8080 」で起動します。
7.コンソール右上の👁マークをクリックしてプレビューします。
※デバッグ後に本番デプロイしようとしましたが、「too many files」エラーでアップできませんでした。
##ソース
scikit learnから解釈過程を表示するWebアプリを作ってみたのDjango版のソースを掲載します。サンプルを動かすところのファイルのみです。管理画面も省略してます。
1.フォルダ構造
wwwrootDJ
├─common.py ライブラリの読み込み等の共通ルーチン
├─manage.py (自動生成ソース略)
├─Data
│ └─_dultCensusIncome_model.sav nootbookで訓練したsavファイル
│
├─mysite
│ ├─settings.py
│ ├─urls.py
│ ├─wsgi.py (自動生成ソース略)
│ └─init.py (自動生成ソース略)
├─ml
│ ├─Income.py メインのプログラム
│ └─static
│ └─result dtreepltの画像が保管されるフォルダ
│
└─templates
├─base.html 共通のhtml
└─ml
└─Income.html
2.Income.html
<!--*****************************************************************
* azureMLサンプル:米国国勢調査提供の収入のサンプルの訓練結果にrestで接続
*
* 2019/03/24
*****************************************************************-->
{% extends "base.html" %}
{% block body %}
<form name='sendform' action="" style="display: inline" method="post">
<!-- form name='sendform' action="/Income/" style="display: inline" method="post"-->
<div class="card" >
<div class="card-header" style="height:50px; font-size:1.5rem; ">
<b>scikit-learn webアプリサンプル</b>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-2 mb-1">年齢</div> <!--col-md-1:mdはPCのMIDDLEサイズ。mb-1はマージン(空白)をボトム(下)に設定-->
<div class="col-md-2 mb-1" >
<select class="form-control input-lg mb-1" id="age">
<option>20</option>
<option>25</option>
<option>30</option>
<option>35</option>
<option>40</option>
<option>45</option>
<option>50</option>
<option>55</option>
<option>60</option>
<option>65</option>
<option>70</option>
<option>75</option>
<option>80</option>
</select>
</div>
<div class="col-md-2 mb-1">ワーククラス</div>
<div class="col-md-2 mb-1" >
<select class="form-control input-lg mb-1" id="workclass">
<option value="Federal-gov">Federal-gov</option>
<option value="Local-gov">Local-gov</option>
<option value="Never-worked">Never-worked</option>
<option value="Private">Private</option>
<option value="Self-emp-inc">Self-emp-inc</option>
<option value="Self-emp-not-inc">Self-emp-not-inc</option>
<option value="State-gov">State-gov</option>
<option value="Without-pay">Without-pay</option>
<option value="Without-pay">Without-pay</option>
</select>
</div>
<div class="col-md-1 mb-1">学校</div>
<div class="col-md-3 mb-1" >
<select class="form-control input-lg mb-1" id="education-num">
<option value="1">Preschool</option>
<option value="2">1st-4th</option>
<option value="3">5th-6th</option>
<option value="4">7th-8th</option>
<option value="5">9th</option>
<option value="6">10th</option>
<option value="7">11th</option>
<option value="8">12th</option>
<option value="9">HS-grad</option>
<option value="10">Some-college</option>
<option value="11">Assoc-voc</option>
<option value="12">Assoc-acdm</option>
<option value="13">Bachelors</option>
<option value="14">Masters</option>
<option value="15">Prof-school</option>
<option value="16">Doctorate</option>
</select>
</div>
</div>
<div class="row">
<div class="col-md-1 mb-1">職業</div>
<div class="col-md-3 mb-1" >
<select class="form-control input-lg mb-1" id="occupation">
<option value="Adm-clerical">Adm-clerical</option>
<option value="Armed-Forces">Armed-Forces</option>
<option value="Craft-repair">Craft-repair</option>
<option value="Exec-managerial">Exec-managerial</option>
<option value="Farming-fishing">Farming-fishing</option>
<option value="Handlers-cleaners">Handlers-cleaners</option>
<option value="Machine-op-inspct">Machine-op-inspct</option>
<option value="Other-service">Other-service</option>
<option value="Priv-house-serv">Priv-house-serv</option>
<option value="Prof-specialty">Prof-specialty</option>
<option value="Protective-serv">Protective-serv</option>
<option value="Sales">Sales</option>
<option value="Tech-support">Tech-support</option>
<option value="Transport-moving">Transport-moving</option>
</select>
</div>
<div class="col-md-2 mb-1">人種</div>
<div class="col-md-2 mb-1" >
<select class="form-control input-lg mb-1" id="race">
<option value="Amer-Indian-Eskimo">Amer-Indian-Eskimo</option>
<option value="Asian-Pac-Islander">Asian-Pac-Islander</option>
<option value="Black">Black</option>
<option value="Other">Other</option>
<option value="White">White</option>
</select>
</div>
<div class="col-md-2 mb-1">性別</div>
<div class="col-md-2 mb-1" >
<select class="form-control input-lg mb-1" id="sex">
<option value="Male">Male</option>
<option value="Female">Female</option>
</select>
</div>
</div>
<div class="row">
<div class="col-md-2 mb-1">キャピタルゲイン</div>
<div class="col-md-2 mb-1" >
<input type="text" class="form-control input-lg" id="capital-gain" value=0 >
</div>
<div class="col-md-2 mb-1">キャピタルロス</div>
<div class="col-md-2 mb-1" >
<input type="text" class="form-control input-lg" id="capital-loss" value=0 >
</div>
<div class="col-md-2 mb-1">労働時間</div>
<div class="col-md-2 mb-1" >
<input type="text" class="form-control input-lg" id="hours-per-week" value=40 >
</div>
</div>
<div class="row">
<div class="col-md-1 mb-1">国</div>
<div class="col-md-3 mb-1" >
<select class="form-control input-lg mb-1" id="native-country">
<option value="Cambodia">Cambodia</option>
<option value="Canada">Canada</option>
<option value="China">China</option>
<option value="Columbia">Columbia</option>
<option value="Cuba">Cuba</option>
<option value="Dominican-Republic">Dominican-Republic</option>
<option value="Ecuador">Ecuador</option>
<option value="El-Salvador">El-Salvador</option>
<option value="England">England</option>
<option value="France">France</option>
<option value="Germany">Germany</option>
<option value="Japan">Japan</option>
<option value="Laos">Laos</option>
<option value="Mexico">Mexico</option>
<option value="United-States">United-States</option>
<option value="Vietnam">Vietnam</option>
<option value="Yugoslavia">Yugoslavia</option>
</select>
</div>
<div class="col-md-2 mb-1">アルゴリズム</div>
<div class="col-md-4 mb-1" >
<select class="form-control input-lg mb-1" id="alg">
<option value="1">DecisionTree_model</option>
<option value="2">RandomForest_model</option>
</select>
</div>
</div>
<div class="row">
<div class="col-md-2 mb-1" >
<button class="btn btn-default" id="button1" style="width: 100px; padding: 5px;">Ajax Call</button>
</div>
<div class="col-md-3 mb-1" >
<button class="btn btn-default" id="button2" style="width: 100px; padding: 5px;">Reload</button>
</div>
</div>
<div class="col-md-5 mb-1" >
<h2>status→{{ status }}</h2>
<textarea rows="40" cols="320">{{text}}</textarea><br>
</div>
</div> <!--class="card-body"の終端-->
</div> <!--class="card"の終端-->
<div class="modal-body" style="width: 100%; overflow-x: auto;">
<!-- モーダルは bodyからスクロールを削除して、代わりにモーダルコンテンツをスクロールする。-->
{% load static %} <!-- Instruct Django to load static files -->
<img src="{% static file_name %}" width='2400' hight='2400' >
</div>
<input type="hidden" name="json_data" value="">
{% csrf_token %}
</form>
<script type="text/javascript">
//*************************************************
//* form送信
//*************************************************
$("#button2").click(function() {
document.forms.sendform.json_data.value = JSON.stringify({
"age":$("#age").val(),
"workclass":$("#workclass").val(),
"education-num":$("#education-num").val(),
"occupation":$("#occupation").val(),
"race":$("#race").val(),
"sex":$("#sex").val(),
"capital-gain":$("#capital-gain").val(),
"capital-loss":$("#capital-loss").val(),
"hours-per-week":$("#hours-per-week").val(),
"native-country":$("#native-country").val(),
"alg":$("#alg").val()
});
document.forms.submit();
});
</script>
{% endblock %}
3.Income.py
# -*- coding: utf-8 -*-
from common import *
# Adult Census Income Binary Classification
# 読み込むモデルをグローバル宣言
fn_RandomForest = './Data/AdultCensusIncome_model.sav'
# incomeデータの前処理
def IncomeConvert(df):
try:
df['workclass'] = df['workclass'].map( {
'?': 99,
'Federal-gov':0,
'Local-gov':1,
'Never-worked':2,
'Private':3,
'Self-emp-inc':4,
'Self-emp-not-inc':5,
'State-gov':6,
'Without-pay':7,
} ).astype(int)
except:
log('erro1')
try:
df['occupation'] = df['occupation'].map( {
'?': 99,
'Adm-clerical':0,
'Armed-Forces':1,
'Craft-repair':2,
'Exec-managerial':3,
'Farming-fishing':4,
'Handlers-cleaners':5,
'Machine-op-inspct':6,
'Other-service':7,
'Priv-house-serv':8,
'Prof-specialty':9,
'Protective-serv':10,
'Sales':11,
'Tech-support':12,
'Transport-moving':13,
} ).astype(int)
except:
log('erro2')
try:
df['race'] = df['race'].map( {
'White':0,
'Amer-Indian-Eskimo':1,
'Asian-Pac-Islander':2,
'Black':4,
'Other':5,
} ).astype(int)
except:
log('erro3')
try:
df['sex'] = df['sex'].map( {
'Male': 0,
'Female': 1,
'Other': 2
} ).astype(int)
except:
log('erro4')
try:
df['native-country'] = df['native-country'].map( {
'?': 99,
'Cambodia':0,
'Canada':1,
'China':2,
'Columbia':3,
'Cuba':4,
'Dominican-Republic':5,
'Ecuador':6,
'El-Salvador':7,
'England':8,
'France':9,
'Germany':10,
'Greece':11,
'Guatemala':12,
'Haiti':13,
'Holand-Netherlands':14,
'Honduras':15,
'Hong':16,
'Hungary':17,
'India':18,
'Iran':19,
'Ireland':20,
'Italy':21,
'Jamaica':22,
'Japan':23,
'Laos':24,
'Mexico':25,
'Nicaragua':26,
'Outlying-US(Guam-USVI-etc)':27,
'Peru':28,
'Philippines':29,
'Poland':30,
'Portugal':31,
'Puerto-Rico':32,
'Scotland':33,
'South':34,
'Taiwan':35,
'Thailand':36,
'Trinadad&Tobago':37,
'United-States':38,
'Vietnam':39,
'Yugoslavia':40,
} ).astype(int)
except:
log('erro5')
try:
df['income'] = df['income'].map( {
'<=50K': 0,
'>50K': 1,
} ).astype(int)
except:
print('erro6')
return df
#**************************************
# TREEの各情報取得 https://scikit-learn.org/stable/auto_examples/tree/plot_unveil_tree_structure.html
#**************************************
def decisionTreeStructure(model,train_x,feature_names,target_names):
text = ""
text = text + '【InputData】\n' + str(train_x.loc[0]) + '\n'
#入力の予測
result = model.predict(train_x)
text = text + "【Result】 = " + target_names[result[0]] + "\n"
#入力の到達するIDを求める
leave_id = model.apply(train_x)
leave_id = leave_id[0] #到達するID
text = text + "【Leave_id】 = " + str(leave_id) + "\n"
#入力の到達する過程がスパース行列で得られる
decision_path = model.decision_path(train_x)
node_indicator = str(decision_path)
text = text + "【Decision Process】 \n" + node_indicator + "\n"
try:
#tree_のプロパティを得る
n_nodes = model.tree_.node_count
children_left = model.tree_.children_left #左の子ノードID(子が無い場合は「-1」がセットされている)
children_right = model.tree_.children_right #右の子ノードID
feature = model.tree_.feature #特徴量のインデックス
threshold = model.tree_.threshold #閾値
#各ノードの最多数クラス https://own-search-and-study.xyz/2016/12/25/scikit-learn%e3%81%a7%e5%ad%a6%e7%bf%92%e3%81%97%e3%81%9f%e6%b1%ba%e5%ae%9a%e6%9c%a8%e6%a7%8b%e9%80%a0%e3%81%ae%e5%8f%96%e5%be%97%e6%96%b9%e6%b3%95%e3%81%be%e3%81%a8%e3%82%81/
nodeClasses = np.argmax(model.tree_.value.T, axis=0)
#################################################
# 木構造をたどり各ノードの深さや葉であるかなどのプロパティを計算し木構造を表示する
#################################################
node_depth = np.zeros(shape=n_nodes, dtype=np.int64)
is_leaves = np.zeros(shape=n_nodes, dtype=bool)
stack = [(0, -1)] # seed is the root node id and its parent depth
#葉かどうか識別
while len(stack) > 0:
node_id, parent_depth = stack.pop()
node_depth[node_id] = parent_depth + 1
#子のノードが左右同じ(-1)だったら葉としてis_leavesにTrueをセット
if (children_left[node_id] != children_right[node_id]):
stack.append((children_left[node_id], parent_depth + 1))
stack.append((children_right[node_id], parent_depth + 1))
else:
is_leaves[node_id] = True
text = text + "【Binary tree structure has】= " + str(n_nodes) + " \n【Decision Tree Structure】\n"
#表示
for i in range(n_nodes):
if is_leaves[i]:
text = text + (node_depth[i] * "\t") + "|-leaf id=" + str(i) + " class= 【"+ target_names[nodeClasses[0][i]] + "】"
if i == leave_id:
text = text + "<---------------------------------[[[[*result*]]]"
text = text +"\n"
else:
text = text + (node_depth[i] * "\t") + "|-node id=" + str(i)
text = text + " if [" + feature_names[feature[i]] + "] <= [" + str(threshold[i])+ "] then node "
text = text + str(children_left[i]) + " else to node " + str(children_right[i]) + "\n"
#################################################
#識別過程を表示する
#################################################
text = text + "【Decision Process】\n"
node_index = decision_path.indices[decision_path.indptr[0]:
decision_path.indptr[1]]
for node_id in node_index:
if leave_id == node_id:
continue
w_train_x = str(train_x.loc[0][feature[node_id]]) #型がint・float・str混在して下記のif判定でエラーになるので強引に文字型にしている。
w_threshold = str(threshold[node_id])
#if (train_x.loc[0][feature[node_id]] <= threshold[node_id]):
if (w_train_x <= w_threshold):
threshold_sign = "<="
next_node = str(children_left[node_id])
else:
threshold_sign = ">"
next_node = str(children_right[node_id])
text = text + "decision id= " + str(node_id) + "【"
text = text + str(feature_names[feature[node_id]]) + "】"
text = text + w_train_x + " "
text = text + threshold_sign + " "
text = text + w_threshold
text = text + " goto " + next_node + " class= 【"+ target_names[nodeClasses[0][i]] +"】\n"
return text
except:
return "!!decisionTreeStructureError!!"
#**************************************
# Adult Census Income Binary Classification dataset
#**************************************
#html表示
def Income(request):
result = {}
try:
dict_request = json.loads(request.POST['json_data'])
df = pd.io.json.json_normalize(dict_request)
df = df.drop(['alg'],axis=1)
train_x = IncomeConvert(df)
except:
result['status'] = 'No Input(Input Error)'
return render(request, 'ml/Income.html',result)
# 保存したモデルをロードする
try:
if dict_request['alg'] == "1":
filename = fn_DecisionTree
else:
filename = fn_RandomForest
model = pickle.load(open(filename, mode='rb'))
except:
result['status'] = 'model Error'
return render(request, 'ml/Income.html',result)
#incomeの特徴名・ターゲット名
#feature_names=['age','workclass','education-num',
# 'occupation','race','sex','capital-gain',
# 'capital-loss','hours-per-week','native-country']
feature_names = df.columns.values #df形式でも大丈夫
target_names=['<=50k', '>50k']
#Understanding the decision tree structure
text = decisionTreeStructure(model,train_x,feature_names,target_names)
# dtreeplt視覚化
save_dir = 'ml/static/result/'
save_file = 'Income-treeML.png'
static_dir = 'result/'
try:
dtree = dtreeplt(
model=model,
feature_names=feature_names ,
target_names=target_names,
)
fig = dtree.view()
fig.savefig( save_dir + save_file)
except:
result['status'] = 'dtreeplt Error'
result['text'] = text
result['file_name'] = static_dir + save_file
return render(request, 'ml/Income.html',result)
result['status'] = 'Ok'
result['text'] = text
result['file_name'] = static_dir + save_file
return render(request, 'ml/Income.html',result)
# ajax送信スクリプト
def IncomeCalc(request):
try:
dict_request = json.loads(request.body) #jQueryのajaxデータはrequest.bodyに入っている。https://e-tec-memo.herokuapp.com/article/12/
df = pd.io.json.json_normalize(dict_request)
if dict_request['alg'] == "1":
filename = fn_DecisionTree
else:
filename = fn_RandomForest
df = df.drop(['alg'],axis=1)
train_x = IncomeConvert(df)
except:
result = "ajaxSendError"
return HttpResponse(result)
# 保存したモデルをロードする
try:
model = pickle.load(open(filename, mode='rb'))
result = model.predict(train_x)
if result[0] == 0:
result = '<=50k'
else:
result = '>50k'
leave_id = model.apply(train_x)
node_indicator = model.decision_path(train_x)
result = '\n predict = ' + result + '\n leave_id = ' + str(leave_id[0])+ '\n node_indicator = \n' + str(node_indicator[0])
except:
result = 'calc error'
return HttpResponse(result)
return HttpResponse(result)
# 学習とモデルの保存
def IncomeClassLearn(request):
result = {}
try:
df = pd.read_csv('./Data/Data_Adult.csv')
df = IncomeConvert(df)
df = df.drop(['fnlwgt','education','marital-status','relationship'],axis=1)
train_x = df.drop('income', axis=1)
train_y = df.income
(train_x, test_x ,train_y, test_y) = train_test_split(train_x, train_y, test_size = 0.1, random_state = 666)
# 決定木
from sklearn.tree import DecisionTreeClassifier
model = DecisionTreeClassifier(max_depth=5) # ←depthを指定
model = model.fit(train_x, train_y)
pred = model.predict(test_x)
log('pred=' + str(pred))
# モデルの保存
filename = './Data/AdultCensusIncome_model(WEBAPPS-depth-5).sav'
pickle.dump(model, open(filename, 'wb'))
except:
result['status'] = 'Error'
result['text'] = 'IncomeClassLearn Error'
return render(request, 'msg.html',result)
result['status'] = 'OK'
result['text'] = 'IncomeClassLearn Model create & Save[AdultCensusIncome_model(WEBAPPS-depth-5).sav]OK'
return render(request, 'msg.html',result)
def IncomeClass(request):
result = {}
try:
df = pd.read_csv('./Data/Input_Adult.csv')
df = IncomeConvert(df)
df = df.drop(['fnlwgt','education','marital-status','relationship'],axis=1)
train_x = df.drop('income', axis=1)
text = 'Input-------- \n' + str(train_x) + '\n'
except:
result['status'] = 'Error'
result['text'] = 'CSV読込・変換エラー'
return render(request, 'msg.html',result)
# 保存したモデル(グローバル宣言してある)をロードする
filename = fn_DecisionTree
try:
model = pickle.load(open(filename, 'rb'))
pred_result = model.predict(train_x)
text = text + 'predict-------- \n' + str(pred_result) + '\n'
except:
result['status'] = 'Error'
result['text'] = 'ロード・予測エラー'
return render(request, 'msg.html',result)
result['status'] = 'OK'
result['text'] = text
return render(request, 'msg.html',result)
4.その他
【settings.py】
from common import *
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SECRET_KEY = 'am$+th5ttev9oa$77w9428jl*2bd-&fpvcy(dkigg99beg(kjh'
DEBUG = True
# 許可するサーバは自分のサーバ名に修正
ALLOWED_HOSTS = ['127.0.0.1']
#ALLOWED_HOSTS = ['dja-vin.azurewebsites.net']
#ALLOWED_HOSTS = ['dja-mako.azurewebsites.net']
# 作成したアプリを追加
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'blog',
'ml',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
# 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# ルートとして使用するコントローラ。デフォルトで生成されたものをそのまま利用。
ROOT_URLCONF = 'mysite.urls'
# CSSなどの静的ファイルの置き場所の設定
STATIC_ROOT = os.path.join(BASE_DIR, "static") #collectstatic コマンドを使って集めたいパス
STATIC_URL = '/static/'
# テンプレートの置き場所の設定
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, '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',
'django.template.context_processors.static',
],
},
},
]
WSGI_APPLICATION = 'mysite.wsgi.application'
# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'
USE_I18N = True
USE_L10N = True
USE_TZ = True
DEBUG = True
-----------------------------------------------------------
【urls.py】
from django.contrib import admin
from django.urls import path
from django.conf.urls.static import static
from ml import Income
urlpatterns = [
path('', Income.Income),
]
web.config、base.html、common.pyはこちらをご覧ください。
##GAEとAzure Webapps(無償F1)の比較
比較内容 | GAE | Azure Webapps | コメント |
---|---|---|---|
環境 | LinuxDebianベース。ディスク5G。メモリ2G。xeon2.2G1コア。本番環境のWebサーバはおそらくnginx。 | WindowsとLinux。ディスク1G。メモリ1G。CPUは不明、CPUは共有されている。WebサーバはWindows環境ではIIS。 | GAEの方が広くて速い |
開発環境と本番環境 | 開発環境と本番環境にわかれている。本番移行はコンテナイメージを作成し、イメージをAppEngineフレキシブル環境にデプロイするとの事。開発環境で動作したものをアップできるなど確実な運用が可能。 | 区別は無くWebAppsをふたつ立てるなどの工夫が必要 | |
デバッグのしやすさ | Pythonのエラーはそのまま吐き出され、print文もコンソールに吐き出すのでデバッグはローカルPCと同等に可能。 | IISからPythonプログラムが起動されるので、エラーもprint文も戻らない。デバッグはやや難しい。 | |
FTPによるソースの操作 | 不可。 | 可能。これによりWebappsのテストサーバのソースをフォルダごと本番サーバに移行できる等、便利。 | |
本番デプロイができなかった為、現時点のGAEは、Webappsでうまくプログラムが動かない際の、ローカルPCのPython検証環境に代わるものという位置づけとなりました。
処理はローカルPC・Azureより早いので引き続き、本番環境へのデプロイは試していきたいと思います。