取り組みの全体像がこちら。
1. 環境の準備(OSセットアップ)
2. 環境の準備(OS内のセットアップ)
3. FlaskのQuickStartの内容をトレースする(インストールと最低限のセットアップ)
4. FlaskのTutrialの内容をトレースする(基本的なアプリケーションの作り方の学習)
5. オリジナルの内容を作る★
前回までで4.まで終わったので、元々の目的であるオリジナルをやります。。
作成するのは、特定の構成を想定したJSONファイルを読み込んで、内容を可視化するアプリケーション。
仕事上JSONで構成ファイルを出力できる製品がいくつかあるので、それをパッと可視化(最終的にはエクセル出力)できたら便利だな、というのが動機。
作らなくても存在する気もするのですが、練習として作ってみます。
要件
- JSONファイルをアップロードする仕組みをもつ
- アップロードされたJSONファイルをパースして内容(の一部)を可視化する
これを満たすようなアプリケーションを作ってみる。本当は実際に使用したいjsonファイルを使ったほうがイメージしやすいが、社外に出せない情報が含まれているので、代わりにインターネット上で公開されているものを使用する。
http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/gettingstartedguide/samples/moviedata.zip
映画のタイトルやレーティング、ジャンルなどが記録されたJSONファイル。
JSONファイルの内容確認
可視化するにしても、まずはどういった構造なのかを把握する必要があります(と思っています…)。
プロの皆さんがどうするのかわかりませんが、素人としてはChromeの拡張機能のコレを使っています。
http://jsoneditoronline.org/
可視化ならこれでいいのでは?というギモンもありますが、もっとExcel風な感じにしたい、というか最終的にはエクセルにしたい、という希望があります。
イメージ
サンプルのJSONは映画の情報をまとめたファイルになります。
それを以下のような出力をイメージにしたいと思います。
Title | Rating | Genre | Actors |
---|---|---|---|
Prisoners | 8.2 | Crime | Hugh Jackman |
Drama | Jake Gyllenhaal | ||
Thriller | Viola Davis |
Genre,Actorsの数は可変です。
環境
環境としては前回まで使用したVMと同じVMを使用し、Pythonの仮想環境(virtualenv)だけ分割する。
セットアップ
ディレクトリ構成
以下のようなディレクトリ構成を使用する。
- jsonparser
- db/
- env/(virtualenvコマンドで作成される)
- static/
- js/
- jquery-3.1.1.min.js
- style.css
- js/
- templates/
- layout.html(これから作成)
- inputfile.html(これから作成)
- output.html(これから作成)
- uploads/
- app.py(これから作成)
- config.py(これから作成)
- parse_json.py(これから作成)
前回とほぼ同じ部分は省略します。
virtualenv作成
省略
configファイル作成
今回は変数を設定する部分を別のファイルに分けておく。
# -*- coding: utf-8 -*-
# configuration
DATABASE = '/root/jsonparser/db/jsonparser.db'
DEBUG = True
UPLOAD_FOLDER = '/root/jsonparser/uploads/'
ALLOWED_EXTENSIONS = set(['json'])
SECRET_KEY = 'development key'
ここで設定した変数は、このファイルをインポートしたpyファイルから、以下の手順でアクセスできる。
1. Flaskのインスタンスを作成
app = Flask(name)
2. ファイルから変数を読み込み
app.config.from_pyfile('config.py')
3. 変数UPLOAD_FOLDERの内容を変数tempに代入
temp = app.config['UPLOAD_FOLDER']
ファイル情報を格納するデータベースを作成
以下のようなSQL文を作成し、前回同様app.pyから呼び出してデータベースを初期化する。この段階のapp.py、初期化のプロセスは前回同様なので省略。
drop table if exists file_entries;
create table file_entries (
id integer primary key autoincrement,
filename string not null,
desc string,
created string
);
本体の作成
View関数
app.pyに以下の関数を追加する。
アップロードされたファイルの拡張子を確認する関数。
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in app.config[
'ALLOWED_EXTENSIONS']
次にファイルをアップロードする機能と、アップロードしたファイル(のエントリ)を削除する機能。
@app.route('/', methods=['GET', 'POST'])
def inputfile():
cur = g.db.execute(
'select filename,desc,created,id from file_entries order by created desc')
entries = [dict(filename=row[0], desc=row[1], created=row[
2], id=row[3]) for row in cur.fetchall()]
if request.method == 'POST':
# check if the post request has the file part
if 'file' not in request.files:
flash('No file part')
return redirect(url_for('inputfile'))
file = request.files['file']
# if user does not select file, browser also
# submit a empty part without filename
if file.filename == '':
flash('No selected file')
return redirect(url_for('inputfile'))
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
current = datetime.now()
g.db.execute('insert into file_entries (filename,desc,created) values (?, ?, ?)',
[file.filename, request.form['desc'], current])
g.db.commit()
message = "File upload finished successfully."
return redirect(url_for('inputfile', message=message))
current = datetime.now().strftime('%Y/%m/%d %H:%M')
message = request.args.get('message', '')
if not message:
message = "Current time is " + current
return render_template('inputfile.html', message=message, entries=entries)
@app.route('/delete', methods=['GET', 'POST'])
def delete():
id = request.args.get('value')
g.db.execute("delete from file_entries where id = ?", [id])
g.db.commit()
return redirect(url_for('inputfile'))
HTML作成
大元になるテンプレート
<!doctype html>
<html>
<head>
<title>JSON Parser</title>
<script type="text/javascript" src="../static/js/jquery-3.1.1.min.js"></script>
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
{% block head %}{% endblock %}
</head>
<div class=page>
<h1>JSON Parser</h1>
<div class=metanav>
<a href="{{ url_for('inputfile') }}">Home</a>
</div>
{% for message in get_flashed_messages() %}
<div class=flash>{{ message }}</div>
{% endfor %}
{% block body %}{% endblock %}
</div>
</html>
ファイルをアップロード、リスト、削除する画面。
{% extends "layout.html" %}
{% block body %}
<p>{{ message }} </p>
<form action="{{ url_for('inputfile') }}" method=post class=add-entry enctype="multipart/form-data">
File: <input type="file" name="file" size="30"/><br>
Description: <input type="text" name="desc" size="30" placeholder="Description"/><br>
<input type="submit" />
</form>
<ul class=entries>
{% for entry in entries %}
<li><h2>{{ entry.filename }}</h2>
<p><a href="{{url_for('delete',value=entry.id)}}">削除</a></p>
Description: {{ entry.desc }}<br>
Created: {{ entry.created }}<br>
{% else %}
<li><em>まだエントリがありません。</em>
{% endfor %}
</ul>
{% endblock %}
form要素のactionからファイルをアップロード。url_forをつかって、app.py内のinputfile関数(に対応するURL)に飛ばす。
CSS作成
スタイルシートはとりあえずチュートリアルのものをそのまま使用するので省略。
ここまでの結果
ここまで来たら、アプリケーションを起動し、ブラウザからアクセスすると、
[root@cnenarnupgd1c jsonparser]# . env/bin/activate
(env) [root@cnenarnupgd1c jsonparser]#
(env) [root@cnenarnupgd1c jsonparser]# python app.py
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger pin code: 173-472-212
ここまではOK。
パース、表示
キックするのは先ほど作成した、アップロードしたファイルの一覧を表示する画面になります。
「構成確認」というリンクを画面に追加し、対象のファイル名と一緒にoutputという関数に飛ばします。
<ul class=entries>
{% for entry in entries %}
<li><h2>{{ entry.filename }}</h2>
<p><a href="{{url_for('output',value=entry.filename)}}">構成確認</a></p>
<p><a href="{{url_for('delete',value=entry.id)}}">削除</a></p>
Description: {{ entry.desc }}<br>
Created: {{ entry.created }}<br>
{% else %}
<li><em>まだエントリがありません。</em>
{% endfor %}
</ul>
受ける関数は下記のようにします。valueとして渡されたファイル名を元に、jsonファイルを読み込んで、パースするためのモジュールに渡し、帰ってきたオブジェクトをテンプレートに渡します。
from parse_json import parse_json
@app.route('/output')
def output():
fname = request.args.get('value').replace(' ','_')
fpath = app.config['UPLOAD_FOLDER'] + fname
jsonfile = open(fpath,'r')
config = json.load(jsonfile)
output = parse_json(config)
return render_template('output.html', entries=output)
次がパースするためのモジュールになります。
import sys
def parse_json(config):
# Initialize array
# Create (num of movie) * empty {}
arr = []
for i in range(len(config)):
try:
arr[i].append({})
except IndexError:
arr.append({})
except:
print 'Unexpected Error:',sys.exc_info()[0]
# Populate arr
for idx,movie in enumerate(config):
try:
arr[idx].update({'title': movie.get('title')})
arr[idx].update({'rank': movie.get('info').get('rank')})
arr[idx].update({'genres': movie.get('info').get('genres')})
arr[idx].update({'actors': movie.get('info').get('actors')})
if movie.get('info').get('genres') and movie.get('info').get('actors'):
arr[idx].update({'rowspan': max(len(movie.get('info').get('genres')),len(movie.get('info').get('actors')))})
elif not movie.get('info').get('genres') and movie.get('info').get('actors'):
arr[idx].update({'rowspan': len(movie.get('info').get('actors'))})
elif movie.get('info').get('genres') and not movie.get('info').get('actors'):
arr[idx].update({'rowspan': len(movie.get('info').get('genres'))})
else:
arr[idx].update({'rowspan': 1})
except:
print 'Unexpected Error:', sys.exc_info()[0]
pass
return arr
正直な話、このレベルではFlaskのコード(HTML上)で処理もできるような気がするが、後々のことを考えてこの形に。また途中でrowspanという変数の計算をしているが、これは各映画に対して何行必要かを示す変数。
生成したオブジェクトを渡すテンプレートはこちら。
{% extends "layout.html" %}
<h1>Movies</h1>
{% block body %}
<table ~~~ style="table-layout:fixed;width:100%;" border = "3">
<colgroup>
<col style="width:25%;">
<col style="width:25%;">
<col style="width:25%;">
<col style="width:25%;">
</colgroup>
<tr bgcolor="skyblue">
<td ~~~ style="word-wrap:break-word;">Title</td>
<td ~~~ style="word-wrap:break-word;">Rank</td>
<td ~~~ style="word-wrap:break-word;">Genres</td>
<td ~~~ style="word-wrap:break-word;">Actors</td>
</tr>
{% for entry in entries %}
{% for i in range(entry.rowspan) %}
<tr>
{% if i == 0 %}
<td ~~~ style="word-wrap:break-word;" rowspan = "{{ entry.rowspan }}">{{ entry.title }}</td>
<td ~~~ style="word-wrap:break-word;" rowspan = "{{ entry.rowspan }}">{{ entry.rank }}</td>
{% endif %}
{% if entry.genres[i] %}
<td ~~~ style="word-wrap:break-word;">{{ entry.genres[i] }}</td>
{% else %}
<td ~~~ style="word-wrap:break-word;" bgcolor="black">Empty</td>
{% endif %}
{% if entry.actors[i] %}
<td ~~~ style="word-wrap:break-word;">{{ entry.actors[i] }}</td>
{% else %}
<td ~~~ style="word-wrap:break-word;" bgcolor="black">Empty</td>
{% endif %}
</tr>
{% endfor %}
{% else %}
<tr><td ~~~ style="word-wrap:break-word;" columnspan="4">No entry here.</td></tr>
{% endfor %}
</table>
{% endblock %}
4行必要なエントリに関しては、タイトルやRankは4行まとめて表示させる。
余ったセルは塗りつぶし。
またセル幅を固定にして、長い文字列は折り返すようにしている。
そこは以下を参考にしています。
http://qiita.com/n_s_y_m/items/cb29d730e63772b02475
結果
以下のような出力になる。項目数などはともかく、一応イメージに近くはあります。
本当はカラムごとのソートなどができるとよいが、いったんこれで。
全体のソースなどは以下で公開しております。
https://github.com/KI1208/jsonparser.git
補足、覚書
パッと見でも現状たくさん問題点や要望があるのですが、いったん目をつぶり、将来の課題とします。
- ファイルを表面上削除しているが実際には消していない
- 同じ名前のファイルの衝突を考慮していない
- ファイルをグルーピングできない(更新できない)
- 表示を動的に触りたい
- 別の種類のJSONについては都度書き換えが必要(汎用さが低い)。
等々