ハッカソンで議論向けWeb掲示板アプリを作った
はじめに
大学の有志とチームを組み、日本最大規模の学生対象ハッカソンである(らしい)JPHACKS 2021に参加しました。この記事はその参加記となります。
作ったもの
賛成・反対・中立の立場を明確に示すことで、より正確な議論の理解を促進することを目的とした議論アプリです。アプリ名は「優しい議論」より「Yasaron」としました。UIはスマホ向けに特化させています。
賛成・反対・中立の立場を確定させることで、初めて本文を入力できるようになります。
類似している(と判定された)意見が自動的にまとめられるため、少数派の意見が多数派の意見と対等に扱われ、不要なコメントに埋もれることなく比較検討がしやすくなっています。これにより見やすい環境下で平等なディスカッションができます。
リポジトリはこちらです。
開発の流れ
Discordでチャット・通話・画面共有をしながら開発を進めました。技術は主に担当したバックエンドについて書きます。
スケジュールは以下のリンク先を参照してください。
初日(Kick Off Day~中間発表)
内輪チームでの参加であり、ハッカソンにエントリーした時点でアイデア出しをしていたため、スタートダッシュを決めて初日から動くものを作ることができました。
技術選定
- デバイス
- スマホ特化のWebアプリ[1]
- フロントエンド
- HTML
- CSS
- JavaScript
- バックエンド
- Python
- Flask
- Flask-SQLAlchemy
- SQLite
- API・データ
予め出しておいたアイデアのうち、スポンサー提供物の自然言語処理APIをうまく活用できそうな案を採用しました。スマホ特化のWebアプリで掲示板(SNS?)的なものを作るという方向性が決まったので次は技術選定です。
出来るだけメンバー全員が開発に携われるようにということで、バックエンドで使う言語はメンバー全員が書けるPythonにしました。またPythonのWebアプリケーションフレームワークとしてはDjangoが有名ですが、多機能故に複雑で覚えることが多いため、Webアプリ未経験者がいることを踏まえてシンプルで軽量なフレームワークであるFlaskを使うことにしました。
GitHub上のリポジトリの運用
GitHubでのチーム開発はまずmasterへの直接pushを禁止し、各自でブランチを切り、更新したらPull Requestを投げて、別のメンバーが承認したらmasterで取り込むという方式で開発を進めました。
Wiki
メンバー向けにGitHubのWikiに環境構築・実行方法・URL・API設計などを書きました。
最初のコミット
コメントの書き込みと表示
このあたりのコミットでとりあえずコメントの書き込みとデータベースへの登録ができるようになりました。
データベースの操作にはORMライブラリであるFlask-SQLAlchemyを使用しました。SQL文が書けなくても簡単にデータベースを利用することができます。
from flask import Flask, render_template, request
from flask_sqlalchemy import SQLAlchemy
import json
import random
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///data.sqlite"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
class Comment(db.Model):
__tablename__ = "comments"
comment_id = db.Column(db.String, primary_key=True) # コメントID
parent_comment_id = db.Column(db.String, nullable=False) # 親のコメントID
title = db.Column(db.String, nullable=False) # タイトル
text = db.Column(db.String, nullable=False) # 本文
attribute = db.Column(db.Integer, nullable=False) # 賛成 or 反対 or 中立
@app.route("/")
def index():
return render_template("index.html")
@app.route("/comment", methods=["POST"])
def post_comment():
comment = json.loads(request.data)
new_comment = Comment(
comment_id=str(random.randint(1, 100000)), ## ID生成は仮なのて適当
parent_comment_id=comment["parent_comment_id"],
title=comment["title"],
text=comment["text"],
attribute=comment["attribute"],
)
db.session.add(new_comment)
db.session.commit()
return comment["title"] # 何かをreturnしないと500 Internal Server Errorが返ってくるので仮で
@app.route("/comment/<comment_id>", methods=["GET"])
def get_comment(comment_id):
comment = Comment.query.filter_by(comment_id=comment_id).first()
return render_template(
"comment.html",
comment_id=comment.comment_id,
parent_comment_id=comment.parent_comment_id,
title=comment.title,
text=comment.text,
attribute=comment.attribute,
)
@app.route("/comments", methods=["GET"])
def get_comment_all():
return render_template(
"comments.html",
comments=Comment.query.order_by(Comment.comment_id.desc()).all(),
)
## CLI用 DB初期化
@app.cli.command("init-db")
def init_db():
db.create_all()
if __name__ == "__main__":
app.run()
バックエンド側でコメント書き込み時にコメントIDを返すようにして初日は終了です。jsonify
にlist型やdict型を渡してreturn
するとheaderにContent-Type: application/json
を付けていい感じに返してくれます。便利ですね。
2日目
トップページにCSSが効いて綺麗になりました。バックエンドの進捗はなしです。
Project
GitHubにはProjectというTrelloのようにカンバン式でタスク管理ができる機能があります。今まで使ったことがなかったためこの時点で触ってみました。Issueをカンバン式で管理できるので便利です。
3日目
進捗なし。
4日目
コメントの返信と子コメント表示
子コメントの表示ができるようになりました。
gooラボAPIを試す
テキストペア類似度APIで類似した意見をまとめたいのと、キーワード抽出APIでタグ検索的なものを実装したい構想があったため、この時点でAPIを試してみました。
# 省略
# 環境変数
from dotenv import load_dotenv
load_dotenv()
# 省略
# キーワード抽出
def get_keywords():
item_data = {
"app_id": os.environ["GOO_LAB_APP_ID"], # ここにgooラボAPIのアプリケーションID
"title": "", # タイトル
"body": "", # 本文
"max_num": 10,
}
try:
response = requests.post("https://labs.goo.ne.jp/api/keyword", json=item_data)
response.raise_for_status()
except requests.exceptions.RequestException as e: # エラーの場合のみ
print(e)
else: # 正常に処理された場合のみ
response_json = json.loads(response.text)
print(response_json)
finally: # 常に実行
pass
# テキストペア類似度
def get_textpair():
item_data = {
"app_id": os.environ["GOO_LAB_APP_ID"], # ここにgooラボAPIのアプリケーションID
"text1": "", # ここに比較するテキスト1
"text2": "", # ここに比較するテキスト2
}
try:
response = requests.post("https://labs.goo.ne.jp/api/textpair", json=item_data)
response.raise_for_status()
except requests.exceptions.RequestException as e: # エラーの場合のみ
print(e)
else: # 正常に処理された場合のみ
response_json = json.loads(response.text)
print(response_json)
finally: # 常に実行
pass
# 省略
適当なブログやヤフコメなどで試して、想定通りに動くことを確認できたところで4日目は終了です。
5日目
存在しないコメントへのアクセスに404を返す
今までは存在しないコメントへのアクセスがあると500 Internal Server Errorを吐いていましたが、ちゃんと404 Not Foundを返すようになりました。
コメントの書き込み日時の記録
コメントの書き込み日時が記録されるようになりました。本来SQLiteにはDateTime型がないため文字列に変換する必要がありますが、SQLAlchemyがDateTime型とString型の変換をいい感じにやってくれるため、何も気にすることなくDateTime型を扱うことができます。
フロントエンド側の日付表示がいい感じになった所で5日目は終了です。
6日目
デザインが良い感じになりました。バックエンドの進捗はなしです。
7日目
コメントID生成
ここで今までstr(random.randint(1, 100000))
で誤魔化していた部分をちゃんと実装しました。
コメントID生成アルゴリズムの案としては、連番ID, UUID, 連番IDや現在時刻をmd5などでハッシュ化したものがありましたが、どれもIDの見た目が安易だったり英数字混じりで汚いということで最終的にSnowflakeというアルゴリズムを採用することにしました。
SnowflakeはTwitterで使われていた(今も?)ID生成アルゴリズムです。Twitterのツイートの個別ページの例で説明するとhttps://twitter.com/<screen_name>/status/<ここがSnowflake>
です。
詳しい説明は省くので以下の記事をご覧ください。
感情分析アダプター
感情分析アダプターの動作確認ができました。追加学習のことなどを考えて感情分析アダプターは掲示板本体とは分離させてAPIとして利用する方針にしました。
ただWindows環境で動かそうとした所、長時間ハマってしまったため[2]、実装は一旦保留としました。
8日目(Pitch Day)
キーワードと類似度
4日目に試していたgooラボAPIを掲示板本体に組み込みました。
トップページの新着コメント表示
トップページの新着コメント表示とページ遷移を実装しました。
# 省略
# index.htmlの新着親コメント表示数
DISPLAY_COMMENT_COUNT = 5
# 省略
@app.route("/")
def index():
now_page = int(request.args.get("p")) if request.args.get("p") is not None else 1
is_last_page = Comment.query.filter_by(parent_comment_id="0").count() <= now_page * DISPLAY_COMMENT_COUNT
return render_template(
"index.html",
comments=Comment.query.filter_by(parent_comment_id="0")
.order_by(Comment.datetime.desc())
.offset(DISPLAY_COMMENT_COUNT * (now_page - 1))
.limit(DISPLAY_COMMENT_COUNT)
.all(),
now_page=now_page,
is_last_page=is_last_page,
)
return render_template("index.html")
# 省略
キーワード検索
特定のキーワードを含むコメント一覧を取得できるようになりました。
悪意のある長文コメントを弾く
文字数が異常に多いコメントがPOSTされたら400 Bad Requestを返すようにしました。
日本時間にする
本番環境でテストした所、時間がグリニッジ標準時になってしまっていたため修正しました。
Code Freeze
見た目を整えてアイコンやロゴを作ったりREADME.md
を書いたりしていたら時間切れ(Code Freeze)となりました。
ロゴ
おわりに
ログイン認証, 感情分析, Vote, PWA, 画像やMarkdown対応など実装が間に合わなかった機能が多かったのが心残りです。スケジュール管理やアプリのプロトタイプをアイデア出しの段階で作っておくなどの前準備をもっとしっかりするべきでした。また発表会を通して他の学生のレベルの高さを知ることができたのは貴重な経験になったと思います。Award Day進出とならなかったことは悔しいですが、参加してよかったです。
今回のハッカソンではフロントエンドはピュアなHTML, CSS, JavaScriptを、バックエンドはPython, Flaskをメインで使用しましたが、フロントエンドではReact, Vue, SvelteなどのようなSPAフレームワークが、バックエンドでは(Pythonにおいては)FastAPIが最新の主流のようなので次はこれらを使った開発にも挑戦したいです。
Discussion