Django + MySQLをDockerで動かしたい ~tips編~

DockerでDjango + MySQLの構成で開発をしたときに困ったあれこれをメモとして残していきます。

環境構築はたくさん記事があると思うので、環境構築の手順はそちらに託すことにします。

DBにデータが登録できない(文字コードが違う)

adminからデータ登録をしようとした時に遭遇したエラーです。

エラー文と問題詳細

(1267, "Illegal mix of collations (latin1_swedish_ci,IMPLICIT) and (utf8_general_ci,COERCIBLE) for operation '='")

ざっくり言うと、文字コードが違うのでデータ登録ができないと怒られています。
MySQLに入って

mysql> SHOW VARIABLES WHERE Variable_name LIKE 'character\_set\_%' OR  Variable_name LIKE 'collation%';

を実行すると、character_set_*latin1collation_*latin1_sweden_ci が入ってると思います。
シングルバイトの文字コードが指定されているところに、漢字などマルチバイトのデータを登録しようとしてエラーが出ています。

解決方法

クライアント側(Django)とサーバ側(MySQL)とでそれぞれ設定が必要です。

クライアント側(Django)の設定

app/settings.py のDBの設定欄にOPTIONSで文字コードを指定します。

# Database
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'django',
        'USER': 'root',
        'PASSWORD': '',
        'HOST': 'db',
        'PORT': 3306,
        'OPTIONS': {
            'charset': 'utf8mb4'
        }
    }
}
サーバ側の設定

docker runの時にオプションを指定する方法もあるのですが、今回はdocker-compose.yamlcommandとして指定します。

  db:
    image: mysql:5.7
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    container_name: mysql
    volumes:
      - ./database/data:/var/lib/mysql
    ports:
      - 3333:3306
    environment:
      MYSQL_DATABASE: 'django'
      MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
    platform:
      linux/amd64

これでOKです!
あとはdockerを立ち上げ直せばうまくいくはずです!


Models.pyでFunctionKeyを使っているとうまく変更できない場合があります。
その時は、FunctionKeyを使っているモデル(テーブル)をコメントアウトして、

root@123456:/django# python manage.py makemigrations
root@123456:/django# python manage.py migrate

をして設定変更を適用した後にコメントアウトを解除してまたmigrateするとうまくいきます。

マスタデータ・テストデータを共有したい(seedしたい)

adminからデータ登録もできるのですが、一括登録したい場合面倒です。
テストデータの共有ができればチーム開発も楽になりますね。

TLDR

app/fixures配下にテストデータ用のjsonを作って、次のコマンドでポストします。

python manage.py loaddata app/fixtures/test_data.json

データが入っていることは、mysqlを起動してSELECT文を実行するか、Djangoのadmin画面から確認できます。

Models.pyにテーブル定義

app/models.pyに下記のテーブルを定義したとします。

from django.db import models

class User(models.Model):
    GENDER = (
        (1, 'male'),
        (2, 'female')
    )
    family_name = models.CharField(max_length=50)
    first_name = models.CharField(max_length=50)
    gender = models.IntegerField(choices=GENDER)
    birthday = models.DateField()

    def __str__(self):
        return f'{self.id}: {self.family_name} {self.first_name}'

test_data.jsonを作成

テストデータ投入用のJSONファイルを作成します。

[
    {
        "model": "app.user",
        "pk": 1,
        "fields": {
            "familiy_name" : "田中",
            "first_name": "太郎",
            "gender": 1,
            "birthday": 1995-01-01
        }
    },
    {
        "model": "app.user",
        "pk": 2,
        "fields": {
            "familiy_name" : "後藤",
            "first_name": "花子",
            "gender": 2,
            "birthday": 1975-10-26
        }
    }
]

データ投入

次のコマンドでポストします。

python manage.py loaddata app/fixtures/test_data.json

データが入っていることは、mysqlを起動してSELECT文を実行するか、Djangoのadmin画面から確認できます。


参考

【GAS】複数シート使用時のgetActiveCell()の挙動について

Google Apps Script(GAS)で最終変更時刻の自動記入機能を作成している際に、getActiveCell()の挙動が思うようにいかなかったので、解決時のメモ。

Twitterで何人か同様にハマっている方がいらっしゃったので、何かの役に立てれば幸いです。

getActiveCell()を使用した最終時刻更新

コード

function insertLastModified() {
  /**
   * @param - None
   * @retuen - None
   */
  
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var active_sheet = ss.getActiveSheet();
  var sheet4update = ss.getSheets()[2];  // 今回はシート3を対象とする

  if (active_sheet.getName() == sheet4update.getName()){
    var column_modified = findColumn(sheet4update, '最終更新時刻');

    /// 更新したセルを取得
    var active_cell = active_sheet.getActiveCell();

    /// 削除時は動作しないように。
    if (active_cell.getValue() != []){
      /// 更新した行を取得
      var active_row = active_cell.getRow();

      /// 更新時刻を記入
      sheet4update.getRange(active_row, column_modified+1).setValue(new Date());
    }
  }
}

function findColumn(sheet, keyword) {
  /**
   * @param {string} sheet - 列番号を取得したい項目のあるシート
   * @param {string} keyword - 取得したい項目名
   * @return {double?} target_column - keywordの列のインデックス
   */
  /// 最終列を取得
  var last_column = sheet.getLastColumn();
  var range = sheet.getRange(1, 1, 1, last_column);

  /// ヘッダー行をすべて(最終列まで)取得
  var headers = range.getValues();

  /// 特定の文字列のある列がどこかを検索して、列のインデックスを返す
  /// シートの列番号として処理したい場合は +1 することに注意。
  var target_column = headers[0].indexOf(keyword);

  return target_column;
}

ポイント

getActiveCell()を使う際のポイントは、シートの取得の際もgetActive〇〇()を使用する必要があるという点にある。

しかし、公式ドキュメントにはこれに言及されていないため、シートを明示的に指定するようなコードを書くと、正しいセルが取得できないという問題が生じる。

ちなみに、getActiveCell()よりも、getCurrentCell()が推奨されている。

ダメだったときのコードとその際の挙動

コード

var ss = SpreadsheetApp.openById('<YOUR_SPREADSHEET_ID>');
var sheet = ss.getSheets()[2];

var column_modified = findColumn(sheet, '最終更新時刻');

/// 更新したセルを取得
var active_cell = active_sheet.getActiveCell();
console.log(active_cell);

/// 更新した行を取得
var active_row = active_cell.getRow();
console.log(active_row);

/// 更新時刻を記入
sheet.getRange(active_row, column_modified+1).setValue(new Date());

挙動

{}
(1,1)

つまり、A1セルを示している。そもそもgetActiveCell()が動作しておらず、空の組にそれぞれ1を加えた値が返ってきて結果的に(1, 1)になっている、という挙動が確認される。

おそらく、上手くいってない方はおおかた上記のようなコードになっていると思うので、初めに示したgetActiveSheet()でシート取得を行うことを試してみてください!

【OpenCV】連続静止画から動画を作成【Python】

OpenCVを使って、連続静止画から簡単に動画を生成するコードを作成します

使用するデータについて

今回は、静止画のデータとして、Visual Tracker Benchmarkを使います。
画像認識(コンピュータビジョン)の研究で、その性能を示すためによく使われる、世界中で有名なデータセットです。

画像は、名前が4桁の連続番号で与えられます。今回は、この静止画を番号順に読み取って1つの動画にしたいと思います。

動画を生成するコード

環境

下記の環境で開発しました。

OS: Windows10
OpenCV: 4.5.2
python: 3.7.7

コード

import os
import cv2

# name of sample file
file_name = 'DragonBaby'

# get number of image files
DIR = './' + file_name + '/img'
num_total_file = sum(os.path.isfile(os.path.join(DIR, name)) for name in os.listdir(DIR))


# read image and get height and width
img = cv2.imread(DIR + '/0001.jpg')
h, w, ch = img.shape

# set codec and filename
fourcc = cv2.VideoWriter_fourcc(*'XVID')
out = cv2.VideoWriter('./'+ file_name +'/' + file_name + '.avi', fourcc, 20.0, (w,h))
# fourcc = cv2.VideoWriter_fourcc(*'MP4V')
# out = cv2.VideoWriter('./'+ file_name +'/' + file_name + '.mp4', fourcc, 20.0, (w,h))

for i in range(1, num_total_file + 1):
    # read image like 0001.jpg, 0002.jpg, ...
    img = cv2.imread(DIR + '/%04d.jpg' % i)

    # if can't read image, escape
    if img is None:
        print("cannot read images!")
        break

    # make video
    out.write(img)

out.release()
cv2.destroyAllWindows()

解説

順にプログラムの中身を説明していきます。

①画像データの総数を数える。

# name of sample file
file_name = 'DragonBaby'

# get number of image files
DIR = './' + file_name + '/img'
num_total_file = sum(os.path.isfile(os.path.join(DIR, name)) for name in os.listdir(DIR))

静止画のデータのあるフォルダを指定し、画像データが全部でいくつあるのかを数えています。
今回はDragonBabyというサンプルを使います。


②画像の大きさを取得する。

# read image and get height and width
img = cv2.imread(DIR + '/0001.jpg')
h, w, ch = img.shape

最初の画像を読み込んで、画像の大きさを取得します。
動画にする際に読み込んだ元画像と縦横比を同じにするためにこの処理をしています。


③作成したい動画の設定を決める。

# set codec and filename
fourcc = cv2.VideoWriter_fourcc(*'XVID')
out = cv2.VideoWriter('./'+ file_name +'/' + file_name + '.avi', fourcc, 20.0, (w,h))
# fourcc = cv2.VideoWriter_fourcc(*'MP4V')
# out = cv2.VideoWriter('./'+ file_name +'/' + file_name + '.mp4', fourcc, 20.0, (w,h))

動画を作成するための設定をしているのがこの部分です。

fourcc = cv2.VideoWriter_fourcc(*'XVID')

の部分で、動画のコーデックを指定しています。
拡張子 .avi なら[code]*'XVID'[/code]、.mp4なら[code]*'MP4V'[/code]のようにそれぞれ決まっています。
その他のコーデックの種類については、OpenCVの動画コーデック - Qiitaに詳しくまとめてくださっているので、こちらをご覧ください。

続いて、

out = cv2.VideoWriter('./'+ file_name +'/' + file_name + '.avi', fourcc, 20.0, (w,h))

の部分が設定の本題です。引数は、それぞれ 動画を保存するパスとファイル名、コーデック、FPS、縦横の大きさ です。


④画像を順番に読み込む。

for i in range(1, num_total_file + 1):
    # read image like 0001.jpg, 0002.jpg, ...
    img = cv2.imread(DIR + '/%04d.jpg' % i)

    # if can't read image, escape
    if img is None:
        print("cannot read images!")
        break
for i in range(1, num_total_file + 1):

for文で順に読み込んでいきます。rangeは終了条件の数字を含まないため、1加えてあげることですべての画像が読み込まれます。

    # read image like 0001.jpg, 0002.jpg, ...
    img = cv2.imread(DIR + '/%04d.jpg' % i)

1つずつ画像を読み込みます。

    # if can't read image, escape
    if img is None:
        print("cannot read images!")
        break

(ないと思いますが)万が一画像が読み込めなかったり、存在しなかった場合にはメッセージを残して終了します。

⑤動画として保存する。

    # make video
    out.write(img)

以上となります!

もし間違い等ございましたらコメントでお教えいただけますと幸いです。

【OpenCV python】 Tracking APIでのAttributeErrorの解決法

f:id:wakuron:20210720171348p:plain
OpenCV pythonで画像内の指定したオブジェクトを追従するTracking APIを実装していたらタイトルのエラーで少し混乱したので、メモ。

状況

環境

OS: Windows10
OpenCV: 4.5.2
python: 3.7.7

エラー文

AttributeError: module 'cv2' has no attribute 'TrackerMedianFlow_create'

該当箇所は

tracker = cv2.TrackerMedianFlow_create()

でした。

解決法

ソースコードにて、cv2.TrackerMedianFlow_create()としていた部分をcv2.legacy.TrackerMedianFlow_create()と変更することで解決。

OpenCV 4.5.1以降でlegacyクラスに一部移行されていたことによって、当該の関数が見つからなかったようです。ちなみに、MedianFlow以外にもいくつかのオブジェクトトラッカーがlegacyクラスに移動されています。


ざっくりとですが、バージョンごとによる使えるtrackerの種類は次項の通りです。

詳細および最新情報は、OpenCV: Tracking APIよりご確認ください!

バージョンごとのTrackerの種類

legacyでないものは、cv2.Tracker*_create()で利用可能です。
*の部分には使いたいTrackerの名前をいれてください。

ver. 4.5.3 (DaSiamRPNが追加)

9種類利用可能。

  • CSRT
  • DaSiamRPN
  • GOTURN
  • KCF
  • MIL

以下、cv2.legacy.Tracker*_create()で利用可能

  • Boosting
  • MOSSE
  • MedianFlow
  • TLD

ver. 4.5.1~4.5.2 (一部Trackerがlegacyへ移動)

8種類利用可能。

  • CSRT
  • GOTURN
  • KCF
  • MIL

以下、cv2.legacy.Tracker*_create()で利用可能

  • Boosting
  • MOSSE
  • MedianFlow
  • TLD

ver. 3.4.5~3.4.15 / ver. 4.5.0

8種類利用可能。

  • CSRT
  • GOTURN
  • KCF
  • MIL
  • Boosting
  • MOSSE
  • MedianFlow
  • TLD

ver. 3.2

6種類利用可能

  • GOTURN
  • MIL
  • Boosting
  • MOSSE
  • MedianFlow
  • TLD

OpenWeatherMAPを使って天候情報を取得したい

天候情報を使ってbotを作りたかったので、その準備としてのメモ。

1日ごとの天候情報が取得したかったが、3時間おきの予報を取得するものしか日本語で書いてある記事がなかったので、残しておきます。

 

 

OpenWeatherMAPについて

OpenWeatherMAPはフリーで使える天候に関するAPIです。有料枠を使うと、より取得できる情報が増えます。

openweathermap.org

無料枠でできるのはこんな感じ。学生の場合は天気予報のDevelopper Planと過去の天候情報のMedium Planが無料で使えるらしいです。

無料プラン 学生プラン
アクセス数 60コール/分
1,000,000コール/月
3,000 コール/分
1,000,000,000 コール/月
天候情報 Current Weather
Minute Forecast 1 hour*
Hourly Forecast 2 days*
Daily Forecast 7 days*
National Weather Alerts*
Historical weather 5 days*
左記プラス
Climatic Forecast 30 days
地図 Basic weather maps Advanced weather maps
Historical maps
過去の天候情報 5,000コール/日
Historical API
Accumulated Parameters
Statistical Weather Data API
その他 Air Pollution API
Geocoding API
同左
ウィジェット Weather widgets 同左
Uptime 95% 99.5%

※* One Call APIを用いて取得(1000 コール/日)
※National Weather Alerts は日本は未対応(2021年4月現在)

 

OpenWeatherMAPへ登録する

利用には登録(無料)が必要です。こちらからサインアップできます。
ユーザー名、メールアドレス、パスワードを入力後、認証メールが来るのでリンクを踏めば完了です。

その後、「API Keys」タブからAPIキーを確認できます。

screen API Key shows

1日ごとの天気を取得する

今回は、1日ごとの情報を取るため、「One Call API」を使います。

API呼び出しは次の通り。

https://api.openweathermap.org/data/2.5/onecall?lat={取得したい地点の緯度}&lon={取得したい地点の経度}&exclude={除外するデータ}&appid={APIキー}&units={単位}&lang={表示言語}

(例)東京の天気を取得する場合(単位:℃、言語:日本語)

https://api.openweathermap.org/data/2.5/onecall?lat=35.6895&lon=139.6917&exclude=current,minutely,hourly,alerts&appid={APIキー}&units=metic&lang=ja

上記の例では、1日ごとの天候情報のみを取得するため、現在時刻や1分ごと、1時間ごとの情報は除外しています。

ちなみに、緯度経度は、「Current Weather Data API」を使うと地名を渡して取得できます。

One Call APIを叩いて取得したjsonデータを整形するとこんな感じ。日本語が不自然。

{daily=[
    {uvi=5.83, 
    wind_speed=0.75, 
    dew_point=6.53, 
    humidity=50.0, 
    sunset=1.617267721E9, 
    feels_like={morn=14.7, eve=18.33, day=16.0, night=14.7}, 
    pressure=1021.0, 
    sunrise=1.617222482E9, 
    temp={max=20.82, night=15.83, day=16.94, eve=19.06, morn=15.6, min=15.0}, 
    dt=1.6172424E9, 
    wind_deg=107.0, 
    weather=[{id=803.0, main=Clouds, icon=04d, description=曇りがち}], 
    clouds=80.0, 
    pop=0.05}, 
    {...}], (同様のデータが7日分あります。並び順はバラバラです。)
    timezone_offset=32400.0, lon=139.6917, lat=35.6895, timezone=Asia/Tokyo}


そのため、当日の天気が知りたければ

var response_weather = UrlFetchApp.fetch('https://api.openweathermap.org/data/2.5/onecall?&lat=35.6895&lon=139.6917&exclude=current,minutely,hourly,alerts&units=metric&lang=ja&appid='+ WEATHER_API_KEY);
var weatherToday = JSON.parse(response_weather).daily[0].weather[0].description;
Logger.log(weatherToday);

のようにすれば取得できます。
※今回はGoogle Apps Script (GAS)を使いました

botについても後日書いていきたいと思います。

当ブログについて。

はじめまして。

 

午後の紅茶[おいしい無糖]と森永のラムネでできています。

未定(undecided)とラムネ(cider)からなづけました。

 

社会勉強や自分の整理を含め、日々の勉強したことを残していけたらと考えています。

現状は制御工学、機械学習、プログラミングを中心にまとめていくつもりです。

 

個人の記録ではございますが、訪問される方の何かの一助になれば幸いです!

よろしくお願いします!!!