5
4

More than 1 year has passed since last update.

Flaskで自動的に化学小テストを生成するアプリを作った話

Last updated at Posted at 2022-04-18

どうも、小田急電鉄株式会社初のIT開発エンジニア、Xuです。

実は仲がいい隣人が化学の教師をやっていて、
「毎回授業で高校一年生に小テストを出してるんだけど、出題するのがめんどくさい」
と言われ、彗星のごとく現れた自動化の天才(自称)として見逃せない事態だと認知し、
重たいVBAでテスト問題を生成していた隣人を助けるべく始めたプロジェクトです。
地味に初めてflaskアプリを公開するので色々とコード汚いのはご愛嬌ということで( ´∀` )

環境

-AWS EC2(t2.micro無料枠)
-Ubuntu 20.04
-Apache2
-Python3.8

前準備

考え方としては
(xg/xmol/xL)の(物質名)の(体積/質量/物質量)的な文を自動的にPDFに生成していき、
出来上がった問題がいい感じであればファイルをダウンロードするというシンプルなもの。

ディレクトリ構成
/var/www/html/
|
|---app
|  |
|  |--flask_app
|  |
|  |--app.wsgi
|
|---static
  |
  |--template.pdf
  |
  |--ipag.ttc

staticは他appと共用のため構造がちょっと汚い^^;

ライブラリインストール

sudo apt-get install apache2
sudo apt -y install python3-pip
sudo pip install mod_wsgi
sudo pip install flask
sudo pip install PyPDF2
sudo pip install reportlab

Python3.8環境でfitzをインストールしようとすると以下のエラーが出るため、

File "/home/adam/venvs/p3/lib/python3.8/site-packages/starlette/staticfiles.py", line 55, in __init__
	raise RuntimeError(f"Directory '{directory}' does not exist")

代わりに

sudo pip install PyMuPDF

を使う、すでにインストールしていた場合はアンインストールしましょう。

とりあえず実行可能なflaskアプリを作って、WGSIでデーモン化してみる

Python /var/www/html/app/flask_app/app.py
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return "Hello World!!!!"

if __name__ == '__main__':
    app.run()
Python /var/www/html/app/flask_app/app.wsgi
import sys

sys.path.insert(0, '/var/www/html/app/flask_app')
from app import app as application

path確認

python -c "import sys; print(sys.path)"

そのうちmod_wsgi-py38.cpython-38-x86_64-linux-gnu.soが入ってるディレクトリを見つけ出し、
/etc/apache2/sites-available/000-default.confに追加:

LoadModule wsgi_module /usr/local/lib/python3.8/dist-packages/mod_wsgi/server/mod_wsgi-py38.cpython-38-x86_64-linux-gnu.so
WSGIDaemonProcess hello user=ubuntu
WSGIScriptAlias /app /var/www/html/app/app.wsgi

本来はサイトごとに.confファイルを書くべきだが、今回はほかにブラウザーからアクセスするアプリがないので、直書きしました。

さて、ブラウザーで確認してみようかとすると、

早速、ここでエラーその1

RuntimeError: The 'apxs' command appears not to be installed or is not executable. Please check the list of prerequisites in the documentation for this package and install any missing Apache httpd server packages.

解決方法:

sudo apt install apache2-dev

無事helloworldが出力されたところで,本題に移る。

フロントは適当にtemplatesディレクトリにhtmlファイルを直書きで、Ajax通信で生成開始クリック時の処理をPythonに投げる

Ajax通信

function sleep(waitSec, callbackFunc) {
    // 経過時間(秒)
    var spanedSec = 0;
    // 1秒間隔で無名関数を実行
    var id = setInterval(function () {
        spanedSec++;
        // 経過時間 >= 待機時間の場合、待機終了。
        if (spanedSec >= waitSec) {
            // タイマー停止
            clearInterval(id);
            // 完了時、コールバック関数を実行
            if (callbackFunc) callbackFunc();
        }
    }, 1000);
}

var btn  = document.getElementById('create');
var loader = document.getElementById('loader');

btn.addEventListener('click', function() {
    btn.classList.add('hide');
    loader.classList.remove('hide');
    var post = $.ajax({
        url: '/app/generat',
        type: 'POST',
        data: JSON.stringify({'key': 'chemistry'}),
        async: false,
        contentType: "application/json"
    }).done(function(response){
        sleep(5, function () {
            console.log(response);
            var file_name = '/static/test_' + response + '.pdf';
            document.getElementById('result_pdf').src = file_name;
            loader.classList.add('hide');
            btn.classList.remove('hide');
        });
    }).fail(function(){
        alert('error!');
    }); 
});

flask本体

いい感じに生成する物質の物質量で割り切れるようにランダム数を操作する。
高校化学嫌いだったので今はもうほとんど覚えてない(なお卒業研究は材料系の研究をしていた模様)

Python app.py
import os
import fitz
import random
import datetime
from decimal import Decimal
from PyPDF2 import PdfFileWriter, PdfFileReader
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4, portrait
from reportlab.lib.units import inch, mm, cm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from flask import Flask, render_template
from flask import request

def random_num():
    while True:
        num = random.random()*10    
        if num > 1.1:
            num -= 1
            break

    return round(Decimal(num), 1)

app = Flask(__name__)

@app.route('/chemistry')
def index():
    return render_template('chemistry.html')

@app.route('/generat', methods=["POST"])
def generate():
    now = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=9)))
    dt_now = str(now.strftime('%Y%m%d%H%M%S'))
    content = []
    template_file     = '/var/www/html/static/template.pdf'
    tmp_file          = '/var/www/html/static/test_tmp.pdf'
    output_file       = '/var/www/html/static/test_'+ dt_now + '.pdf'
    ttc_file          = '/var/www/html/static/ipag.ttc'
    w, h              = portrait(A4)
    cv                = canvas.Canvas(tmp_file, pagesize=(w, h))
    pdfmetrics.registerFont(TTFont('ipag', ttc_file))
    cv.setFillColorRGB(0, 0, 0)

    for i in range(1, 11):
        sub_vol = random_num()
        vol = random_num() * Decimal('22.4')
        mass = random_num() * 71

        conn = ['6.0x10 個の', str(vol)+'Lの', str(mass)+'gの', str(sub_vol)+'molの']
        name = ['水素H2', '炭素C', '窒素N2', '酸素O2', 'フッ素F2', 'ナトリウムNa', 'マグネシウムMg', 'アルミニウムAl', '硫黄S', '塩素Cl', 'カリウムK', 'カルシウムCa', '鉛Pb', '銀Ag', 'ヨウ素I2', 'メタンCH4', 'プロパンC3H8', 'エタノールC2H5OH', '二酸化炭素CO2', '水H2O', 'アンモニアNH3', 'ヘリウムHe']
        cel_vol = [2, 12, 28, 32, 38, 23, 24, 27, 32, 71, 39, 40, 207, 108, 254, 16, 44, 46, 44, 18, 17, 4]
        ques = ['分子の数', '体積', '質量', '物質量']

        rdm_num_1 = random.randint(0, len(conn)-1)
        rdm_num_2 = random.randint(0, len(name)-1)
        rdm_num_3 = random.randint(0, len(ques)-1)

        while rdm_num_1 == rdm_num_3:
            rdm_num_1 = random.randint(0, len(conn)-1)
            rdm_num_3 = random.randint(0, len(ques)-1)
            
        content.append(conn[rdm_num_1] + name[rdm_num_2] + '' + ques[rdm_num_3])

いい感じの間隔で文字を配置する、これマジで職人技だと思う。

    for i in range(0, 10):
        font_size     = 12
        cv.setFont('ipag', font_size)
        cv.drawString(35*mm, h-(52.5+i*20)*mm, content[i])
        if '6.0x10' in content[i]:
            font_size = 6
            cv.setFont('ipag', font_size)
            cv.drawString(47.7*mm, h-(50.5+i*20)*mm, '23')

    cv.showPage()
    cv.save()

    template_pdf      = PdfFileReader(template_file)
    template_page     = template_pdf.getPage(0)
    tmp_pdf           = PdfFileReader(tmp_file)
    template_page.mergePage(tmp_pdf.getPage(0))
    output            = PdfFileWriter()
    output.addPage(template_page)
    with open(output_file, 'wb') as fp:
      output.write(fp)

    os.remove(tmp_file)
    return dt_now
    
if __name__ == '__main__':
    app.run()

修正を加えた後は

sudo systemctl restart apache2

を実行しないと変更が反映されない。

さあーてそろそろ動くだろうと思った矢先に

エラー続出!

エラーその2

apache2 wsgi ImportError: No module named 'xxx'

どうやら一部pathが違うところに通ってしまったライブラリがあるらしい、

sudo pip install xxx

または同じ仮想環境に入れるように同一しましょう。

/requests/init.py:89: RequestsDependencyWarning: urllib3 (x.x.x) or chardet (x.x.x) doesn't match a supported version!

https://teratail.com/questions/169266
バージョンを確認
https://pypi.org/project/chardet/#files
https://pypi.org/project/urllib3/#files

pip install chardet -U
pip install urllib3 -U
sudo pip install chardet -U
sudo pip install urllib3 -U

とりあえず最新にすることで解決できる。

flask and passenger "TypeError: 'module' object is not callable"

app.wsgiを見直しましょう、大抵どっちかが抜けてます

from app import app as application

これでとりあえず完成!

なんのデコレーションもない雑な感じだけどw
chemistry_result.PNG

あとはロードアニメメーションでもつけてわかりやすくして完成です、それではまた次回お会いしましょう~

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4