JSONをパースして可視化する(Python+Flaskでウェブアプリケーション⑤)

取り組みの全体像がこちら。
1. 環境の準備(OSセットアップ)
2. 環境の準備(OS内のセットアップ)
3. FlaskのQuickStartの内容をトレースする(インストールと最低限のセットアップ)
4. FlaskのTutrialの内容をトレースする(基本的なアプリケーションの作り方の学習)
5. オリジナルの内容を作る★

[前回](http://qiita.com/KI1208/items/f4541c7355dbe57e9d6d)までで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/

![20170309_004.jpg](https://qiita-image-store.s3.amazonaws.com/0/159980/b749bc5c-3f6b-b98c-e969-2668967297e0.jpeg)

可視化ならこれでいいのでは?というギモンもありますが、もっとExcel風な感じにしたい、というか最終的にはエクセルにしたい、という希望があります。

## イメージ
サンプルのJSONは映画の情報をまとめたファイルになります。
それを以下のような出力をイメージにしたいと思います。

| Title | Rating | Genre | Actors |
|:----:|:----:|:----:|:----:|:----:|
| Prisoners | 8.2 | Crime | Hugh Jackman |
|  |  | Drama | Jake Gyllenhaal |
|  |  | Thriller | Viola Davis |

Genre,Actorsの数は可変です。

# 環境
環境としては[前回](http://qiita.com/KI1208/items/f4541c7355dbe57e9d6d)まで使用したVMと同じVMを使用し、Pythonの仮想環境(virtualenv)だけ分割する。

# セットアップ
## ディレクトリ構成
以下のようなディレクトリ構成を使用する。

- jsonparser
    - db/
    - env/(virtualenvコマンドで作成される)
    - static/
        - js/
            - jquery-3.1.1.min.js
        - style.css
    - templates/
        - layout.html(これから作成)
        - inputfile.html(これから作成)
        - output.html(これから作成)
    - uploads/
    - app.py(これから作成)
    - config.py(これから作成)
    - parse_json.py(これから作成)

前回とほぼ同じ部分は省略します。
## virtualenv作成
省略

## configファイル作成
今回は変数を設定する部分を別のファイルに分けておく。

```py:config.py
# -*- 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、初期化のプロセスは前回同様なので省略。

```sqlite3:schema.sql
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に以下の関数を追加する。
アップロードされたファイルの拡張子を確認する関数。

```py
def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in app.config[
               'ALLOWED_EXTENSIONS']
```

次にファイルをアップロードする機能と、アップロードしたファイル(のエントリ)を削除する機能。

```py
@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)
```

```py
@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作成
大元になるテンプレート

```html:layout.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>
```

ファイルをアップロード、リスト、削除する画面。

```html:inputfile.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作成
スタイルシートはとりあえずチュートリアルのものをそのまま使用するので省略。

# ここまでの結果
ここまで来たら、アプリケーションを起動し、ブラウザからアクセスすると、
 
```shell-session
[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
```

![20170309_002.jpg](https://qiita-image-store.s3.amazonaws.com/0/159980/0f134af4-b29c-4c3d-70a5-97dfa442f5c5.jpeg)

ファイルをアップロードすると、
![20170309_007.jpg](https://qiita-image-store.s3.amazonaws.com/0/159980/b791825a-e9a0-946b-1ff8-daadc2acea5f.jpeg)

ここまではOK。

# パース、表示
キックするのは先ほど作成した、アップロードしたファイルの一覧を表示する画面になります。
「構成確認」というリンクを画面に追加し、対象のファイル名と一緒にoutputという関数に飛ばします。

```html:inputfile.html(一部)
  <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ファイルを読み込んで、パースするためのモジュールに渡し、帰ってきたオブジェクトをテンプレートに渡します。

```py:app.py(一部)
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)
```

次がパースするためのモジュールになります。

```py:parse_json.py
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という変数の計算をしているが、これは各映画に対して何行必要かを示す変数。

生成したオブジェクトを渡すテンプレートはこちら。

```html:output.html
{% 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

# 結果
以下のような出力になる。項目数などはともかく、一応イメージに近くはあります。
![20170310_003.jpg](https://qiita-image-store.s3.amazonaws.com/0/159980/ffcc68b6-e79c-ad92-0a99-29398ca4db86.jpeg)

本当はカラムごとのソートなどができるとよいが、いったんこれで。
全体のソースなどは以下で公開しております。
https://github.com/KI1208/jsonparser.git

# 補足、覚書
パッと見でも現状たくさん問題点や要望があるのですが、いったん目をつぶり、将来の課題とします。
- ファイルを表面上削除しているが実際には消していない
- 同じ名前のファイルの衝突を考慮していない
- ファイルをグルーピングできない(更新できない)
- 表示を動的に触りたい
- 別の種類のJSONについては都度書き換えが必要(汎用さが低い)。
等々