著者:石上 椋一
今回は、私が開発した文章校正用のWebアプリケーションについて紹介します。文章校正機能は「textlint」というNode.js上で稼働するアプリケーションを使って実現しているため、少ないコード量で実装できました。米Google社のアプリケーション開発プラットフォーム「Firebase」を利用したユーザー認証機能を付加する方法も解説します。
シェルスクリプトマガジン Vol.86は以下のリンク先でご購入できます。
図1 テンプレートファイル「index.html」に記述するコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<!doctype html> <html lang="ja"> <head> <title>文章校正アプリ</title> </head> <body> <h1>こんにちは!文章校正アプリです</h1> <h2>PDFファイルアップローダー</h2> <form action="/result" method="POST" enctype="multipart/form-data"> <input type=file name="pdf_file"> <button>ファイル送信</button> </form> </body> </html> |
図2 テンプレートファイル「result.html」に記述するコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<!doctype html> <html lang="ja"> <head> <title>文章校正アプリ 結果表示</title> </head> <body> <h1>こんにちは!文章校閲アプリです</h1> <h2>PDFファイルアップローダー</h2> <form action="/result" method="POST" enctype="multipart/form-data"> <input type=file name="pdf_file"> <button>ファイル送信</button> </form> <table border="1"> <thead> <tr> <th>何行目</th> <th>何文字目</th> <th>修正内容</th> </tr> </thead> <tbody> {% for result in result_table %} <tr> <td>{{result[0]}}</td> <td>{{result[1]}}</td> <td>{{result[2]}}</td> </tr> {% endfor %} </tbody> </table> <br> <p>修正した文章</p> <textarea rows="10" cols="80">{{result_text}}</textarea> </body> </html> |
図3 「web_app.py」ファイルに記述するコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
from flask import Flask, request, render_template, \ redirect, url_for, session import subprocess import re app = Flask(__name__) def upload_pdf_file(pdf_file): pdf_file.save("uploads/target.pdf") def run_pdf2text(): cmd = "pdftotext -nopgbrk uploads/target.pdf uploads/target.md" subprocess.run(cmd, shell=True, capture_output=True, text=True) def run_textlint_and_get_result_list(): cmd = "npx textlint uploads/target.md" result = subprocess.run(cmd, shell=True, capture_output=True, text=True) result_list = result.stdout.split("\n") result_table = [] for result in result_list[2:-4]: tmp_result = result.split(" error ") if len(tmp_result) >= 2: row = int(tmp_result[0].split(":")[0].strip()) colum = tmp_result[0].split(":")[1].strip() error_data = re.sub("ja-technical-writing/[a-z | -]*", "",tmp_result[1]).strip() result_table.append([row, colum, error_data]) return result_table def run_textlint_fix_data(): cmd = "npx textlint --fix ./uploads/target.md" subprocess.run(cmd, shell=True, capture_output=True, text=True) with open("./uploads/target.md", ) as md_file: data_lines = md_file.read() return data_lines @app.route("/index", methods=["GET"]) def index(): return render_template("index.html") @app.route("/", methods=["GET"]) def root(): return redirect(url_for("index")) @app.route("/result", methods=["GET", "POST"]) def result(): if request.method == "POST": pdf_file = request.files["pdf_file"] upload_pdf_file(pdf_file) run_pdf2text() result_table = run_textlint_and_get_result_list() result_text = run_textlint_fix_data() return render_template("result.html", result_table=result_table, result_text=result_text) return redirect(url_for('index')) if __name__ == "__main__": app.debug = True app.run(host='0.0.0.0', port=5000) |
図4 「.textlintrc.json」ファイルに記述する設定の例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
{ "rules": { "preset-ja-technical-writing": { "sentence-length": { "max": 100 }, "max-comma": { "max": 4 }, "max-ten": { "max": 4 }, "max-kanji-continuous-len": { "max": 7, "allow": [] }, "arabic-kanji-numbers": true, "no-mix-dearu-desumasu": { "preferInHeader": "", "preferInBody": "である", "preferInList": "である", "strict": true }, "ja-no-mixed-period": { "periodMark": "。" }, "no-double-negative-ja": true, "no-dropping-the-ra": true, "no-doubled-conjunctive-particle-ga": true, "no-doubled-conjunction": true, "no-doubled-joshi": { "min_interval": 1 }, "no-invalid-control-character": true, "no-zero-width-spaces": true, "no-exclamation-question-mark": true, "no-hankaku-kana": true, "ja-no-weak-phrase": true, "ja-no-successive-word": true, "ja-no-abusage": true, "ja-no-redundant-expression": true, "ja-unnatural-alphabet": true, "no-unmatched-pair": true } } } |
図10 「~/webapp/static/json/firebase.json」ファイルに記述する設定の例
1 2 3 4 5 6 7 8 9 |
{ "apiKey": "AIzaSyA1LQooSfvSda0jkBQl20ZfGR6lHOC5XBw", "authDomain": "test-1d03e.firebaseapp.com", "databaseURL": "https://test-1d03e-default-rtdb.firebaseio.com", "projectId": "test-1d03e", "storageBucket": "test-1d03e.appspot.com", "messagingSenderId": "919322583901", "appId": "1:919322583901:web:abb5d744e9ed04b8927eb4" } |
図11 テンプレートファイル「create_account.html」に記述するコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<!doctype html> <html lang="ja"> <head> <title>文章校正アプリ アカウント作成ページ</title> </head> <body> <form class="login-form" action='/create_account' method='POST'> <input type="text" name="email" placeholder="email address"/> <input type="password" name="password" placeholder="password"/> <button>Create an account</button> <p>{{msg}}</p> <p>Already registered? <a href="/login">Login</a> </p> </form> </body> </html> |
図12 テンプレートファイル「login.html」に記述するコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<!doctype html> <html lang="ja"> <head> <title>文章校正アプリ ログインページ</title> </head> <body> <form class="login-form" action="/login" method="POST"> <input type="text" name="email" placeholder="email address"/> <input type="password" name="password" placeholder="password"/> <button>Login</button> <p>{{msg}}</p> <p>Not registered? <a href="/create_account">Create an account</a> </p> </form> </body> </html> |
図13 「web_app.py」ファイルの「app = Flask(name)」行の前後に追加するコード
1 2 3 4 5 6 7 8 9 |
import os, json import pyrebase app = Flask(__name__) app.config["SECRET_KEY"] = os.urandom(24) with open("static/json/firebase.json") as f: firebaseConfig = json.loads(f.read()) firebase = pyrebase.initialize_app(firebaseConfig) auth = firebase.auth() |
図14 「web_app.py」ファイルの既存のルーティング設定にコードを追加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
(略) @app.route("/index", methods=["GET"]) def index(): if session.get("usr") == None: return redirect(url_for("login")) return render_template("index.html") @app.route("/", methods=["GET"]) def root(): return redirect(url_for("index")) @app.route("/result", methods=["GET", "POST"]) def result(): if session.get("usr") == None: return redirect(url_for("login")) if request.method == "POST": pdf_file = request.files["pdf_file"] (略) |
図15 「web_app.py」ファイルにルーティング設定を追加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
@app.route("/login", methods=["GET", "POST"]) def login(): if request.method == "GET": return render_template("login.html", msg="") email = request.form["email"] password = request.form["password"] try: auth.sign_in_with_email_and_password(email, password) session["usr"] = email return redirect(url_for("index")) except: message = "メールアドレスまたはパスワードが違います" return render_template("login.html", msg=message) @app.route("/create_account", methods=["GET", "POST"]) def create_account(): if request.method == "GET": return render_template("create_account.html", msg="") email = request.form["email"] password = request.form["password"] try: auth.create_user_with_email_and_password(email, password) session["usr"] = email return redirect(url_for("index")) except: message = "アカウントを作成できませんでした" return render_template("login.html", msg=message) @app.route('/logout') def logout(): del session["usr"] return redirect(url_for("login")) |