【資格】情報処理安全確保支援士試験(セキスペ)に合格しました。
タイトルのとおりです。セキスペを取りました。登録セキスペにも申請済みで、来月から正式に登録セキスペとなる見込みです。
タイトルのとおりです。セキスペを取りました。登録セキスペにも申請済みで、来月から正式に登録セキスペとなる見込みです。
ご無沙汰しております。IMAXおじさんです。
11.28〜12.1まで開催されていたAWS re:Invent 2023に参加してきました。これに行く前にAWSの資格を一つくらい取得しておきたいと思い立ち、AWS Solutions Architect - Associateを受験しました。 結果は合格(760/1000)でした。次はDeveloper Associateの取得を目指します。
お久しぶりです。IMAXおじさんです。
今日はこのサイトをさくらVPSからAWSに移行した話をします。現在はさくらVPSに戻しております。
元々はさくらVPSで小さめのサーバを借りてnginx + django + mysqlの構成でサイトを構成していました。詳細はこちらの記事に掲載しています。
この構成でやりたいことはだいたいできており、不満は全くなかったのですがお仕事でAWSを使い始めたこともあり、こちらに移行することにしました。
個人サイトなのであまりお金がかかるのも困りものです。そこで、下の要求を満たすように構成を考えました。
まだ全てを満たす構成にはできていませんが、現在は下のような形でデプロイをしています。
合格発表から時間が空いてしまいました。2023年4月にネットワークスペシャリスト試験を受験し、合格していました。
点数は午前2:8割、午後1:6割、午後2:8割といった具合で、落ちるなら午前1かなぁと思っていたところなんとか耐えていました。
次はセキスペとAWS SAAを受けようと思います。
今回は無料で使えるホスティングサービスであるHerokuにLINE Webhookをデプロイするまでの手順を備忘録的に記します。
実家でデジタル難民と化している祖母が他の家族からのLINEメッセージを見れるように、↓のようなシステムを作ろうと思い立ちました。
このシステムを作るにあたり、LINEメッセージを送ると自作のAPIにデータをPOSTするようなWebhookをつくりたいです。 今回はゴールとして、まずはLINE bot(Python)に送ったメッセージをそのままLINEで返信してくるようなエコーボットをデプロイすることにします。
herokuの利用にはアカウント作成が必要です。今回は無料枠の範疇のことしかしないので、Freeアカウントで作成します。 こちらからアカウントを作成することができます。
また、heroku CLIを使うためインストールが必要です。こちらの手順に従い、インストールしてください。
今回のwebhook作成に関しては、LINEbotが必要となります。LINEbotの作成にはLINE developerアカウントが必要となるため、アカウント登録をおこないます。 こちらからアカウント作成をしてください。
今回のwebhookを作成するにあたり、ローカル上で下記のようなフォルダ構成を作成します。
*/
|__main.py # webhookの処理を記述する
|__requirements.txt # 使用するライブラリの一覧
|__runtime.txt # herokuで使用する言語の諸元
|__Procfile # herokuで実行するコマンド
|__.env # herokuで使用する環境変数
main.py
main.py
にはline-bot-sdk
を使って行う処理を記述することになります。
詳細は3
節で説明するため、ここでは説明を省きます。
requirements.txt
requirements.txt
には今回使用するライブラリを記述します。これをプロジェクトフォルダに含めることで、デプロイ時に自動でライブラリをインストールしてくれます。
Flask==2.0.3
line-bot-sdk==2.2.1
requests==2.27.1
runtime.txt
runtime.txt
にはherokuで使用するPythonのバージョンを記述します。その際、herokuが対応しているバージョンを選択しなければならないため、現在のherokuが何に対応しているかを確認するようにします。
今回は以下の内容を記述します。一行のみです。
python-3.9.0
Procfile
Procfile
はheroku上でデプロイ時に実行するコマンドを記述します。今回はmain.py
を実行することでサーバーを立ち上げたいので、下記の内容とします。
web: python3 main.py
.env
.env
ファイルはherokuの環境に登録したい環境変数を記述します。main.py
などに直接記載したくない情報(例えばline-bot-sdkのシークレットキーなど)を記述し、herokuの環境に反映します。
line-bot-sdk
について今回は開発者公式が公開しているline-bot-sdk
のサンプルプログラムを使用します。サイトはこちら(GitHub)から見れます。
元のサンプルプログラムはアクセストークンとシークレットキーをベタ書きしているため、この二つだけは環境変数から読み込むように変更を加えています。
from flask import Flask, request, abort
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage, TextSendMessage
import os
# Flaskでwebhookを立ち上げ
app = Flask(__name__)
# アクセストークンとシークレットキーは環境変数から読み込む
YOUR_CHANNEL_ACCESS_TOKEN = os.environ["YOUR_CHANNEL_ACCESS_TOKEN"]
YOUR_CHANNEL_SECRET = os.environ["YOUR_CHANNEL_SECRET"]
line_bot_api = LineBotApi(YOUR_CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(YOUR_CHANNEL_SECRET)
@app.route("/callback", methods=['POST'])
def callback():
signature = request.headers['X-Line-Signature']
body = request.get_data(as_text=True)
app.logger.info("Request body: " + body)
try:
handler.handle(body, signature)
except InvalidSignatureError:
print("invalid signatrue. please check channel info")
abort(400)
return 'OK'
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
line_bot_api.reply_message(event.reply_token, TextSendMessage(text=event.message.text))
if __name__ == "__main__":
port = int(os.getenv("PORT"))
app.run(host="0.0.0.0", port=port)
ここまでのプログラムの準備ができたところで、herokuへのデプロイを行います。大まかな手順は下記の通りです。
まずは、herokuの個人コンソールから「create new app」を選択し、新しいアプリケーションを作成します。
名前を選ぶように促されるので、適当な名前をつけます。「hogehoge.herokuapp.com」というURLを与えられますが、世界で一意な名前である必要があるため、適度に長い名前にしてください。
ローカルのプロジェクトをgitリポジトリとして登録し、herokuにpushします。
ローカルで
git init
git add *
git commit -m "first commit"
とし、まずはgitのローカルリポジトリを作成します。
次に、herokuのリモートリポを紐付けます。heroku CLIから以下を実行します。
heroku git:remote -a {自分のアプリの名前}
このままgit pushすると、環境変数を設定しないままビルドが始まってしまうので、git pushする前に.env
ファイルで設定した環境変数をherokuに反映します。
heroku CLIを用いて、.env
ファイル内に記述した環境変数をherokuのアプリに反映します。
プロジェクトディレクトリ内でheroku config:push
を実施します。成功すると「Successfully wrote settings to Heroku!」とログが出ます。
venv) hogehoge:~/Desktop/line-webhook-app$ heroku config:push
Successfully wrote settings to Heroku!
ここまで実施できたら、git push heroku master
でheroku上のリモートリポジトリにプロジェクトをpushします。
pushが成功すると自動でビルド→デプロイが行われます。
LINE botにメッセージを送って、全く同じ内容の返事が来れば成功です。
プロジェクト内でheroku logs --tail
とすると、デプロイしたアプリのログを確認することができます。
うまくLINE botが返事を返してくれなければ、このログを見てトラブルシュートを行います。
これまでVPSをメインで使っていたので、herokuのようなPaaSを使えば無料で簡単にデプロイできるんだなと感心しました。
タイトルの通り、ソートアルゴリズムを真面目に実装します。 言語はPython、実装するソートアルゴリズムはクイックソート・ヒープソート・マージソートです。
ソートは基本的に標準ライブラリのsort
関数に投げがちなので、内部がどうなっているかを気にすることがないかと思います。
今回は応用情報技術者試験の対策も兼ねて、真面目にソートを実装します。
なお、めんどくさいので昇順ソートしか実装しません()
クイックソートは、分割統治の考え方でソートを行うアルゴリズムです。
下記のような配列を考えたとき、その配列に対してpivot
と呼ばれる軸を設定し、そのpivot
未満の配列とpivot
以上の配列に分割していきます。
これを再帰的に繰り返すことで、整列済みの配列を得るアルゴリズムです。また、安定なソートではありません。
平均計算量は、O(NlogN)
となります。
具体的な計算量の導出はこちらが参考になりました。
ざっくり言えば、適切にpivot
を選んだ場合上記の画像で示す通りpivot
による分割と結合はそれぞれ回数H = logN
で抑えることができます。
それぞれの高さに関して、N
回比較が発生するので、計算量はO(NlogN)
であるとわかります。
ただし、予めソートされたデータに対して最大値/最小値をpivot
として取り続けた場合は最悪計算量がO(N^2)
となります。
A = list(map(int, input().split()))
l = 0
r = len(A) - 1
def quick_sort(tgt):
tgt_len = len(tgt)
if len(tgt) <= 1:
return tgt
pivot = tgt[0]
left = []
right = []
for i in range(1, tgt_len):
if tgt[i] <= pivot:
left.append(tgt[i])
else:
right.append(tgt[i])
return quick_sort(left) + [pivot] + quick_sort(right)
res = quick_sort(A)
print(res)
ヒープソートはヒープの再構成がO(logN)
でできることを利用したソートアルゴリズムです。
ヒープの根が常に配列の最小値となっているので、ヒープの根をpop
→ヒープを再構築...を繰り返し、pop
した順に配列を作ればO(NlogN)
でソートできます。
ヒープに突っ込むため、安定なソートではないですが、常に計算量がO(NlogN)
となります。
ヒープを自力で実装し、ヒープからどんどんpopしていく方針を取ります。
class Heap:
def __init__(self, arr:list):
self.arr = []
for item in arr:
self.push(item)
def push(self, x):
self.arr.append(x)
child = len(self.arr) - 1
while child > 0:
parent = (child - 1)//2
if self.arr[parent] < x:
break
else:
self.arr[child] = self.arr[parent]
child = parent
self.arr[child] = x
def root(self):
return self.arr[0] if self.arr else None
def pop(self):
if not self.arr:
return None
root = self.arr[0]
self.arr[0] = self.arr[-1]
self.arr.pop()
parent = 0
smallest_index = parent
heap_size = len(self.arr)
while 2*parent+1 < heap_size:
l = 2*parent+1 # 左の子
r = l+1 # 右の子
# 自分、左の子、右の子の中で、最小のものを選び取る
if (r < heap_size) and (self.arr[parent] >= self.arr[r]):
smallest_index = r
if (l < heap_size) and (self.arr[smallest_index] > self.arr[l]):
smallest_index = l
# 自分が最小になった時点で、ヒープ化されたのでbreak
if smallest_index == parent:
break
# 親と子を入れ替える
self.arr[parent], self.arr[smallest_index] = self.arr[smallest_index], self.arr[parent]
parent = smallest_index
return root
def heap_sort(arr):
heap = Heap(arr)
N = len(arr)
print(heap.arr)
result = []
for _ in range(N):
result.append(heap.pop())
print(heap.arr)
return result
if __name__ == "__main__":
sample = [1, 1, 2, -1, 100, 92]
result = heap_sort(sample)
print(result) # -> [-1, 1, 1, 2, 92, 100]
マージソートも、分割統治法の考え方を用いてソートを行うアルゴリズムです。 下記画像のように、まず要素が一つになるまで分割を繰り返します。
その後、分割した配列を2つずつマージしていきます。 このとき、分割した配列を結合する際に、昇順になるように入れていきます。各配列はソート済みであるので、それぞれ先頭のindexから順に比較していけば良いことになります。
こちらの説明がとてもわかりやすいです。
上の図で示した通り、分割とマージのプロセスはそれぞれH = logN
で抑えられます。
マージの際には、それぞれの分割のレイヤーでN
回比較が走るので、結果的に計算量はO(2NlogN) = O(NlogN)
となります。
再帰的な実装を行います。(実は分割のところがO(N)かかっているのではないかという説があります)
def merge_sort(data):
if len(data) <= 1: return data
mid = len(data) // 2
# 再帰的に分割していく
left = merge_sort(data[:mid:])
right = merge_sort(data[mid::])
return merge(left, right)
def merge(left, right):
result = []
i, j = 0, 0
# 配列の先頭が最小値になっているので、毎回比べながら追加していく
while (i < len(left)) and (j < len(right)):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
# add the remainging part
if i < len(left):
result.extend(left[i:])
if j < len(right):
result.extend(right[j:])
return result
if __name__ == "__main__":
array = list(map(int, input().split()))
res = merge_sort(array)
print(res)
簡単な自作競プロジャッジサーバーを作った話をします。(競プロとは?:こちらを参照) デプロイしてから結構経つのに、詳細な構成に関して何も記事を書いていなかったので、ここらで文章に落とそうと思います。 サーバは一時期まで公開していましたが、Dockerで実行環境を隔離しているとはいえ任意のコードが実行できてしまうのは怖いので最近は公開していません。
ジャッジサーバ内のコンポーネント間の構成とデータフローを示します。
回答提出から正誤判定までのジャッジサーバ内での処理の流れ
コードを提出してから、ジャッジが完了するまでにある程度時間がかかります。 実行に時間がかかる場合やテストケースが多い場合は、POSTしてからページが表示されるまで何も表示されない状態になってしまいます。 また、実行時間があまりに長い場合はブラウザにリクエストタイムアウトと判定されてしまいま す。 そのため、今回は下記のようなフローで非同期処理を実装することにしました。
バックエンドではジャッジがcelery+redisで実装されたFIFOキューを保持し、提出されたコードに順次正誤判定を行なっています。 ジャッジ未完了の状態ではブラウザ更新を促し、ジャッジ完了の状態では結果を通知します。
競技プログラミングにおいては、コードの実行時間
が非常に重要です。多くの場合、2秒程度の実行時間内に問題を解けるコードを書く必要があります。
制限時間内に解けなかったコードにはTLE
(Time Limit Exceeded)判定が与えられ、不正解扱いとなります。
最初は簡単なジャッジサーバができれば良い程度に考えていたので、コードの正解・不正解しか判定しないロジックを書いていました。
ところが、段々と完成度を追求したくなってくるもので、やはりTLE
の判定がしたくなってきます。
色々実行時間を計測する術を検討しました。例えば、下記のようにコード実行前後で時刻を計測し、その差分を測るようなやり方です。
start = time.time()
ret = subprocess.run(cmd, shell=True, ...) #提出コードを実行
end = time.time()
timeTaken = end - start # 実行時間を計測
しかし、このやり方ではwhile True: pass
のような無限ループが発生した場合にTLE判定を出せなくなってしまいます。
一定時間以上実行している場合には途中で実行を打ち切るような動作が必要になるわけです。
最終的にはコードを実行しているsubprocess
モジュールで実行時間を計測し、オーバーした場合にTLE
判定を出すようにしました。
subprocess
モジュールには、実行時間を制限するオプションtimeout
があるため、ここに実行制限時間の秒数を渡せば上記の問題に対応しつつTLE
を判定できます。
# subprocessモジュールによるコードの実行
try:
ret = subprocess.run(
cmd, timeout=2, shell=True, stdin=fin, # timeoutに制限時間を指定
stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
except (CalledProcessError, TimeoutExpired) as e:
if TimeoutExpired:
tle += 1 # 実行時間が超過した場合はTLE判定を出す
continue
elif CalledProcessError:
...
最近更新しておりませんでした、IMAXおじさんです。 この頃本サイトに色々機能を足していたので、そろそろ全体をまとめた構成図を作ろうと思い立ちました。
本サイトはさくらVPS上にデプロイしています。 当初はUbuntu Server上のネイティブで動かしていたのですが、お仕事でDockerをよく使うこともあり途中からDockerでのデプロイに切り替えました。
各コンポーネントの基本的な構成は以下となります。
また、コンテナ間の関係とデータフローは以下の通りです。
また、Djangoでは下記のアプリを作成して公開しています。
ログ監視
今後は以下のことに取り組みたいな〜と考えています。
応用情報に合格しました。午前午後ともに8割程度取れていました。次はNWスペシャリスト頑張ります。
本日基本情報を受けてきました. CBTに移行してからはスコアレポートが受験後すぐに見れるようになっているのですが,午前・午後どちらも6割を超えており合格していると思います.
(2022/07/18追記) 合格証書がお家に送付されて来ました。合格のようです。
仕事が思ったより忙しくあまり勉強の時間が取れませんでしたが,受かってよかったです.
次は応用情報取得を目指して勉強していこうと思います.
Djangoでマークダウン形式のエディタを爆速で実装します.参考にしたサイトは以下です.
Markdown形式のエディタをDjangoに爆速で実装できるライブラリです.ここ 実際に動作しているところはリンク先をご覧ください.結構有能なのでびっくりします.
まず,pipで必要なライブラリを入れましょう
pip install django-mdeditor
pip install Markdown
mdeditor自体の設定をしていきます.settings.pyに以下を追記します.
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'mdeditor' #これを追記
]
X_FRAME_OPTIONS = 'SAMEORIGIN'
MDEDITOR_CONFIGS = {
'default': {
'language': 'en',
}
}
次に,models.pyを設定していきましょう.app内で定義するModelを以下のように定義します. 普段使っているmodels.TextFieldが Markdown形式のオブジェクトになります
#models.py
from django.db import models
from mdeditor.fields import MDTextField
# Create your models here.
class Article(models.Model):
title = models.TextField(max_length=200)
content = MDTextField() #ここがMarkdown形式のTextFieldになる
def __str__(self):
return self.title
また,{project_name}/urls.pyに以下を追記します.
urlpatterns = [
path('admin/', admin.site.urls),
path('mdeditor/', include('mdeditor.urls')) #これを追記
]
admin.pyへの追記を忘れずに!
# admin.py
from django.contrib import admin
from .models import Article
# Register your models here.
admin.site.register(Article)
現時点でDjango Adminの管理画面からMarkdown形式で編集できるようになっているはずです.
次に,MarkdownをHTMLに変換していい感じに表示されるようにしていきましょう. 先ほど定義したArticleモデルに次を追記していきます.
#models.py
from django.db import models
from mdeditor.fields import MDTextField
import markdown #ここを追記
# Create your models here.
class Article(models.Model):
title = models.TextField(max_length=200)
content = MDTextField() #ここがMarkdown形式のTextFieldになる
def __str__(self):
return self.title
# 以下を追記
def markdown_to_html(self):
md = markdown.Markdown(
extensions=['extra', 'admonition', 'sane_lists', 'toc']
)
html = md.convert(self.content)
return html
このメソッドをHTML内のDjangoテンプレートから呼び出していきます. 以下は関連する部分のHTMLコードです.
<!doctype html>
{% extends 'base.html' %}
{% load static %}
{% block content %}
{% for item in object_list %}
<article class="blog-post">
<h4 class="blog-post-title">{{ item.title }}</h4>
{{ item.markdown_to_html|safe }}
</article>
{% endfor %}
{% endblock %}
これだけだとコード整形のみが為されてお洒落なシンタックスハイライトがつきません. いい感じの見栄えにするために以下をHTML内に追記します.
<head>
...
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/a11y-dark.min.css">
...
</head>
<body>
...
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
</body>
これで完了です.試しにサーバーを立ち上げて確かめてみてください.
python3 manage.py runserver
技術系の記事を書き始めることにしました. 検索機能,月別アーカイブ,マークダウン,シンタックスハイライト等を実装したのでなんかいい感じに見れるようになると思います. 試しにPython形式でかいたコードの見栄えを確認してみましょう.
def func(param):
return param**2
いい感じですね.
お久しぶりです。IMAXおじさんです。 今日は今までのWEB系の話とは少し変わって、PyModbusでModbus/TCPのサーバーを構築する話をします。
HMSのサイト M-system技研のModbus/TCP通信仕様書
Modbus/TCPとは、Modicon社(現Schneider Electric社)が1979年に策定した産業用Ethernetプロトコルです。 主にPLCやリモートIOとの通信に使用される産業用Ethernetプロトコルであり、世界的なシェアとしては5%程度を占めています(2020年現在)。
サーバー(フィールド側, PLCやリモートIO)
xxx.yyy.zzz.100
|
|
クライアント(PC)
xxx.yyy.zzz.101
Modbus/TCPはサーバー/クライアント方式の通信プロトコルです。 PLCやリモートIOなどがスレーブ(==サーバー)、データ収集を行うPCなどがマスター(==クライアント)としてデータのやり取りを行います。 名前の通り、各サーバーとクライアントがTCPでペイロードをやり取りします。 物理レイヤーの仕様(端子形状など)は指定されておらず、あくまで通信のレイヤーのみの仕様となります。
ペイロードの仕様に関しては、下記のM-system技研発行の通信仕様書がよくまとまっており参考になります。
PyModbusとは上記のModbusサーバー・クライアントを擬似的にPythonで構築できるライブラリです。 対向機をわざわざ購入するとなると、数万円かかってしまいますが、PyModbusを使えば無料で手軽に対向機を用意できます。
さて、Modbus/TCPの説明が終わったところで早速デモの構築に移りましょう。 PC上にPyModbusで構築したサーバーまでデータを取りに行くデモを構築します。
PyModbusのサーバーを早速構築します。ローカルホストにサーバーを立てることにするため、特にIPアドレスは気にしなくてもよいです。 実際にデバイス間通信を試したい人は、Ethernetポートを備えたデバイスを2個用意して固定IPを振ってあげるとよいかと思います。
今回はinput_register
に値を入れることにします。
サーバーがレジスタ(input_register
)に保持する値は、以下の通りとしましょう。
----------------------------
register1 | int16
----------------------------
register2 | float
register3 | (32bit)
----------------------------
Modbusでは、1レジスタ=2byte(16bit)で扱われることに注意してください。なので、32bitのfloatなど16bitを超えるようなデータはレジスタをまたがって保持されることになります。
さて、このような値を返してくれるサーバーをPyModbusで構築していきます。まずは必要ライブラリのimportを。
# sample_server.py
from pymodbus.constants import Endian
from pymodbus.version import version
from pymodbus.server.asynchronous import StartTcpServer
from pymodbus.datastore import ModbusSequentialDataBlock
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext
from pymodbus.payload import BinaryPayloadBuilder
サーバー側でアクセス・送受信しているパケットの内容を確認するため、ログの設定をします。
import logging
FORMAT = ('%(asctime)-15s %(threadName)-15s'
' %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s')
logging.basicConfig(format=FORMAT)
log = logging.getLogger()
log.setLevel(logging.DEBUG)
次に、レジスタに入れる値を作っていきます。
PyModbusではpymodbus.BinaryPayloadBuilder
を使ってペイロードのオブジェクトを作ることになります。
builder = BinaryPayloadBuilder(byteorder=Endian.Big) #ペイロードビルダーのインスタンスを作成。バイトオーダーは再現したいリモートIOの仕様に準ずること。
# 最初のint_16のペイロードを作成
builder.add_16bit_int(100)
# 2つ目のfloatのペイロードを作成
builder.add_32bit_float(123.45)
# ペイロードをModbusSequentialDataBlockへ変換
block = ModbusSequentialDataBlock(1, builder.to_registers())
# レジスタへの登録を行う
store = ModbusSlaveContext(ir=block, zero_mode=False)
context = ModbusServerContext(slaves=store, single=True)
最後に、サーバーを起動しておしまいです。
StartTcpServer(context, address=("0.0.0.0, 502")) # ローカルホストで起動
サーバーの構築はできたので、サーバーにためている情報を取りに行くクライアントを作ります。
Modbusではクライアントからのみ通信を始めることができます。
クライアントから打てるリクエストにはいくつか種類(ファンクションコード)があります。
詳しくはModbus/TCPの通信仕様書を見ていただきたいのですが、今回はファンクションコード4(FC4)を使います。
FC4はread_input_registers
というリクエストになっており、その名の通りinput_registers
の値を読み取るものになります。
(余談ですが、Modbus/TCPではread_input_registers
で読まれるレジスタアドレスは300001
から始まると決まっているようです。FC4なのに開始アドレスが3xxxなのは気持ちが悪い...)
サーバーの設定では、レジスタ3つにまたがってデータが溜まっているのでread_input_registers
で3つ分レジスタを読んであげましょう。
from pymodbus.client.sync import ModbusTcpClient
from pymodbus.constants import Endian
from pymodbus.payload import BinaryPayloadDecoder
# Modbusクライアントのインスタンス。ローカルホストにサーバーが立っているので、アクセス先をlocalhostと指定。
client = ModbusTcpClient("localhost", port=502)
# FC4のread_input_registersでレジスタアドレス300001+0 ~ 300001+2を読み取る。
# 第一引数が開始アドレス(相対)、第二引数がレジスタ数
res = client.read_input_registers(0, 3, unit=1)
# 読み取ったレジスタ値をデコーダインスタンスへ渡す
# サーバー側がビッグエンディアン設定なので、こちらも合わせる。
decoder = BinaryPayloadDecoder.fromRegisters(res.registers, byteorder=Endian.Big)
# 最初のint_16bitをデコード
int16 = decoder.decode_16bit_int()
# 次のfloat_32bitをデコード
float32 = decoder.decode_32bit_float()
print(int16) # -> 100
print(float32) # -> 123.45...
これで、サーバーのinput_registers
に溜まったデータをModbus/TCPで読み取ることができました。
産業用ネットワーク難しい
nginx + uwsgi + django on dockerでサイトを構築し,localhost上で公開するまでの解説です.
djangoにはpython3 manage.py runserver
で起動できる便利な開発サーバー機能が備わっています.
しかし,本番環境ではこの機能は使わずに,apacheやnginxなどのwebサーバーを使用することが推奨されています.
今回は,nginxを使用してdjangoアプリケーションデプロイすることにします.
また,環境の移植性を考慮して,dockerを使用したデプロイを行います.
nginx + uwsgi + djangoの具体的な処理の流れはこちらで解説されていますので,ぜひ参照してください. 非常に分かりやすく書かれており,これを読むだけである程度の設定はできてしまいます.
Setting up Django and your web server with uWSGI and nginx
簡単に図解すると,下の図のようにリクエストが流れていくことになります.
webクライアント -> nginx(80,443ポート) -> uwsgi(8000ポート) -> djangoバックエンド
この構成を実現するため,以下ではnginx, uwsgi, djangoの各項目の設定事項を見ていきます.
docker-compose.yml
の中身まず,全体の見通しをよくするために,docker-compose.yml
の中身を紹介しておきます.
version: "3"
services:
nginx:
image: nginx:latest
volumes:
- ./nginx/conf:/etc/nginx/conf.d
- ./nginx/uwsgi_params:/etc/nginx/uwsgi_params
- ./django-project/myapp/static:/static
- ./django-project/myapp/media:/media
ports:
- 8000:80
environment:
- LANG=C.UTF-8
- LANGUAGE=en_US
depends_on:
- django-project
- mysql
django-project:
build: ./django-project
restart: always
command: bash /init.sh
tty: true
volumes:
- ./django-project/app:/myapp
- ./django-project/init.sh:/init.sh
environment:
- DEBUG=False
- LOCALHOST=localhost
- DEPLOYHOST=#デプロイ先のドメイン名
depends_on:
- mysql
- redis-server
mysql:
# mysqlに関する記述
....
project/
|__django-project/
| |__myapp/
| | |__myapp/
| | |__static/
| | |__media/
| | |__manage.py
| |__Dockerfile
|__nginx
| |__conf/nginx.conf
| |__uwsgi_params
|__mysql
|__docker-compose.yml
まずnginxの設定を行います.
ホストOS側で./nginx/conf/nginx.conf
を作成し,に以下の項目を書き足していきます.
upstream django {
ip_hash;
server django-project:8000;
}
server {
listen 80;
charset utf-8;
client_max_body_size 75M;
location /static {
alias /static;
}
location /media {
alias /media;
}
location / {
uwsgi_pass django;
include /etc/nginx/uwsgi_params;
}
}
server_tokens off;
上記のポイントは,以下です.
listen 80
で80番ポートをリッスンlocation /static, location /media
で静的ファイルの場所を指定.docker-compose.yml
内でマウントした/static, /media
内のファイルを探します.location /
でuwsgiをリクエストを流す先として指定します.uwsgi_params
はここからコピペで問題ありません.uwsgiはdjangoと同じdockerイメージの中に入れます../django-project/Dockerfile
内に以下の内容を書き加えます.
FROM python:3.8-slim-buster
# djangoなど必要なライブラリをインストール
COPY ./requirements.txt /requirements.txt
# RUN source /venv/bin/activate
RUN python3.8 -m pip install --upgrade pip
RUN python3.8 -m pip install -r requirements.txt
RUN python3.8 -m pip install uwsgi # uwsgiを入れること
これでdjangoをuwsgiで動かす準備はできました. 次にuwsgiでリクエストを待ち受けるために,コンテナ起動時に実行するシェルスクリプトを書いていきます.
#!/bin/bash
cd /myapp;
bash -c "python3 manage.py makemigrations";
bash -c "python3 manage.py migrate";
bash -c "python3 manage.py collectstatic --noinput"
bash -c "uwsgi --socket :8000 --module myapp.wsgi --py-autoreload 1s";
uwsgi(8000番ポート)で待ち受けることを最後の行で書き加えています.
最後に,djangoの設定を行なっていきます.
変更が必要なのは,/myapp/myapp/settings.py
のみです.
docker-compose.yml
のenvironment
でDEBUG=True/False
と指定することでデバッグモードが変えられるように,環境変数の読み込み部分を追記します.
また,djangoはDEBUG=False
の際には外部からの通信を受け付けないので,ALLOWED_HOSTS
にも環境変数の読み込みを追加します.
DEPLOYHOST
の部分にデプロイするサーバーのホスト名を書きます.# settings.py
# SECURITY WARNING: don't run with debug turned on in production!
is_debug_mode = os.environ.get("DEBUG")
DEBUG = (is_debug_mode == "True")
ALLOWED_HOSTS = [os.environ.get("LOCALHOST"), os.environ.get("DEPLOYHOST")]
STATIC_ROOT
とMEDIA_ROOT
を指定することで,DEBUG=False
の際に静的ファイルはnginxが直接サーブするようにできます.# settings.py
STATIC_URL = '/static/'
STATICFILE_DIRS = [
os.path.join(BASE_DIR, 'userctrl/static/'),
os.path.join(BASE_DIR, 'contest_manage/static/'),
os.path.join(BASE_DIR, 'ace-builds')
]
STATIC_ROOT = os.path.join(BASE_DIR, "static")
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = "/media/"
alpine linuxイメージをプロキシ環境下で使用する場合、様々な障壁に阻まれてコケることがあります。 以下では、その回避策を列挙していきます。(問題の切り分けができていないので一部おかしいことを言っているかもしれません)
Dockerfile内でプロキシを登録します。
ここでRUN export http_proxy={プロキシ:ポート}
とすると、なぜか環境変数に反映されておらずハマりました。
解決策としては
ENV http_proxy=http://{プロキシのIPアドレス}:{ポート}/
ENV https_proxy=http://{プロキシのIPアドレス}:{ポート}/
とすることで環境変数に反映することができました。
プロキシを経由する関係でapkレポジトリからライブラリを引っ張ってこれない場合があります。
これは、デフォルトでapkがインデックスを探しに行くレポジトリURLがhttps://~
となっている場合に起こります。
下記のissueにはdl-cdn.alpinelinux.org does not support TLS at all. (中略) APK repositories should be http onlyとあります。
そのため、/etc/apk/repositories
にプレーンなhttpのURLの記述とする必要があります。
すなわち、
RUN rm /etc/apk/repositories
RUN echo "http://cd-cdn.alpinelinux.org/alpine/v3.13/main" >> /etc/apk/repositories
RUN echo "http://cd-cdn.alpinelinux.org/alpine/v3.13/community" >> /etc/apk/repositories
とし、httpでapkのレポを探しに行きます。
MySQLイメージを使う場合は、{既存のDBファイルを含むローカルフォルダ}:/var/lib/mysqlとしてバインドするとハマります。 新しい名前付きボリュームor空ディレクトリをバインドすることで、このエラーを回避できます。
Pythonイメージ内でdjangoプロジェクトをビルドし、djangoからMySQLをDBとして使用したいです。 その際、MySQLのデータを永続化させるために既にDB関連のデータを含んでいるローカルのフォルダ(下記のmysql/data_db)をマウントしようとしてハマりました。
はじめに試したディレクトリ構造は以下のようになっています。
my-docker-project/
|__django-project/
| |__src/...
| |__Dockerfile
|
|__mysql/
| |__data_db/...
| |__init_db/...
|
|__docker-compose.yml
また、docker-compose.yml
は下記の通りです。
version: "3"
services:
mysql-db:
image: amd64/mysql:5.7
environment:
- MYSQL_DATABASE=python_db
- MYSQL_USER={ユーザー名}
- MYSQL_PASSWORD={パスワード}
- MYSQL_ROOT_PASSWORD=password
volumes:
- ./mysql/data_db:/var/lib/mysql
- ./mysql/init_db:/docker-entrypoint-initdb.d
privileged: true
ports:
- 3306:3306
django-project:
build: ./django-project
restart: always
command: bash -c "python3 /mysite/local_manage.py makemigrations && python3 /mysite/local_manage.py migrate && python3 /mysite/local_manage.py runserver 0.0.0.0:8000"
ports:
- 8000:8000
volumes:
- ./django-project/mysite/history:/mysite/history
- ./django-project/mysite/media:/mysite/media
depends_on:
- mysql-db
以上の情報をもとに、django側のsettings.py
を以下の通りに設定しました。
# settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'python_db',
'USER': 'root',
'HOST': 'mysql-db',
'PASSWORD': 'password',
'PORT': '3306'
}
}
表記の通りの環境構築をDocker上で行いdocker compose up --build
すると、mysqlにアクセスできず以下のエラーに見舞われます。
django.db.utils.OperationalError: (1045, "Access denied for user 'root'@'192.168.32.3' (using password: YES)")
調べて見ると以下の記事に行き当たりました。 stackoverflowの記事
上記の記事によると、 Do note that none of the variables below will have any effect if you start the container with a data directory that already contains a database: any pre-existing database will always be left untouched on container startup. とのことでした。すなわち、一度docker buildなどでDBファイルシステムが構成されてしまったディレクトリをバインドしようとすると、DBコンテナの初期化に失敗するらしいです。
docker-compose.yml
内で/var/lib/mysqlに紐付けるフォルダを、docker compose up --build
する前に空にしておくことでうまくいきます。すなわち、
$ rm -r ./mysql/data_db
でdata_dbを削除$ mkdir ./mysql/data_db
で再度ディレクトリ作成$ docker compose up --build
でビルドという手順を踏むことになります。これはうまくいくことを確認しました。
タイトルの通り,Django + Ace + DockerでオンラインPython実行環境を実装します. 一回で収まらないかもしれないので「その1」としています.
Django==3.0
django_ace==1.0.11
Python==3.6.8
python:3.7-slim-buster
$ python3 -m venv venv
$ source venv/bin/activate
$ pip install Django==3.0
$ pip install django-ace==1.0.11
$ django-admin startproject project
$ cd project
$ django-admin startapp editor
$ mkdir history #実行するスクリプトの保存先
この時点で,ディレクトリは以下のような構成になっているはずです.
project/
├ editor/
├ project/
├ history/
└ manage.py
エディタ部分を実装するために,フォームビューを実装します. CDN等で提供されるAceを埋め込んで利用しても良いですが,ここではdjango-aceを使用します.
#forms.py
from django import forms
from django_ace import AceWidget
class EditorForm(forms.Form):
code = forms.CharField(
widget = AceWidget(
mode="python",
theme="twilight",
width="100%"
)
)
EditorForm
を作成したら,Viewを追加していきます.
下記のコードでは,index.html
をテンプレートとして,HTML中のformから実行コードを取得します.
取得したコードは,history
フォルダ内に保存され,dockerコンテナ内で実行されます.
start_docker(code)
: dockerコンテナに実行するコードを投げ,実行するコード.Home(views.FormVIew)
: html内のフォームから取得したコードを,start_docker
に投げます.# views.py
from .forms import EditorForm
FILE_DIR = os.path.join(settings.BASE_DIR, 'history')
DOCKER_CMD = 'docker run -i --rm --name tmp_container -v {}:/usr/src/myapp -w /usr/src/myapp python:3.7 python {}'
def start_docker(code):
# dockerコンテナ内でコードを実行する
file_name = '{}.py'.format(datetime.datetime.now().isoformat())
file_path = os.path.join(FILE_DIR, file_name)
with open(file_path, 'w', encoding='utf-8') as file:
file.write(code)
cmd = DOCKER_CMD.format(FILE_DIR, file_name)
ret = subprocess.run(
cmd, timeout=15, shell=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
return ret.stdout.decode()
class Home(generic.FormView):
template_name = 'index.html'
form_class = EditorForm
sucess_url = reverse_lazy('home')
def form_valid(self, form):
# 送信ボタンで呼ばれる
code = form.cleaned_data['code']
output = start_docker(code)
context = self.get_context_data(form=form, output=output)
return self.render_to_response(context)
<!--index.htmlの一部のみ抜粋-->
<head>
....
{{ form.media}}
</head>
<body>
....
<div class="col-12">
<form action="" method="POST">{% csrf_token %}
{{ form.code }}
<button class="btn btn-primary" type="submit">Run</button>
</form>
</div>
<div class="col-12">
<label for="exampleFormControlTextarea1" class="form-label">実行結果</label>
<textarea class="form-control" id="exampleFormControlTextarea1" rows="3" disabled>{{ output }}</textarea>
</div>
...
</body>
以上をいい感じにまとめると,下記のようなコード編集画面が現れます.(あとで追記)