【Python】漢字シャッフルクイズを作成:実行形式ファイルに変更

  • URLをコピーしました!
目次

漢字シャッフルクイズ

前回、漢字シャッフルクイズで画像を分割し、シャッフルする部分のプログラムを作成しました。

今回はこれを関数化して、実行形式ファイルに変更していきます。

またその際、Twitterでツイートする部分も追加していきます。

前回からすると一気に形を変えていきますが、ご容赦ください。

最終的に出力されるツイート

まずは最終的に出力されるツイートから、紹介していきます。

こんな感じで、前回の答え、ハッシュタグ、問題(宣伝用のバナー付き)でツイートするという形にしていきます。

フォルダ構成

今回の場合、最初の画像を作成し、そこから分割した画像を作成、最後にシャッフルした画像を作成します。

さらに前回の答えをツイートするため、問題のログを保存しておくことが必要になります。

ということで最終的なフォルダ構造をこんな感じに変えました。

ShuffleQuiz
├── ShuffleQuiz.ipynb
├── ShuffleQuiz.py
├── question
│   └── 20210731225250
│       ├── 0.png 〜 8.png
│       ├── 20210731225250.png
│       ├── banner_20210731225250.png
│       └── question_20210731225250.png
├── fonts
│   └── ... 
├── question.json
├── fonts.json
├── question_log.txt
└── settings.json

まず「question」フォルダの下に画像を作成した日時のファイルを作成し、その中にシャッフル前後、分割した画像、バナー画像を出力するように変更しています。

上記の例では2021年7月31日22時52分50秒に出力しているため、20210731225250というフォルダが作成されています。

その中のシャッフル前の画像が「日時.png」、シャッフル後の画像が「question_日時.png」、分割した画像が「数字.png」、シャッフル画像の下に付けるバナー画像が「banner_日時.png」です。

「question.json」、「fonts.json」は変更ありません。

また前回はありませんでしたが、「違う文字を探す脳トレ」で使ったTwitter API keyを記載しておく「settings.json」が追加されています。

さらに問題を作成した日時、問題、使用したフォントを保存しておくログファイル用のテキスト「question_log.txt」を追加しました。

(後で思いましたが、ファイル名はquestion.logでいいんじゃないだろうか…)

フォルダ構造の変更点はこんな感じです。

プログラムの解説

ここから一気に変更したプログラムを紹介・解説していきます。

default_dir = './'
divide_num = 3

import random
import json
from matplotlib import pyplot as plt
import matplotlib.font_manager as fm
import datetime
import os
from PIL import Image
import random
import tweepy
import csv

def question_choice(question_json, question_log):
    with open(question_log, 'r') as f_in:
        reader = csv.reader(f_in)
        
        for row in reader:
            prev_question = row[1]
    
    with open(question_json, 'r') as f_in:
        question_list = json.load(f_in)

    question = question_list[random.choice(list(question_list))]
    
    return question, prev_question

def font_choice(fonts_json, default_dir):
    with open(fonts_json, 'r') as f_in:
        fonts_list = json.load(f_in)
    
    font_name = random.choice(list(fonts_list))
    font_path = os.path.join(default_dir, fonts_list[font_name])
    
    return font_name, font_path

def log_write(question_log, timenow, question, font_name):
    with open(question_log, 'a') as f_in:
        row = f'{timenow},{question},{font_name}\n'
        f_in.write(row)

def fig_make(output_original_path, font_path, question):
    fp = fm.FontProperties(fname=font_path)
    
    fig = plt.figure(figsize=(6,6))
    plt.clf()

    plt.text(0.5, 0.4, question, horizontalalignment='center', verticalalignment='center', fontsize=400, fontproperties=fp)

    plt.axis('off')
    plt.savefig(output_original_path, facecolor="white", pad_inches = 0)
    
def banner_make(output_banner_path, font_path):
    fp = fm.FontProperties(fname=font_path)
    
    fig = plt.figure(figsize=(6,0.6))
    plt.clf()

    plt.text(0,0.3, "3PySci https://3pysci.com", fontsize=20, fontproperties=fp)

    plt.axis('off')
    plt.savefig(output_banner_path, facecolor="white", pad_inches = 0)
    
def divide_image(divide_num, output_original_path, output_dir):

    im = Image.open(output_original_path)

    size = im.size[0]

    divide_size = int(size/divide_num)

    left_upper = []
    for i in range(divide_num):
        for j in range(divide_num):
            left_upper.append([i*divide_size,j*divide_size])

    right_bottom = []
    for i in range(1, divide_num+1):
        for j in range(1, divide_num+1):
            right_bottom.append([i*divide_size,j*divide_size])

    crop_list = []
    for lu, rb in zip(left_upper, right_bottom):
        crop_list.append([lu[0], lu[1], rb[0], rb[1]])

    img_num = []
    for i in range(len(crop_list)):
        crop_im = im.crop(crop_list[i])
        output_file_name = os.path.join(output_dir, f'{i}.png')
        crop_im.save(output_file_name)
        img_num.append(i)
    
    return img_num, crop_list, size

def shuffle_image(img_num, output_dir, crop_list, size, output_question_path):
    random.shuffle(img_num)
    
    new_im = Image.new('RGB', (size, size))

    for i, place in zip(img_num, crop_list):
        paste_file_name = os.path.join(output_dir, f'{i}.png')
        paste_img = Image.open(paste_file_name)
        new_im.paste(paste_img, (place[0], place[1]))

    new_im.save(output_question_path)
    
def add_banner(output_question_path, output_banner_path):
    
    question_im = Image.open(output_question_path)
    banner_im = Image.open(output_banner_path)
    
    question_im_width = question_im.size[0]
    question_im_height = question_im.size[1]
    banner_im_height = banner_im.size[1]
    
    new_im = Image.new('RGB', (question_im_width, question_im_height + banner_im_height))
    
    new_im.paste(question_im, (0, 0))
    new_im.paste(banner_im, (0, question_im_height))
    
    new_im.save(output_question_path)
    
def applytotwitter(settings_json, output_question_path, prev_question):
    
    text = f'前回の答えは「{prev_question}」でした。\nこのシャッフルされた漢字はなんでしょう?😆\n#分かったらRT\n#脳トレ\n#クイズ\n#パズル\n#アハ体験\n#Python\n#プログラミング'
    
    with open(settings_json, 'r') as f_in:
        settings = json.load(f_in)

    auth = tweepy.OAuthHandler(settings['consumer_key'], settings['consumer_secret'])
    auth.set_access_token(settings['access_token'], settings['access_token_secret'])

    api = tweepy.API(auth, wait_on_rate_limit = True)

    api.update_status_with_media(status = text, filename = output_question_path)

def main():
    question_json = os.path.join(default_dir, 'question.json')
    fonts_json = os.path.join(default_dir, 'fonts.json')
    settings_json = os.path.join(default_dir, 'settings.json')
    question_log = os.path.join(default_dir, 'question_log.txt')
    
    timenow = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
    
    question_dir = os.path.join(default_dir, 'question')
    
    output_dir = os.path.join(question_dir, timenow)
    os.mkdir(output_dir)
    
    output_original_name = timenow + '.png'
    output_original_path = os.path.join(output_dir, output_original_name)
    
    output_banner_name = 'banner_' + timenow + '.png'
    output_banner_path = os.path.join(output_dir, output_banner_name)
    
    output_question_name = 'question_' + timenow + '.png'
    output_question_path = os.path.join(output_dir, output_question_name)
    
    question, prev_question = question_choice(question_json, question_log)
    font_name, font_path = font_choice(fonts_json, default_dir)
    log_write(question_log, timenow, question, font_name)
    fig_make(output_original_path, font_path, question)
    banner_make(output_banner_path, font_path)
    img_num, crop_list, size = divide_image(divide_num, output_original_path, output_dir)
    shuffle_image(img_num, output_dir, crop_list, size, output_question_path)
    add_banner(output_question_path, output_banner_path)
    applytotwitter(settings_json, output_question_path, prev_question)

if __name__ == '__main__':
    main()

多分main関数を中心に解説していった方が分かりやすいと思うので、セッティングやライブラリのインポートの部分を解説したら、main関数に移ります。

まずこのファイルの中で使用者に設定して欲しい項目を最初に2つ持ってきました。

default_dir = './'
divide_num = 3

「default_dir」はこのプログラムが配置されているパスです。

特にサーバーで使用する際、Pythonやプログラムファイルを絶対パスで呼び出したりします。

その時にプログラム内のパスが相対パスだけだと必要なファイルに辿り着けないということが多々あります。

ということでここに基準となるパス(このプログラムを置いた場所のパス)を記載しておくことで、プログラム内部のパスを絶対パスとして使用できるようにしています。

二つ目は画像の分割数ですが、個人的には3がちょうどいいと思っていますが、好きに変更してもらえるようにここに配置しました。

使用するライブラリはこんな感じ。

import random
import json
from matplotlib import pyplot as plt
import matplotlib.font_manager as fm
import datetime
import os
from PIL import Image
import random
import tweepy
import csv

前回から増えているのは「datetime」、「os」、「csv」といったところですが、これまでにも使用しているライブラリなので、まずは詳細は飛ばします。

ということでmain関数の解説に移ります。

main関数:ファイル、フォルダの設定

main関数の最初の方で読み込むファイルや出力するフォルダの設定をしています。

    question_json = os.path.join(default_dir, 'question.json')
    fonts_json = os.path.join(default_dir, 'fonts.json')
    settings_json = os.path.join(default_dir, 'settings.json')
    question_log = os.path.join(default_dir, 'question_log.txt')
    
    timenow = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
    
    question_dir = os.path.join(default_dir, 'question')
    
    output_dir = os.path.join(question_dir, timenow)
    os.mkdir(output_dir)
    
    output_original_name = timenow + '.png'
    output_original_path = os.path.join(output_dir, output_original_name)
    
    output_banner_name = 'banner_' + timenow + '.png'
    output_banner_path = os.path.join(output_dir, output_banner_name)
    
    output_question_name = 'question_' + timenow + '.png'
    output_question_path = os.path.join(output_dir, output_question_name)

ここでまずよく見かけるのが「os.path.join(a, b)」といった記述です。

これはaというパスにbというフォルダ名やファイル名、パスを結合する際に用います。

絶対パスに対して、相対パスを結合させることもできるので、今回のように様々なファイルやフォルダを使う際に重宝します。

timenowには現在の日時を文字にして格納しています。

timenow = datetime.datetime.now().strftime("%Y%m%d%H%M%S")

大体前半でJSONファイルやログファイルの読み込み先の設定、後半部分で画像の出力先の設定をしています。

question_choice関数

それでは実際に処理をしていく部分の解説に進んでいきます。

まずはquestion_choice関数。

question, prev_question = question_choice(question_json, question_log)

ここではログファイルから前回の答えを取得、さらにquestion_jsonファイルから新しい問題をランダムに取得し、返しています。

def question_choice(question_json, question_log):
    with open(question_log, 'r') as f_in:
        reader = csv.reader(f_in)
        
        for row in reader:
            prev_question = row[1]
    
    with open(question_json, 'r') as f_in:
        question_list = json.load(f_in)

    question = question_list[random.choice(list(question_list))]
    
    return question, prev_question

fonts_choice関数

次にフォントの名前(ログ記載用)とそのパスを取得する関数です。

font_name, font_path = font_choice(fonts_json, default_dir)

ここではfonts_jsonファイルからフォント名とパスを取得しますが、引数に「default_dir」があるのはサーバー上で動かすなど絶対パスに変換する必要がある時用です。

def font_choice(fonts_json, default_dir):
    with open(fonts_json, 'r') as f_in:
        fonts_list = json.load(f_in)
    
    font_name = random.choice(list(fonts_list))
    font_path = os.path.join(default_dir, fonts_list[font_name])
    
    return font_name, font_path

前回、フォントのパスを取得するのに「font_path = os.path.join(default_dir, fonts_list[random.choice(list(fonts_list))])」としてフォントのパスだけを取得していました。

しかし今回はフォント名も取得したいので、このように二段階に分割し、フォント名とフォントパスを取得しています。

    font_name = random.choice(list(fonts_list))
    font_path = os.path.join(default_dir, fonts_list[font_name])

log_write関数

次に画像を作成した日時、問題、使用したフォントをログとして残すための部分です。

log_write(question_log, timenow, question, font_name)

特筆することはなく、単純にそれぞれの値を引数にして、受け取った値をテキストに追記しています。

def log_write(question_log, timenow, question, font_name):
    with open(question_log, 'a') as f_in:
        row = f'{timenow},{question},{font_name}\n'
        f_in.write(row)

fig_make関数

次はシャッフル前の画像を作成するための関数です。

fig_make(output_original_path, font_path, question)

出力するパス、フォントのパス、問題を元にmatplotlibを使って画像を作成しますが、詳細は前回解説していますので、割愛します。

def fig_make(output_original_path, font_path, question):
    fp = fm.FontProperties(fname=font_path)
    
    fig = plt.figure(figsize=(6,6))
    plt.clf()

    plt.text(0.5, 0.4, question, horizontalalignment='center', verticalalignment='center', fontsize=400, fontproperties=fp)

    plt.axis('off')
    plt.savefig(output_original_path, facecolor="white", pad_inches = 0)

banner_make関数

これは前回出てきていない部分ですが、画像の下に宣伝用に付けるバナー用の関数です。

banner_make(output_banner_path, font_path)

こちらもmatplotlibを使い、その中に宣伝用にこのブログの名前とURLを記載しています。

def banner_make(output_banner_path, font_path):
    fp = fm.FontProperties(fname=font_path)
    
    fig = plt.figure(figsize=(6,0.6))
    plt.clf()

    plt.text(0,0.3, "3PySci https://3pysci.com", fontsize=20, fontproperties=fp)

    plt.axis('off')
    plt.savefig(output_banner_path, facecolor="white", pad_inches = 0)

やっていることが漢字の画像を作るのとほぼ同じで、画像サイズやテキストを配置する場所を変えています。

また[.savefig()]のオプションに「facecolor=’white’」としているのは画像の背景色を白とするためです。

もし宣伝のテキストを変更したい場合は「plt.text()」内の「”3PySci https://3pysci.com”」の部分を変更し、位置やフォントサイズを調整してください(できれば変更しないでおいてくれると嬉しいです)。

divide_image関数

次に画像を分割するための関数です。

img_num, crop_list, size = divide_image(divide_num, output_original_path, output_dir)

中身は前回解説していますので、割愛します。

def divide_image(divide_num, output_original_path, output_dir):

    im = Image.open(output_original_path)

    size = im.size[0]

    divide_size = int(size/divide_num)

    left_upper = []
    for i in range(divide_num):
        for j in range(divide_num):
            left_upper.append([i*divide_size,j*divide_size])

    right_bottom = []
    for i in range(1, divide_num+1):
        for j in range(1, divide_num+1):
            right_bottom.append([i*divide_size,j*divide_size])

    crop_list = []
    for lu, rb in zip(left_upper, right_bottom):
        crop_list.append([lu[0], lu[1], rb[0], rb[1]])

    img_num = []
    for i in range(len(crop_list)):
        crop_im = im.crop(crop_list[i])
        output_file_name = os.path.join(output_dir, f'{i}.png')
        crop_im.save(output_file_name)
        img_num.append(i)
    
    return img_num, crop_list, size

shuffle_image関数

シャッフル後の画像を作成するための関数です。

shuffle_image(img_num, output_dir, crop_list, size, output_question_path)

これも前回解説していますので、詳細は割愛します。

def shuffle_image(img_num, output_dir, crop_list, size, output_question_path):
    random.shuffle(img_num)
    
    new_im = Image.new('RGB', (size, size))

    for i, place in zip(img_num, crop_list):
        paste_file_name = os.path.join(output_dir, f'{i}.png')
        paste_img = Image.open(paste_file_name)
        new_im.paste(paste_img, (place[0], place[1]))

    new_im.save(output_question_path)

add_banner関数

シャッフル後の画像の下に宣伝用バナーを追加するための関数です。

add_banner(output_question_path, output_banner_path)

まずシャッフル後の画像、バナー画像をそれぞれ読み込み、縦横のサイズを変数に格納します。

シャッフル後の画像のサイズの下にバナー画像のサイズが入るようにした新しい画像new_imを作成し、そこにシャッフル後の画像、バナー画像を貼り付けています。

def add_banner(output_question_path, output_banner_path):
    
    question_im = Image.open(output_question_path)
    banner_im = Image.open(output_banner_path)
    
    question_im_width = question_im.size[0]
    question_im_height = question_im.size[1]
    banner_im_height = banner_im.size[1]
    
    new_im = Image.new('RGB', (question_im_width, question_im_height + banner_im_height))
    
    new_im.paste(question_im, (0, 0))
    new_im.paste(banner_im, (0, question_im_height))
    
    new_im.save(output_question_path)

applytotwitter関数

Twitterに画像付きツイートをするための関数です。

applytotwitter(settings_json, output_question_path, prev_question)

本当はツイートする内容のテキストは「default_dir」のようにファイルの最初の方に記述したかったのですが、前回の答えを入れる必要があるため、変数を使っています。

変数は宣言後にしか使えないために、仕方なくこの位置の記述にしています。

def applytotwitter(settings_json, output_question_path, prev_question):
    
    text = f'前回の答えは「{prev_question}」でした。\nこのシャッフルされた漢字はなんでしょう?😆\n#分かったらRT\n#脳トレ\n#クイズ\n#パズル\n#アハ体験\n#Python\n#プログラミング'
    
    with open(settings_json, 'r') as f_in:
        settings = json.load(f_in)

    auth = tweepy.OAuthHandler(settings['consumer_key'], settings['consumer_secret'])
    auth.set_access_token(settings['access_token'], settings['access_token_secret'])

    api = tweepy.API(auth, wait_on_rate_limit = True)

    api.update_status_with_media(status = text, filename = output_question_path)

この関数を実行するためには「settings.json」が必要ですが、中身としてはこんな感じで、各自API keyを記載してください。

{
    "account":"@Account Name",
    "consumer_key":" your consumer key ",
    "consumer_secret":" your consumer secret ",
    "access_token":" your access token ",
    "access_token_secret":" your access token secret "
}

これで設定をして、実行すると最初に紹介したツイートがされます。

今回はなかなか長くなってしまいましたが、サーバーにアップロードするなり、自分のPC上で実行するなり、比較的簡単にできるようになったかと思います。

これで漢字シャッフルクイズのプログラムに関しては完成です。

ただ実はこの大掛かりな変更にはもう一つ、しかしものすごく大きなメリットがあります。

そのメリットは新しいクイズプログラムを作成する際に発揮されます。

ということで次回はさらにもう一つ新しいクイズプログラムを作成していきましょう。

ではでは今回はこんな感じで。

よかったらシェアしてね!
  • URLをコピーしました!

コメント

コメントする

目次