医療とテクノロジーの交差点にて【第6話】プログラミングで自作する実用アプリ(1)NEVER-NOTE
どうも、トア・ルドクターです。9月も終盤に差し掛かり夏の終わりを感じますね。最近、新潟で小畑健展が開催されたことに伴い『DEATH NOTE』の新作読み切りがジャンプ+で公開されました。『DEATH NOTE』は小畑健の華やかな作画も含め、個人的に好きな漫画ランキング7位くらいです。それはさておき、今回は『DEATH NOTE』ではなく『NeverNote』という自作アプリについての記事です。アプリの系統としてはEvernoteのようなメモアプリなのですが、掲示板のようにメモを付加していき、溜まったメモをタグ・検索ワード・日付に基づきand/or検索できるというものです。
この記事では『NeverNote』のソースコードをフル公開しますので、是非とも作って&使ってみてください。こうしたアプリ作成においても、『競技プログラミング』で培った知識・スキルが実装の技術的部分で非常に役立つことが実感できるでしょう。
それでは参りましょう!
NeverNoteとは
『NeverNote』は、サーバーサイド言語としてpython3を使用したウェブ・アプリケーションです。自作アプリなんて既存アプリには敵わないと考える方は多いでしょう。概ね否定しえない事実ですが、ひとつ自作アプリの利点を挙げるならば情報の「機密性」があります。自らのパソコンをホストマシンであると同時にサーバーマシンとして用いることで、ローカルだけで完結して作動させることができます(情報を自分のパソコン内だけに留めておける)。通常、一般的なアプリケーションはインターネットを介してサーバーマシン(Google / LINE / Instagram / Evernoteなどの企業が有するマシン)と通信をとり、サーバーマシンの中で「検索などの計算」「情報のストック」といった処理を行なっております。ネットにつながらない場所でググることができなかったり、新しいツイートを取得できないのは、そうした計算や読み込みがサーバーマシンに依存するからなのです。『NeverNote』は、自分のパソコン内でそうした処理を完結させるため、「ネットにつながずとも情報管理・情報検索ができる」「外部のパソコンと通信しないので、情報漏洩などのリスクがない」といったメリットがあります。
それでは、『NeverNote』がどんなアプリなのか簡単に説明しましょう。
これは情報を記録する画面です。フロントエンドのスキルが皆無なので、四半世紀前の掲示板みたいな仕様なのは悪しからず。@マークでタグ付けすることで情報の仕分けをしたり、画像アップロードしたりすることができます。
こちらが情報を検索する画面です。幾つか情報を記録したら、検索ワードやタグでand/or検索をすることができます。日付検索も可能です。さらに、画像メモのみを検索するimg検索、表示順を反転するreverse検索、アトランダムに表示するshuffle検索(テスト勉強などに有用?)なども実装しております。
このあとはアプリ作成について順々に説明していきます。
アプリの全体像(ファイル構造)
・nevernote
・index.html(フロントサイド)
・search.html(フロントサイド)
・stylesheet.css(フロントサイド)
・cgi-bin
・main.py(サーバーサイド)
・text
・english.txt
・quiz.txt
・test.txt
・image
ソースコード
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Nevernote</title>
<link href="stylesheet.css" rel="stylesheet" type="text/css"></link>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
</head>
<body>
<h2>Nevernote</h2>
<div class="link">
<input type="button" value="検索" onclick="window.open('search.html')">
</div>
<form action="http://localhost:5000/cgi-bin/main.py" method="post" target="frame">
<div class="form">
タグ:<input type="text" id="name" name="name" size=35 value="">
画像:
<input type="file" id="img" name="img_path" accept="image/*" style="display:none" onchange="if(this.value != '')
$('#check').prop('checked',true);">
<input type="button" value="ファイル選択" onclick="$('#img').click();">
<input type="checkbox" id="check" onclick="this.click()">
<input type="submit" id="submit" style="display:none">
<input type="button" value="実行" onclick="$('#submit').click(); $('#img').prop('value',null); $('#contents').prop('value',''); $('#check').prop('checked',false);">
<input type="button" value="リセット" onclick="$('#img').prop('value',null); $('#contents').prop('value',''); $('#check').prop('checked',false);">
</div>
<textarea id="contents" name="contents"></textarea>
<div class="file">
ノート:
<input type="radio" name="file" value="text/english.txt" checked="checked">英語
<input type="radio" name="file" value="text/quiz.txt">クイズ
<input type="radio" name="file" value="text/test.txt">資格試験
</div>
</form>
<iframe src="http://localhost:5000/cgi-bin/main.py" name="frame" height=700 width=700>
</iframe>
</body>
</html>
search.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>検索</title>
<link href="stylesheet.css" rel="stylesheet" type="text/css"></link>
<script src="script.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
</head>
<body>
<form action="http://localhost:5000/cgi-bin/main.py" method="post" name="Form">
<div class="file">
ノート:
<input type="radio" name="file" value="text/english.txt" checked="checked">英語
<input type="radio" name="file" value="text/quiz.txt">クイズ
<input type="radio" name="file" value="text/test.txt">資格試験
</div>
コマンド: all, img, rm, ¥, 消したい画像URL
<div class="search">
検索:<input type="text" name="search" size=30 value="">
<input type="radio" name="mode" value="and" checked="checked">and
<input type="radio" name="mode" value="or">or
<input type="checkbox" name="img" value=1>img
<input type="checkbox" name="reverse" value=1>reverse
<input type="checkbox" name="shuffle" value=1>shuffle
<input type="submit" value="実行" onclick="document.Form.target='frame';" >
<input type="button" value="リセット" onclick="document.getElementsByName('search')[0].value='';">
</div>
</form>
<iframe src="http://localhost:5000/cgi-bin/main.py" name="frame" height=700 width=700>
</iframe>
</body>
</html>
stylesheet.css
::selection {
background-color:pink;
}
body {
background-color:lightblue;
margin:20px;
}
h2 {
background-color:blue;
color:white;
border:solid white 1px;
width:135px;
height:20px;
padding:5px;
padding-bottom:20px;
margin:5px;
}
.link {
background-color:;
height:30px;
margin:5px;
}
.form {
background-color:;
height:30px;
margin:5px;
}
textarea {
background-color:white;
color:black;
font-size:14px;
height:170px;
width:600px;
margin:0px;
}
.search {
background-color:;
height:30px;
margin:5px;
}
.file {
background-color:;
height:30px;
margin:5px;
}
hr {
border:solid 1.5px blue;
}
a {
background-color:pink;
padding:2px;
}
img {
border:solid blue 2.5px;
}
iframe {
border:none;
}
main.py
#! /usr/bin/env python3
import datetime
import os
import random
#import cv2
import cgi ; form = cgi.FieldStorage()
import cgitb ; cgitb.enable()
print("Content-Type: text/html ;charset=utf-8") ; print()
# 日付の取得
today = datetime.datetime.today()
date = str(today.year)+"/"+str(today.month)+"/"+str(today.day)
# HTMLの入力フォームをpythonに受け渡す
LOG_FILE = form.getvalue("file","text/english.txt")
name = form.getvalue("name","").strip()
contents = form.getvalue("contents","").replace("\n","<br>").split()
contents = " ".join(contents)
img_path = form.getvalue("img_path","").strip()
search = form.getvalue("search","").strip()
mode = form.getvalue("mode","and")
img = form.getvalue("img",0)
reverse = form.getvalue("reverse",0)
shuffle = form.getvalue("shuffle",0)
# 画像操作(osモジュールの操作は相対パスだが、pathは/...と絶対パス)
# ローカルの画像をドキュメントルートに保存しなおす
if img_path != "" and name != "" and contents != "":
img_path = "/users/*/desktop/"+img_path
img_name = "image/img"+today.strftime("%Y%m%d%H%M%S")+".jpg"
os.rename(img_path, img_name)
img_path = "/"+img_name
# 画像削除コマンド
if "image/" in search:
img_name = "image/"+search.split("image/")[1]
os.remove(img_name)
search=""
# ログファイルを破壊的に変更する
# ログ上書き:aは追記。これをwにすると全部リセットされるからやばい。
with open(LOG_FILE, mode="a") as f:
if name != "" and contents != "":
names = name.split()
for i in range(len(names)): names[i] = "@"+names[i]
name = " ".join(names)
if img_path != "": f.write("【 "+name+" 】@"+date+"<br>"+contents+"<br><img src='"+img_path+"'>"+"\n")
else: f.write("【 "+name+" 】@"+date+"<br>"+contents+"\n")
# removeコマンド:これがaだとどんどん増えちゃう
if "rm" in search:
try:
search = int(search.replace("rm","").strip())-1
if search<0: search=""
with open(LOG_FILE, mode="r")as f:
lines = f.readlines(); lines.pop(search)
with open(LOG_FILE, mode="w")as f:
f.writelines(lines)
except: pass
if type(search)==int: search=""
# ¥¥コマンド:¥を全て削除
if "¥¥" in search:
with open(LOG_FILE,mode="r")as f: lines=f.readlines()
with open(LOG_FILE,mode="w")as f:
ans=[]
for line in lines: ans.append(line.replace("¥",""))
f.writelines(ans)
search=""
# ¥コマンド:これがaだとどんどん増えちゃう
if "¥" in search:
try:
search = int(search.replace("¥","").strip())-1
with open(LOG_FILE,mode="r")as f:
lines=f.readlines(); lines[search]="¥"+lines[search]
with open(LOG_FILE,mode="w")as f: f.writelines(lines)
except: pass
if type(search)==int: search="¥"
# HTML定型文
head = """
<meta charset="UTF-8">
<title>Nevernote</title>
<link href="/stylesheet.css" rel="stylesheet" type="text/css"></link>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
""".format(LOG_FILE)
body = """
<body name="main" style="background:skyblue ; font-size:15"></body>
"""
# ここから検索アルゴリズム
# index付加したlinesリストを用意
with open(LOG_FILE, mode="r", encoding="utf-8") as f:
lines=[]; i=1
for line in f: line = "@"+str("{0:03d}".format(i))+" "+line; lines.append(line); i+=1
lines.reverse()
# 出力用のanssリストを用意
anss=[]
# 空白:直近10表示
if search=="": anss=lines[:10]
# allコマンド:全表示
elif "all" in search: anss=lines
# and検索
elif mode=="and":
searches = search.split() # 空白区切でsearchesリストに格納
for line in lines:
flag = True
for search in searches:
if search in line: line = line.replace(search,"<a>"+search+"</a>")
else: flag = False ; break
if flag: anss.append(line);
# or検索:imgコマンド対応
elif mode=="or":
searches = search.split() # 空白区切でsearchesリストに格納
for line in lines:
flag = False
for search in searches:
if search in line: line = line.replace(search,"<a>"+search+"</a>") ; flag = True
if flag: anss.append(line);
# img
if img: anss = [ans for ans in anss if "img" in ans]
# shuffle
if shuffle: random.shuffle(anss); anss = anss[:10]
# reverse
elif reverse: anss.reverse()
# 出力
print(head,body)
total=len(lines); hit=len(anss);
print("ヒット数:"+str(hit)+" / "+str(total))
cnt = 1
for ans in anss: print("<hr>","<a>"+str(cnt)+"/"+str(hit)+"</a>",ans); cnt += 1
解説
上記のソースコードをコピペするだけでもアプリは完成できるでしょう。しかしながら、きちんと中身を理解していないと、アプリ仕様を改変したり機能追加したりなど応用が効きません。そこで幾つかのコードについて解説を付加しておきます。
動かし方(Macbookの場合)
ドキュメントルート(nevernoteフォルダ入ってすぐの階層)に移動して次のコマンドを入力すると、startという実行ファイルが作成され、実行ファイルの中身を編集できます。
touch start
chmod +x start
open -e start
実行ファイルの中身には複数のコマンド処理を格納できるので、以下の通りに記述し保存します。
open index.html
python3 -m http.server --cgi 5000
あとはstartという実行ファイルを実行すればウェブアプリが起動します。
なお、Windowsの場合は知りません。笑
それではまたいつか!
【著者プロフィール】
都内で医師として研鑽する傍ら、独学でプログラミングを学ぶ26歳。趣味は『ギター / バイオリン / 美術鑑賞 / youtube鑑賞 / 創作料理 / 囲碁 / チェス / 折り紙 / スノボ / サーフィン / ドライブ』など枚挙にいとまがない。CIAの格闘武術クラブマガを始める。得意料理はバナナシチュー。ビールと牡蠣は生派だが生セックスは断固せず、経験人数の常用対数は2未満と清純を極める。略歴としては高2で数学全国1位(駿台)、文系で官僚をめざすも、ドラマ『コードブルー』の影響から気づいたら医師に。ディープラーニングG検定、統計検定2級、知的財産検定3級など取得。TOEICは次回900目指す予定(仮)。
【記事アーカイブ】
【第1話】医者なのにプログラミングを勉強してみた話
【第2話】pythonプログラミングの小技(1)ラムダ
【第3話】プログラミング初心者が学ぶべき3つのポイント
【第4話】競技プログラミングのススメ
【第5話】競技プログラミング物語(1)バイトリーダーの苦悩
【第6話】プログラミングで自作する実用アプリ(1)NEVER-NOTE
【第7話】プログラミングで解析したDNA鑑定の精度
【第8話】統計学は最強の学問であるのか?
【第9話】プログラミングすれば人類最高IQに対抗できる説
この記事が気に入ったらサポートをしてみませんか?