ブラウザ上でPython実行環境を作ってみた(Flask + subprocess)

作ったもの

ブラウザ上で問題に合わせてプログラミングして、実行結果が正しいか判定するアプリケーションを作成しました。某サイトの影響を強く受けてます(笑)

なぜ作ったか?

私のチームメンバーにはPythonがメインのプログラミング言語ではない方もいるので、全体のレベルの把握や今後の教育に向けて、ブラウザ上から気軽に実行できる環境設定のいらない実行環境を用意しようと思い作成しました。実際のアプリ構成ではDockerを使用した構成にしていたり、ログを残す機能を搭載していますが、今回は最低限のポイントだけまとめようと思います。

開発環境

開発環境以下の通りです。

  • 動作環境:Ubuntu 20.04 (WSL)
  • Python:3.8
  • パッケージ管理:pipenv

主な使用要素

  • バックエンド
    • Flask
    • subprocess
  • フロントエンド(今回は説明は割愛します)
    • ace(エディタ部分)
    • axios(非同期通信 回答の正誤判定に使用)

実際のコード

アプリのメインとなる部分はPythonのwebフレームワークであるFlaskで作成しています。ルートにアクセスすると問題が表示され、回答を/execにpostすると用意されたinput.txt(入力値)とexpected_output.txt(期待出力)をもとに正誤判定を行う仕組みになっています。

app.py
import subprocess
from subprocess import PIPE, STDOUT
from flask import Flask, render_template, request, jsonify

QUESTION_DIR = "question/01/"


def create_app():
    app = Flask(__name__, template_folder="templates", static_folder="./static")

    @app.route("/", methods=["GET"])
    def root():
        question_text = load_question_text()
        return render_template("index.html", question_text=question_text)

    @app.route("/exec", methods=["POST"])
    def exec():
        # フロントエンドから送られてきたtextデータを取得
        text = request.form.get("text")
        # 期待する出力を読み込み
        with open(f"{QUESTION_DIR}expected_output.txt", "r") as f:
            expected_output = f.readline()
        # 入力されたtextデータを評価
        result = is_correct(expected_output, text)
        return jsonify({"result": result})

    def save_file(text: str) -> None:
        """テキストをpythonファイルとして保存する"""
        file_name = "temp.py"
        with open(file_name, "w") as f:
            f.write(text)

    def exec_file() -> str:
        """pythonファイルにinputファイルを渡して実行し、出力を返す"""
        cmd = f"python3 temp.py < {QUESTION_DIR}input.txt"
        result = subprocess.run(cmd, stdout=PIPE, stderr=STDOUT, shell=True, text=True)
        return result.stdout

    def is_correct(expected_output, input_text) -> bool:
        """期待する出力と入力されたpythonプログラムの出力が一致するかを判定"""
        save_file(text=input_text)
        answer_output = exec_file()
        return expected_output == answer_output

    def load_question_text() -> list:
        """問題文をリスト形式で読み込む"""
        with open(f"{QUESTION_DIR}question.txt", "r") as f:
            question_txt = f.readlines()
        return question_txt

    return app

ポイントは33~37行目の正誤判定の部分で、subprocessを使用してPythonファイルにパイプでinputファイルを渡すことで、ユーザーが作成したプログラム上のinput()に値を読み込ませています。

input.txt
3
1
ユーザーが作成したプログラム
N1 = input() # 3が読み込まれる
N2 = input() # 1が読み込まれる

複数のinput.txtとexpected_output.txtを用意するだけで同一のプログラムを複数テストケースで試すことが可能になります。

注意点

今回の構成ではプログラムの内容次第でサーバーの中身にアクセスすることができてしまいます。デプロイ時には実行をDockerのコンテナにするなどして、悪意のあるプログラムを実行されてもサーバーに影響のない構成にしてください。

参考

今回のアプリのソースコードをGitHub上に公開しておりますので、全体が見たい方はご覧ください。
https://github.com/shutils/python_sandbox

コメント

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