PR
PR

【Pythonの学習 Day4】時計サービスをオシャレにパワーアップ!時間帯で背景が変わる機能を追加してみた

記事内に広告が含まれています。

PythonをGemini先生に教わって勉強するの4日目です。

さて、これまで作成してきた現在の時刻をリアルタイムで表示するという時計サービスですが、今回は「時間帯によって背景画像が自動で切り替わる」という、ちょっとオシャレな機能に挑戦してみたいと思います!

いつも通り、頼れるGemini先生に「こんなことできますか?」と相談したところ、今回も的確なアドバイスをいただきました。

どうやら、以下のファイルを作成したり、修正したりする必要があるようです。

  • 背景変更用JavaScriptの作成: static/js/background_updater.js
  • CSSの作成: static/css/style.css
  • 時刻リアルタイム表示用JavaScriptの改造:static/js/realtime_clock.js
  • HTMLテンプレートの編集: templates/time.html
  • 背景用画像の準備

うーん、今回もPythonは使用しないのですね…

背景変更用JavaScriptの作成 static/js/background_updater.js

時計アプリの背景を時間帯によって変化させるためのJavaScriptファイル、「static/js/background_updater.js」を作成していきます。

以下がGemini先生が教えてくれたJavaScriptのコードになります。

今回はPythonの勉強がメインなので、こちらについては詳しい説明は省略します。

// 現在適用されている背景クラス 
let currentBgClassGlobal = ''; 

// 時間帯を判定する関数 (スマートバージョン)
function getTimeOfDayClass(hours) {

    // hours が数値であり、0から23の有効な整数であることを確認する
    if (typeof hours !== 'number' || !Number.isInteger(hours) || hours < 0 || hours > 23) {

        // 不正な入力値の場合の処理
        console.error('getTimeOfDayClass: 無効な時間です:', hours, 'デフォルトのクラスを返します。');
        return 'bg-00';
    }

    // hoursを2桁の文字列に変換する
    const formattedHours = String(hours).padStart(2, '0');

    // 文字列テンプレートを使ってクラス名を組み立てる
    return `bg-${formattedHours}`;
}

// 背景を更新するグローバル関数 
function updateBackgroundDisplay(hours) {
    const bodyElement = document.body; // body要素を取得

    // bodyElement が見つからない場合のガード処理
    if (!bodyElement) {
        console.error('updateBackgroundDisplay: body要素が見つかりません。');
        return;
    }

    // 新しい背景クラスを取得
    const newBgClass = getTimeOfDayClass(hours);

    // newBgClassが有効かチェック
    if (newBgClass !== currentBgClassGlobal) {

        // 現在適用されているクラスがあれば削除
        if (currentBgClassGlobal) {
            bodyElement.classList.remove(currentBgClassGlobal);
        }

        // 新しいクラスを追加
        bodyElement.classList.add(newBgClass);

        // 現在のクラスを更新
        currentBgClassGlobal = newBgClass;

        // デバッグ用
        // console.log(`背景クラスが ${newBgClass} に更新されました。`); 
    }
}

CSSの作成 static/css/style.css

JavaScriptだけでは「どの時間に、どんな背景画像を表示するか」という具体的な見た目の指定ができません。

そこで登場するのが、Webページのスタイリングを担当するCSS (カスケーディング・スタイル・シート) です!

こちらもGemini先生が教えてくれたCSSのコードになります。

「body」「h1」「p」でページ全体の文字や時計の文字の見た目を設定しています。

JavaScriptから指定されるクラス名「.bg-xx」に対応する背景画像を指定しています。

body {
    /* 背景が切り替わる時にフワッと変化するようにアニメーションを設定 */
    transition: background-color 1.5s ease-in-out, color 1.5s ease-in-out;
    min-height: 100vh; /* 画面全体の高さを確保 */
    margin: 0; /* 余白をなくす */
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    text-align: center;
    font-family: sans-serif;
}

h1 {
    color: #FFFFFF;
    font-weight: bold;
    color: #FFFFFF;
    letter-spacing : 8px;           /* 文字間 */
    text-shadow    : 
       4px  4px 1px #000000,
      -4px  4px 1px #000000,
       4px -4px 1px #000000,
      -4px -4px 1px #000000,
       4px  0px 1px #000000,
       0px  4px 1px #000000,
      -4px  0px 1px #000000,
       0px -4px 1px #000000;        /* 文字の影 */
}

p {
    font-size: 36pt;
    font-weight: bold;
    color: #FFFFFF;
    letter-spacing : 8px;           /* 文字間 */
    text-shadow    : 
       4px  4px 1px #000000,
      -4px  4px 1px #000000,
       4px -4px 1px #000000,
      -4px -4px 1px #000000,
       4px  0px 1px #000000,
       0px  4px 1px #000000,
      -4px  0px 1px #000000,
       0px -4px 1px #000000;        /* 文字の影 */
}

/* --- 時間帯ごとの背景クラス --- */

/* 00:00~00:59 */
.bg-00 {
    background-image: url("../images/00/01_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-01 {
    background-image: url("../images/01/01_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-02 {
    background-image: url("../images/02/02_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-03 {
    background-image: url("../images/03/03_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-04 {
    background-image: url("../images/04/04_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-05 {
    background-image: url("../images/05/05_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-06 {
    background-image: url("../images/06/06_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-07 {
    background-image: url("../images/07/07_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-08 {
    background-image: url("../images/08/08_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-09 {
    background-image: url("../images/09/09_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-10 {
    background-image: url("../images/10/10_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-11 {
    background-image: url("../images/11/11_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-12 {
    background-image: url("../images/12/12_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-13 {
    background-image: url("../images/13/13_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-14 {
    background-image: url("../images/14/14_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-15 {
    background-image: url("../images/15/15_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-16 {
    background-image: url("../images/16/16_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-17 {
    background-image: url("../images/17/17_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-18 {
    background-image: url("../images/18/18_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-19 {
    background-image: url("../images/19/19_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-20 {
    background-image: url("../images/20/20_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-21 {
    background-image: url("../images/21/21_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-22 {
    background-image: url("../images/22/22_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

.bg-23 {
    background-image: url("../images/23/23_cat_01.jpg");
    background-size: cover;
    background-position: center;
}

時刻をリアルタイム表示するためのJavaScript static/js/realtime_clock.jsの改造

もともとリアルタイムで時刻を表示していた「static/js/realtime_clock.js」を改造し、時刻表示の更新と同時に、背景更新の命令も出すように機能を追加します。

こちらもGemini先生に教えてもらったコードをそのまま使用します。

/**
 * @param {Date} dateObject - 表示する時刻のDateオブジェクト
 */
function updateTimeDisplay(dateObject) {

    // Dateオブジェクトから年月日時分秒を取得して整形
    const year = dateObject.getFullYear();
    const month = (dateObject.getMonth() + 1).toString().padStart(2, '0');
    const day = dateObject.getDate().toString().padStart(2, '0');
    const hours = dateObject.getHours().toString().padStart(2, '0');
    const minutes = dateObject.getMinutes().toString().padStart(2, '0');
    const seconds = dateObject.getSeconds().toString().padStart(2, '0');

    // 表示する文字列を作成
    const timeString = `${year}年${month}月${day}日 ${hours}時${minutes}分${seconds}秒`;

    // id="realtime-clock" の要素のテキストを更新
    const clockElement = document.getElementById('realtime-clock');
    if (clockElement) {
        clockElement.textContent = timeString;
    }
}

/**
 * 背景更新処理を呼び出す専門の関数
 * @param {Date} dateObject - 現在時刻のDateオブジェクト。
 */
function triggerBackgroundUpdate(dateObject) {

    // 背景判定用の「時」を取得 (整形なしの数値)
    const hoursForBackground = dateObject.getHours(); 

    // background_updater.jsで定義されているはずの関数を呼び出す
    if (typeof updateBackgroundDisplay === 'function') {

        // 整形前の「時」の数値を渡す
        updateBackgroundDisplay(hoursForBackground);
    } else {
        // console.warn("背景更新関数 'updateBackgroundDisplay' が見つかりません。");
    }
}

/**
 * 時刻表示と背景更新の呼び出しを統括する新しい関数
 * この関数が1秒ごとに実行されるメインルーチン
 */
function masterUpdateRoutine() {

    // Dateオブジェクトをここで一度だけ生成
    const currentDate = new Date(); 
    
    updateTimeDisplay(currentDate);
    triggerBackgroundUpdate(currentDate);
}

// --- 実行部分の変更点 ---

/**
 * 1秒ごとの実行と初回実行で呼び出す関数
 */
setInterval(masterUpdateRoutine, 1000);

document.addEventListener('DOMContentLoaded', function() {
    masterUpdateRoutine(); // 初回実行も masterUpdateRoutineで行う
});

HTMLテンプレート templates/time.htmlの編集

これまで時計アプリに「時間帯で背景が変わる」という機能を追加するために、以下のファイルの作成・編集を行ってきました。

  • 背景画像を切り替えるロジック (background_updater.js)
  • 背景画像の具体的なスタイル (style.css)
  • 時刻表示と背景更新の指令を出す (realtime_clock.js)

これらのファイルを組み込み、ユーザーが実際に目にするWebページを定義する「templates/time.html」を編集します。

以前からの変更点としては、新たに作成した「style.css」と「background_updater.js」を組み込むために、赤字部分の記述を追加しています。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>リアルタイム時刻表示(背景自動変更機能付き!)</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
    <script src="{{ url_for('static', filename='js/background_updater.js') }}" defer></script>
    <script src="{{ url_for('static', filename='js/realtime_clock.js') }}" defer></script>
</head>
<body>
    <h1>現在の時刻</h1>
    <p id="realtime-clock">{{ current_time }}</p>
</body>
</html>

背景画像の準備とPythonの出番! Pillowで画像をサクッと変換

時計サービスの見た目を華やかにするための背景画像は、全部(24枚)Gemini先生に作ってもらいました!

さて、これらの画像をアプリで使うために、「static/css/style.css」で指定した画像のパスに合わせて、「static/images」フォルダの中に、00時から23時までの各時間に対応するフォルダを作成します。

$ mkdir -p ~/project/time_app/static/images/{00..23}

ここに、Gemini先生謹製の時間帯別背景画像を格納していくことになります。

まず、「~/project/time_app/static/images」にGemini先生が作成してくれた画像ファイルを格納にしておきます。

ところで、Gemini先生から提供された画像はPNG形式でした。PNGはとても綺麗な画像形式なのですが、Webで使うにはファイルサイズが少し大きめになりますし、ページの表示速度にも関わってくるので、できればもう少し軽量なJPEG形式に変換したいところです。

「画像編集ソフトで一枚一枚変換するのは大変だなぁ…。そうだ!せっかくPythonを勉強しているんだから、Pythonで一括変換できないかな?」

そう思い立ってGemini先生に相談してみると、こんな頼もしいお返事が!

「images フォルダに入っているたくさんのPNG画像を、Pythonを使って一括でJPEG形式に変換したいんだね!
それはPythonの画像処理ライブラリ Pillow を使うと、比較的簡単にできるのだ。」

ということで、最初に「Pillow」ライブラリをインストールします。

今回は「uv」というパッケージインストーラーを使っているので、仮想環境 (time_app) に入って以下のコマンドを実行し「Pillow」をインストールします。

(time_app) $ uv pip install Pillow
Resolved 1 package in 436ms
Prepared 1 package in 1.87s
Installed 1 package in 7ms
 + pillow==11.2.1

PNG画像をJPEG形式に一括で変換してくれるPythonスクリプトを作成します。

(time_app) $ vi ~/project/time_app/png_to_jpeg.py 

Gemini先生が教えてくれたコードがこちらです。

from PIL import Image  # PillowライブラリのImageモジュールをインポート
import os              # ファイルやディレクトリ操作のためのosモジュールをインポート

# PNGファイルがあるフォルダを指定
SOURCE_DIRECTORY_NAME = "static/images"

def convert_png_to_jpeg_in_directory(source_dir): 
    print(f"\nディレクトリ '{source_dir}' 内のPNGファイルを検索中...")

    # 見つかったPNGファイルの数と、画像変化を行った数をカウントするための変数を初期化
    found_pngs = 0
    processed_count = 0

    for filename in os.listdir(source_dir):
        if filename.lower().endswith(".png"):
            found_pngs += 1
            png_file_path = os.path.join(source_dir, filename)
            
            # JPEGファイル名を作成 (例: image.png -> image.jpg)
            base_filename = os.path.splitext(filename)[0]
            jpeg_filename = base_filename + ".jpg"
            jpeg_file_path = os.path.join(source_dir, jpeg_filename)

            try:
                print(f"\nファイルNo.{found_pngs}: '{png_file_path}' を処理中...")
                with Image.open(png_file_path) as img:
                    if img.mode == 'RGBA' or img.mode == 'P':
                        print(f"  '{filename}' は透過情報を持つ可能性があるのでRGBに変換。")
                        img_rgb = img.convert('RGB')
                    elif img.mode != 'RGB':
                        print(f"  '{filename}' はRGBモードではないのでRGBに変換 (現在のモード: {img.mode})。")
                        img_rgb = img.convert('RGB')
                    else:
                        img_rgb = img

                    img_rgb.save(jpeg_file_path, "jpeg", optimize=True, progressive=True)
                    print(f"  変換完了! -> '{jpeg_file_path}'")
                    processed_count +=1

            except FileNotFoundError:
                print(f"  エラー: ファイル '{png_file_path}' が見つからなかったのでスキップします。")
            except Exception as e:
                print(f"  エラー: '{png_file_path}' の変換中に問題が発生しました: {e}")
    
    if found_pngs == 0:
        print(f"ディレクトリ '{source_dir}' にPNGファイルは見つかりませんでした。")
    else:
        print(f"\n合計 {found_pngs} 個のPNGファイルが見つかり、{processed_count} 個の処理を試みました。")
    print("全ての処理が完了しました。")


# --- スクリプトが直接実行されたときの処理 ---
if __name__ == "__main__":
    print("--- PNGからJPEGへの一括変換スクリプト ---")

    if not os.path.isdir(SOURCE_DIRECTORY_NAME):
        print(f"エラー!: 設定されたソースフォルダ '{SOURCE_DIRECTORY_NAME}' が見つかりません。")
        print(f"このスクリプトと同じ階層に '{SOURCE_DIRECTORY_NAME}' という名前のフォルダを作成し、")
        print("その中に変換したいPNG画像を入れてから、もう一度実行してください。")
    else:
        print("\n以下の設定で処理を実行しようとしています。:")
        print(f"  - PNG画像のソースフォルダ: '{SOURCE_DIRECTORY_NAME}'")
        print(f"  - JPEG画像の出力先フォルダ: ソースフォルダと同じ場所 ('{SOURCE_DIRECTORY_NAME}')")
        print("--------------------------------------------------")

        user_confirmation = input("上記の設定で処理を開始してもよろしいですか? (yes / no): ").strip().lower()

        if user_confirmation == 'yes':
            print("\n処理を開始します...")
            convert_png_to_jpeg_in_directory(
                SOURCE_DIRECTORY_NAME
            )
        else:
            print("処理をキャンセルしました。設定を見直して、また実行してください。")

コードの解説

新しく使用するコードについて解説をしていきます。

def convert_png_to_jpeg_in_directory(source_dir):

defは「これから関数を定義(define)する」という合図です。

その後に記述している「convert_png_to_jpeg_in_directory」が関数名で、「(source_dir)」の部分は引数になり、最後は「:」(コロン)で終わり、次の行からインデント(字下げ)して関数の処理内容を記述していきます。

今回の場合は「source_dir」という値を引数として、関数「convert_png_to_jpeg_in_directory」に渡しています。

ちなみに、今回の場合「source_dir」は以下の部分で定義されていて、変数「SOURCE_DIRECTORY_NAME」の値が「source_dir」に渡されています。

if __name__ == "__main__":

### 中略 ###

if user_confirmation == 'yes':
    print("\n処理を開始します...")
    convert_png_to_jpeg_in_directory(
        SOURCE_DIRECTORY_NAME
    )

print(f"\nディレクトリ '{source_dir}' 内のPNGファイルを検索中...")

print()は、画面に文字列を出力するための関数です。

()内文字列を引数として値を画面に出力します。

今回は引数として(f"\nディレクトリ '{source_dir}' 内のPNGファイルを検索中...")が指定されています。

(f"...")は「f文字列」(f-strings, フォーマット済み文字列リテラル)と呼ばれる特別な文字列となります。

「f文字列」の中では{}(波括弧)で囲まれた変数を直接埋め込むことが出来るようになります。

for filename in os.listdir(source_dir):

os.listdir(source_dir) で、指定されたフォルダ (source_dir) の中にあるファイルやフォルダの名前をリストとして全部取得しています。

for ... in ...:

「for ... in ...:」は繰り返し処理の定番で、今回の場合は「os.listdir(source_dir)」の中身を一つずつ「filename」という変数に格納しています。

if filename.lower().endswith(".png"):

変数「filename」に格納されている文字列(今回の場合はファイル名)が、「.png」で終わるかどうかを判定しています。

.lower()

「.lower()」は文字列メソッドの一つで、文字列のすべてを小文字に変換します。

今回の場合は、変数「filename」の文字列をすべて小文字に変換しているので、拡張子が「.PNG」と大文字で書かれていたり、「.png」小文字で書かれていたりする場合も、同じ「.png」として扱えるようになります。

.endswith(".png")

「.endswith()」は、文字列メソッドの一つで、()の中に指定された文字列でその文字列が終わっているかどうかを判定しています。

今回の場合は「.png」で終わっているかを判定しています。

判定の結果は真偽値(TrueまたはFalse)となります。

  • 終わっている場合:True
  • 終わっていない場合:False

png_file_path = os.path.join(source_dir, filename)

ディレクトリとファイル名を結合して、ファイルのパスを作成して変数「png_file_path」に格納しています。

os.path.join(source_dir, filename)

「os.path.join」は、Pythonの「os」モジュールに含まれる「path」サブモジュールの中の「join」という関数になります。

この関数の主な目的は、一つ以上のパスの構成要素(ディレクトリ名やファイル名)を受け取り、それらをOSに適した形で連結して一つのパス文字列を生成することです。

この関数が必要な理由

オペレーティングシステム(OS)によって、パスの区切り文字が異なります。

  • Windowsではバックスラッシュ 「\」
  • macOSやLinuxではスラッシュ 「/」

os.path.join() を使うと、プログラムが実行されているOSを自動的に判別し、適切な区切り文字を使ってパスを組み立ててくれます。

base_filename = os.path.splitext(filename)[0]

変数「filename」に格納されているファイル名(例: "image.png")から、拡張子(例: ".png")を取り除いた部分(例: "image")だけを取得し、それを「base_filename」という変数に格納しています。

os.path.splitext(filename)

「os.path.splitext」は、Pythonの「os」モジュールの中の「path」サブモジュールに含まれる関数の中の「splitext」という関数になります。

この関数の役割は、引数として渡されたファイル名 (今回は、変数filename) を、「拡張子の前の部分(ルートやベース名と呼ばれます)」と「拡張子」の2つの部分に分割することです。

分割は、文字列の最後にあるピリオド「.」を基準に行われます。

この関数は、結果をタプル (tuple) という形式で返します。

タプルは複数の要素をひとまとめにしたもので、(ルート部分, 拡張子部分) のように2つの要素を持ちます。

  • os.path.splitext("image.png") を実行すると、("image", ".png") というタプルが返されます。
  • os.path.splitext("backup.tar.gz") を実行すると、("backup.tar", ".gz") というタプルが返されます。(最後の「.gz」のみが拡張子として扱われます)
  • os.path.splitext("filename_without_extension") を実行すると、("filename_without_extension", "") というタプルが返されます(拡張子がない場合は、拡張子部分が空文字列になります)。
[0]

[0] は、インデックスアクセスと呼ばれる操作です。

Pythonでは、タプルやリストなどの複数の要素を持つデータ構造に対して、[インデックス番号] を使って特定の要素を取り出すことができ、インデックス番号は0から始まります。

つまり、[0] はタプルの最初の要素を取り出すことを意味します。

「image.png」の場合は、「image」の部分が取り出されて、変数の「base_filename」に格納されます。

try: .... expect ...:

try: は、プログラムの中でエラー(Pythonでは「例外」と呼びます)が発生する可能性のある処理を実行する際に使われるキーワードです。

try: の後に続くインデントされたコードブロック(tryブロック)の中に、エラーが起こるかもしれない処理を書きます。

エラー(例外)が発生すると、対応するexpectブロックに処理が移ります。

これは、例外処理(エラーハンドリング)という仕組みの重要な部分を担います。

try:
    正常な場合の処理
except A:
    Aという特定のエラーが発生した場合の処理
except as e:
    その他のエラーが発生した場合処理 {e}

「e」にエラーの詳細情報が入ります。

with Image.open(png_file_path) as img:

with

with文は、主にリソースの管理を自動化し、コードをより安全で読みやすくするための構文です。

with文は、「コンテキストマネージャ (context manager)」という仕組みを利用しています。

コンテキストマネージャとは、特定の処理(with ブロック内の処理)の前後で、決まった準備処理(セットアップ)と後始末処理(クリーンアップ)を実行することを保証するオブジェクトのことです。

ファイルを開いたり、ネットワーク接続を確立したり、データベースに接続したりといった、「使い始めたら必ず後片付け(クローズや解放など)が必要なもの」を扱う際に非常に便利です。

with文を使う理由

ファイルのようなリソースは、使い終わったら必ず閉じる必要があり、もしファイルを閉じるのを忘れると、以下のような問題が起こる可能性があります。

  • リソースリーク: 開いたままのファイルが増えすぎると、システムが利用できるファイルリソースを使い果たしてしまう可能性があります。
  • データの不整合: 書き込み中のファイルが正しく閉じられないと、データが完全に保存されないことがあります。
  • 他のプログラムからのアクセス制限: あるプログラムがファイルを開きっぱなしにしていると、他のプログラムがそのファイルを利用できなくなることがあります。

with 文を使わない場合、ファイルを確実に閉じるためには以下のように「try...finally」構文を使うのが一般的です。

withを使った場合と比較すると、とても長くなってしまいます。

# with文を使わない場合
img = None  # tryの外で変数を初期化
try:
    img = Image.open(png_file_path)
    # imgを使った処理
    # ...
except Exception as e:
    print(f"エラーが発生しました: {e}")
finally:
    if img:  # imgが正常に開かれていれば
        img.close() # 明示的に閉じる
Image.open(png_file_path)

「Image」はPillowはライブラリの中の主要なモジュールになり、「.open()」はその関数(メソッド)で、指定したファイルパスから画像ファイルを開く役割を持っています。

今回の場合は、変数「png_file_path」を引数として渡しています。

Image.open(png_file_path) が成功すると、開かれた画像データを操作するための「画像オブジェクト」が返されます。

as img:

Image.open(png_file_path) が返した「画像オブジェクト」が、「with」文のブロック内で「img」という名前の変数として使えるようにしています。

if img.mode == 'RGBA' or img.mode == 'P':

「.mode」は画像オブジェクトが持つ属性(プロパティ)の一つです。

mode属性(プロパティ)には、その画像がどのような方法で色情報を保持しているかを示すカラーモード(色の表現方法)を表す文字列が格納されています。

カラーモードには、'RGB' (赤緑青)、'RGBA' (赤緑青+透明度)、'L' (グレースケール)、'P' (パレットモード) など、様々な種類があります。

「with Image.open(png_file_path) as img」で取得した画像オブジェクトを変数「img」に格納したので、それを「img.mode」という形でimgのカラーモードを取得し、「RGBA」または「P」であるかどうかを判別しています。

img_rgb = img.convert('RGB')

「.convert()」関数(メソッド)は、画像のカラーモードを指定された別のカラーモードに変換してくれます。

今回は引数として指定されている「'RGB'」にカラーモードを変更して、「img_rgb」という変数に画像オブジェクトを格納しています。

img_rgb.save(jpeg_file_path, "jpeg", optimize=True, progressive=True)

「img_rgb」はカラーモードがRGBの画像オブジェクトが格納されている変数です。

.save()は画像オブジェクト「img_rgb」が持っているメソッドです。

このメソッドの役割は、指定されたファイルパスに、指定されたフォーマットで画像を保存します。

(jpeg_file_path, "jpeg", optimize=True, progressive=True)

引数はそれぞれ以下の意味を持っています。

  • jpeg_file_path: 保存先のファイルパス
  • "jpeg": 保存する画像のフォーマット
  • optimize=True: 画像の圧縮を有効にする
  • progressive=True: プログレッシブJPEGとして保存

if not os.path.isdir(SOURCE_DIRECTORY_NAME):

「if not」は「os.path.isdir(SOURCE_DIRECTORY_NAME)」の評価結果を反転させています。

os.path.isdir(SOURCE_DIRECTORY_NAME)

「os」は、Pythonの標準ライブラリーの一つで、オペレーティングシステム(OS)とやり取りを行うための様々な機能を提供します。

「.path」は、「os」モジュールの中のサブモジュールであり、主にファイルパスの操作に関する関数が集められています。

「.isdir()」は、「path」モジュールが提供する関数の一つで、引数として渡されたパス文字列が示す対象が、実際に存在していて、それがディレクトリであるかの判定を行います。

  • 指定されたパスが実在するディレクトリであれば「True」を返します
  • 指定されたパスがディレクトリでない場合や、存在しないパスである場合は「False」を返します

user_confirmation = input("上記の設定で処理を開始してもよろしいですか? (yes / no): ").strip().lower()

ユーザーに処理を実行してよいかを確認し、その回答を受け取り、後の比較処理がしやすいように文字列を整形して、変数「user_confirmation」に格納しています。

input("上記の設定で処理を開始してもよろしいですか? (yes / no): ")

「input()」は、Pythonの組み込みメソッド(関数)の一つで、ユーザーにメッセージを表示し、ユーザーがキーボードから何かを入力してEnterキーを押すのを待ちます。

「input()」の引数として渡された文字列「"上記の設定で処理を開始してもよろしいですか? (yes / no): "」が画面に表示されるメッセージとなり、ユーザーによるキーボード入力と「Enter」キーの押下を待ちます。

ユーザーが入力し「Enter」キーを押下すると、入力された内容が文字列として「input()」関数の戻り値となります。

.strip()

「.strip()」は文字列オブジェクトが持つメソッドの一つで、「input()」関数から返された文字列に対して適用されます。

文字列の先頭と末尾にある空白文字(スペース、タブ、改行など)をすべて取り除きます。

なお、文字列の途中にある空白は取り除きません。

「.strip()」メソッド(関数)を使用することで、ユーザーが意図せずに文字列の先頭や末尾に余計なスペー加えて入力した場合 (例: " yes" や "yes " など)「.strip()」によって、これらのスペースが取り除かれ、純粋な入力内容のみを取得できます。

.lower()

「.lower()」も文字列オブジェクトが持つメソッド(関数)の一つです。

これは、上記「.strip()」で処理された後の文字列に対して、さらに適用され文字列に含まれるすべてのアルファベットを小文字に変換します。

ユーザーは「yes」と答える際に、「"Yes"」「"YES"」「"yEs"」のように大文字・小文字を混在させて入力する可能性がありますが、「.lower()」を使うことで、これらの入力をすべて一律で「"yes"」という小文字の文字列に統一できます。

convert_png_to_jpeg_in_directory(SOURCE_DIRECTORY_NAME)

定義済みの「convert_png_to_jpeg_in_directory」関数を呼び出し、変数「SOURCE_DIRECTORY_NAME」に格納されたディレクトリのパスを引数として渡し実行しています。

画像変換

作成したPythonのスクリプト「png_to_jpeg.py」を実行して、実際に画像を変換しました。

実行時のログは以下の通りです。

(time_app) $ python ./png_to_jpeg.py 
--- PNGからJPEGへの一括変換スクリプト ---

以下の設定で処理を実行しようとしています。:
  - PNG画像のソースフォルダ: 'static/images'
  - JPEG画像の出力先フォルダ: ソースフォルダと同じ場所 ('static/images')
--------------------------------------------------
上記の設定で処理を開始してもよろしいですか? (yes / no): yes

処理を開始します...

ディレクトリ 'static/images' 内のPNGファイルを検索中...

ファイルNo.1: 'static/images/19_cat _01.png' を処理中...
  変換完了! -> 'static/images/19_cat _01.jpg'

##### 以下省略 #####

スクリプトの実行により、PNGファイルがJPEGファイルに変換されたことを確認しました。

変換後のファイル構成は以下の通りです。

$ ls -l static/images/ | head -7
合計 136664
drwxrwxr-x 2 tamohiko tamohiko    4096  5月 19 14:49 00
-rw-rw-r-- 1 tamohiko tamohiko  275536  5月 19 22:21 00_cat_01.jpg
-rw-rw-r-- 1 tamohiko tamohiko 5187262  5月 17 23:11 00_cat_01.png
drwxrwxr-x 2 tamohiko tamohiko    4096  5月 19 14:49 01
-rw-rw-r-- 1 tamohiko tamohiko  409732  5月 19 22:21 01_cat_01.jpg
-rw-rw-r-- 1 tamohiko tamohiko 4762317  5月 19 09:51 01_cat_01.png

変換されたjpegの画像ファイルは、それぞれの「static/images/xx」ディレクトリに手作業で格納していきます。

本当はPythonを使うと、一括でディレクトリに格納することもできそうですが、今回は早く時計サービスの動作検証を行いたいので、手作業で格納を行います。

動作確認

いよいよ「time_app.py」を実行して、実際に背景が表示されて1時間ごとに自動的に変更されるかの確認をしていきます。

$ python ./time_app.py 
 * Serving Flask app 'time_app'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 787-176-237
 

Webブラウザで「http://127.0.0.1:5000」にアクセスし、背景が表示されるかを確認します。

背景が期待通りに表示されることを確認しました。

次に、背景が自動的に変更されるかを確認するため、1時間ほど待ってから再度アクセスします。

時間を空けて確認したところ、背景は無事に変更されていました!

これで、目標としていた背景の自動切り替え機能を追加することができました!

今日の学習まとめ:背景の自動更新と画像ファイルの一括変換で学んだこと

Gemini先生とのPython学習4日目、Webページにリアルタイムで時刻を表示する時計サービスに背景を追加して、1時間毎に背景を自動更新をする機能を実装することに挑戦しました!

今回の学習を通して、主に以下のことができるようになり、理解が深まりました。

  • Pythonの画像処理ライブラリ:Pillow を使うことで、Pythonプログラムから画像を開いたり、フォーマットを変換(今回はPNGからJPEGへ)したり、様々な処理を簡単に行えることを学びました。
  • Pythonの基本的な型:Pythonのブログラムで使用できる、基本的な型を学びました
    • 関数 (def): 特定の処理をまとめる「関数」を定義する方法 (def 関数名():) と、それを呼び出す方法を学びました。今回は画像変換を行う関数を作成しました。
    • f文字列: print(f"\{変数名}")で{}内の変数を画面に出力できることを学びました。
    • 繰り返し: リストなどの要素を順番に処理したり、特定の回数だけ処理を実行したりするために for 文を使う方法を学びました
    • for 変数 in イテラブルオブジェクト:
          繰り返したい処理
      
    • 条件分岐:if、elif、else を使って、条件によって処理の流れを変える方法を学びました
    • if 条件式:
          条件式が真(True)の時の処理
      else:
          条件式が偽(False)の時の処理
       変数 in イテラブルオブジェクト:
      

      条件が複数ある場合は「elif」を使う。

      if 条件式1:
          条件式1が真(True)の時の処理
      elif 条件式2:
          条件式1が偽(False)で、条件式2が真(True)の時の処理
      else:
          すべての条件式が偽(Fasle)の時の処理
      
    • エラー(例外)処理: 予期せぬエラーが発生した場合でも、プログラムが停止しないように「try...except」を使ってエラーを捕捉し、対処する方法を学びました。
    • try:
          正常な場合の処理
      
      except A:
          Aという特定のエラーが発生した場合の処理
      
      except as e:
          その他のエラーが発生した場合処理 {e}
      
    • with: with文を使用することで、処理が終わった後にファイルを確実に閉じるなどの後始末を自動で行え、安全なコードを書けることを学びました。
    • ユーザー入力 (input): ユーザーに実行確認を求めるために input() 関数を使う方法をや「.strip()」「.lower()」と組み合わせて入力値を整形する方法も学びしました。

イテラブルオブジェクト: for 文で繰り返し処理ができる対象(イテラブルオブジェクト)には、リスト、タプル、文字列、辞書、range() 関数など、様々な種類があります。

今後の予定

Flaskの開発サーバーでは、時計サービスの基本的な動作(時刻のリアルタイム表示と背景の自動切り替え)を確認できました。

次のステップとして、WSGIサーバー(例: Gunicorn, uWSGIなど)を導入し、作成した時計サービスをインターネット上に公開することを目指します。

コメント

タイトルとURLをコピーしました