LoginSignup
6
12

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-03-10

取り組みの全体像がこちら。
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/

20170309_004.jpg

可視化ならこれでいいのでは?というギモンもありますが、もっと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
    • templates/
      • layout.html(これから作成)
      • inputfile.html(これから作成)
      • output.html(これから作成)
    • uploads/
    • app.py(これから作成)
    • config.py(これから作成)
    • parse_json.py(これから作成)

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

virtualenv作成

省略

configファイル作成

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

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、初期化のプロセスは前回同様なので省略。

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

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作成

大元になるテンプレート

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>

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

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作成

スタイルシートはとりあえずチュートリアルのものをそのまま使用するので省略。

ここまでの結果

ここまで来たら、アプリケーションを起動し、ブラウザからアクセスすると、

[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

ファイルをアップロードすると、
20170309_007.jpg

ここまではOK。

パース、表示

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

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

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)

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

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

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

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://github.com/KI1208/jsonparser.git

補足、覚書

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

6
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
12