著者:石上 椋一
今回は、私が開発した文章校正用のWebアプリケーションについて紹介します。文章校正機能は「textlint」というNode.js上で稼働するアプリケーションを使って実現しているため、少ないコード量で実装できました。米Google社のアプリケーション開発プラットフォーム「Firebase」を利用したユーザー認証機能を付加する方法も解説します。
シェルスクリプトマガジン Vol.86は以下のリンク先でご購入できます。


図1 テンプレートファイル「index.html」に記述するコード
<!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」に記述するコード
<!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」ファイルに記述するコード
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」ファイルに記述する設定の例
{
"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」ファイルに記述する設定の例
{
"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」に記述するコード
<!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」に記述するコード
<!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)」行の前後に追加するコード
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」ファイルの既存のルーティング設定にコードを追加
(略)
@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」ファイルにルーティング設定を追加
@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"))