シェルスクリプトマガジン

香川大学SLPからお届け!(Vol.86掲載)

著者:石上 椋一

今回は、私が開発した文章校正用の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"))