Vue.jsからPythonにリクエストを飛ばしてエクセル書き込み

  • 2020.11.28
  • 2021.01.24
  • Flask
Vue.jsからPythonにリクエストを飛ばしてエクセル書き込み

この記事からの続きとなります。
まだご覧になっていない方はどうぞ。

エクセルに書き込むメソッドの作成

Vue側から飛ばすデータの内容は決まっているので(表示しているデータ)
それを元にエクセルに書き込むメソッドを作成します。

main.pyと同じ配下にfunction.pyのファイルを作成します。
下記書き込んでください。

足りないライブラリは各自pip installして下さい

import sys
import pathlib
from flask.globals import request
import openpyxl

path = './excel_data.xlsx'

class Func(object):
  @classmethod
  def create(self, excel_path=path):
    wb = openpyxl.Workbook(pathlib.Path(excel_path))
    wb.save(pathlib.Path(excel_path))

  @classmethod
  def write(self, data, excel_path=path):
    if not data:
      return None
    wb, ws = Func.check(data)
    row_count = ws.max_row + 1
    try:
      for d in data:
        column = 1
        for v in d.values():
          ws.cell(row=row_count, column=column).value = v
          column = column + 1
        row_count = row_count + 1
      wb.save(pathlib.Path(excel_path))
    except Exception as e:
      print(e)
      sys.exit()
    return data

  @classmethod
  def check(self, data_list, excel_path=path):
    try:
      wb = openpyxl.load_workbook(pathlib.Path(excel_path))
      ws = wb.active
    except FileNotFoundError as e:
      Func.create(excel_path)
      try:
        wb = openpyxl.load_workbook(pathlib.Path(excel_path))
        ws = wb.active
        Func.column(data_list[0], wb, ws)
      except FileExistsError as e:
        print(e)
        sys.exit()
    return wb, ws
  
  @classmethod
  def column(self, data, wb, ws, excel_path=path):
    try:
      i =  1
      for k in data.keys():
        ws.cell(row=1, column=i).value = k
        i = i + 1
      wb.save(pathlib.Path(excel_path))
    except Exception as e:
      print(e)
      sys.exit()

Vue側からリクエストを投げる

次はindex.html側の編集をします。

ボタンの追加とメソッドを追加します。
前回記事とダブっているコードは、
なんとなくどこに書いているか分かるレベルで記述を省いています。

<body>
<div id="sample_app">
 <div>
  [[ msg ]]
 </div>

 <h1>BOSTON DF</h1>
  
    <form v-on:submit.prevent="writeExcel('/excel')" class="form-inline" method="POST" >
      <button class="btn btn-outline-success my-2 my-sm-0">EXCEL</button>
    </form>

</body>

<script>
  data: {


        msg: '',
        success: false
   },
  methods: {


        writeExcel(url) {
					const data = {
						data: this.results,
					};
          this.requestGet(url, data);
				},
        requestGet(url, data) {
          axios.post(url, data)
          .then(req =>{
            this.msg = req.data.msg
            this.success = req.data.success
          })
          .catch(err => {
            console.log(err);
          });
        },
 }
</script>

簡単に説明するとformで追加したボタンが押されると’./excel’を引数に
<script>にあるwriteExcel(url)メソッドが動きます
dataの中に現在Vueで表示されているthis.resultsを詰め込んでaxios.postで今から記述するFlaskの指定のurl(‘./excel’)へ飛ばします。

サーバからのレスポンスは書き込みの結果を受け取る予定となっていますのでmsgとsuccsessを用意しています。

サーバ側の追記

次はサーバ側のmain.py編集をします。
こちらも前回記事とコードが被っているところは省いています。

必要なライブラリと、先程作成したfunctionを呼び込みます。
URLで’/excel’が叩かれたとき発動するメソッドを書きます

from flask import request
from function import Func


@app.route('/excel', methods=['POST'])
def excel():
    get_request = request.get_json()
    data = get_request['data']
    data_list = Func.write(data)
    if not data_list:
        msg = {
            'msg': '書き込みに失敗しました',
            'success': 'false'
            }
    else:
        msg = {
            'msg': '書き込みに成功しました',
            'success': 'true'
            }
    return jsonify(msg)

動作確認

これでVue側からデータを受け取ってエクセルに書き込むための準備が整いました。
main.pyを実行して

$ python main.py
http://127.0.0.1:5000/index

へアクセスして下さい。
ボタン付きで表示されていればOKです。

ボタンを押して「書き込みに成功しました」メッセージが表示されれば成功しているはずです。

ついでにエクセルへちゃんと書き込まれたか確認します。

function.pyのpathを変えていなければmain.pyと同じ配下に作成されているはずです。
ちなみに押せば押すほど最終行へ追記されていきます。

forで回して書き込んでいるだけなのでa〜zの順で書き込まれてしまっていますが、とりあえずヨシということにしましょう。
気になる方は勉強も兼ねて自力で直してみて下さい。

メッセージの表示変更

メッセージの表示が殺風景なのでついでに修正します。

index.htmlを編集します。
下記追記して下さい。

<body>
  <div id="sample_app">
  <template v-if="msg != ''">
    <div v-bind:class="success ? 'alert alert-success' : 'alert alert-danger'" role="alert">
        [[ msg ]]
    </div>
  </template>
  <h1>BOSTON DF</h1>


</body>

サーバを再起動して再度下記へアクセスしてボタンを押して下さい。

http://127.0.0.1:5000/index

ボタンを押してメッセージの表示が変わっていればOKです。

次はわざと失敗してみて下さい。
多分、成功したときと同じ緑色が出るはずです。

成功しても失敗しても同じ色が出る理由は先程追記した下記コードでうまく判定ができていないからです。

<div v-bind:class="success ? 'alert alert-success' : 'alert alert-danger'" role="alert">

なぜ判定できてないかと言うとsuccessで文字列を受け取ってしまっているからです。
文字列からBoolean型へ変更するメソッドを追記すればうまくいくはずです。

文字列からBooleanへ変更するメソッド追記

index.htmlを編集します。

<script>



       requestGet(url, data) {
          axios.post(url, data)
          .then(req =>{
            this.msg = req.data.msg
            this.success = this.toBoolenan(req.data.success) //変更
          })
          .catch(err => {
            console.log(err);
          });
        },
        //追記メソッド
        toBoolenan(str) {
          return str.toLowerCase() === 'true';
        }

</script>

これで再起動して無理矢理でもVue側へfalseを送ってみて下さい。
私はFlask側で成功した時にfalseを送るようにしてみました。

    else:
       msg = {
         'msg': '書き込みに成功しました',
         'success': 'false' //デバッグのため一時falseへ変更
       }

再起動して確認して色が変わっていればOKです。

表示されたメッセージを消す

このメッセージが表示にされたままだと邪魔なので消します。

msgを受け取った3秒後にmsgをリセットする関数を追加します。

<script>

      methods: {
        requestGet(url, data) {
          axios.post(url, data)
          .then(req =>{
            this.msg = req.data.msg
            this.success = this.toBoolenan(req.data.success)
          })
          .catch(err => {
            console.log(err);
          });
          setTimeout(this.resetMsg, 3000); //←追加
        },

        //追加
        resetMsg() {
					this.msg = '';
					this.success = null;
        }
      }

</script>

本当はToastというライブラリがありますが、今回はこの方法で無理やり消すことにします。

うまく表示されエクセルへ書き込まれたでしょうか。
下記最終的なコードを載せます。

最終的なコード

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
	<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
	<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script>
	<title>Document</title>
	<style>
		.asc::after {
  		content: "▼";
		}
		.desc::after {
			content: "▲";
		}
		.container {
    	max-width: 90%;
		}
		.navbar {
			margin-bottom: 10px;
		}
		.table {
			margin-top: 10px;
		}
		.left {
			float: right;
			margin-bottom: 10px;
		}
		.text-box-area {
			display: flex;
		}
	</style>
</head>
<body>

	<div id="app">
		<nav class="navbar navbar-light bg-light">
			<a class="navbar-brand">Note</a>
			<form v-on:submit.prevent="searchNote('/search')" class="form-inline" method="POST" >
				<input class="form-control mr-sm-2" v-model="nort_key" type="text" name="search" placeholder="search" aria-label="Search">
				<button class="btn btn-outline-success my-2 my-sm-0">Search</button>
			</form>
		</nav>
		<template v-if="msg != ''">
			<div v-bind:class="success ? 'alert alert-success' : 'alert alert-danger'" role="alert">
				[[ msg ]]
			</div>
		</template>
		<div class="container">
		<div class="search-area">
			<table class="table table-bordered tbl">
				<tr>
					<th colspan="2" style="text-align: center;">絞り込み機能</th>
					<template v-if="detailQuery">
						<td @click="detailQuery = !detailQuery">閉じる▼</td>
					</template>
					<template v-else>
						<td @click="detailQuery = !detailQuery">開く▲</td>
					</template>
				</tr>
				<template v-if="detailQuery">
				<tr>
					<th>ユーザ検索</th>
					<td colspan="2"><input v-model="keyword" placeholder="Search..." ></td>
				</tr>
				<tr>
					<th>data</th>
					<td>
						<div class="text-box-area">
							<vuejs-datepicker
							v-model="start"
							:format="DatePickerFormat"
							:language="ja"></vuejs-datepicker>
							<span>〜</span>
						<vuejs-datepicker
							v-model="end"
							:format="DatePickerFormat"
							:language="ja"></vuejs-datepicker>
						</div>
						<td @click="sortBy('date')" :class="sortClass('date')">ソート</td>
				</td>
				</tr>
				<tr>
					<th>like</th>
					<td>
						<div class="text-box-area">
							<div class="text-box">
								<input v-model.number="likeMin" placeholder="min...">
								<span>〜</span>
								<input v-model.number="likeMax" placeholder="max...">
							</div>
						</div>
					</td>
					<td @click="sortBy('like')" :class="sortClass('like')">ソート</td>
				</tr>
				<tr>
					<th>price</th>
					<td>
						<div class="text-box-area">
							<div class="text-box">
								<input v-model.number="priceMin" placeholder="min...">
								<span>〜</span>
								<input v-model.number="priceMax" placeholder="max...">
						</div>
					</div>
				</td>
					<td @click="sortBy('price')" :class="sortClass('price')">ソート</td>
				</tr>
			</template>
			</table>

		</div>
		<div class="volume-area">
			<div class="left">
				<div class="btn-group">
					<button type="button" class="btn btn-outline-success my-2 my-sm-0 dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
						Action
					</button>
					<div class="dropdown-menu">
						<form v-on:submit.prevent="writeExcel('/excel')" class="form-inline" method="POST" >
							<button class="btn btn-link dropdown-item">Excelへ書き込み</button>
						</form>
						<form v-on:submit.prevent="searchNote('/note')" class="form-inline" method="POST" >
							<button class="btn btn-link dropdown-item">書き込みの履歴表示</button>
						</form>
					</div>
				</div>
			</div>
			<template v-if="volume != ''">
				<div style="padding: 7px;">[[ volume ]]件ヒット</div>
			</template>
		</div>
		<table class="table table-hover">
			<thead>
				<tr>
					<th v-for="(value, key) in columns" :class="sortClass(key)">
						[[ value ]]
					</th>
				</tr>
			</thead>
			<tbody>
				<tr v-for="result in results">
					<td v-for="(value, key) in columns">
						[[ result[key] ]]
					</td>
				</tr>
			</tbody>
		</table>
	</div>
</div>
	<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
	<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4" crossorigin="anonymous"></script>
	<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js" integrity="sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1" crossorigin="anonymous"></script>
	<script src="https://unpkg.com/vuejs-datepicker"></script>
	<script src="https://cdn.jsdelivr.net/npm/vuejs-datepicker@1.6.2/dist/locale/translations/ja.js"></script>
	<script src="https://momentjs.com/downloads/moment.js"></script>
	<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
	<script>
		const vm = new Vue({
			el: '#app',
			delimiters: ['[[', ']]'], // Flaskのdelimiterとの重複を回避
			components: {
    		'vuejs-datepicker':vuejsDatepicker
  		},
      data:{
				// main表示
				series: [],
				results: [],
				columns: null,
				sort_key: '',
				sort_asc: true,
				keyword: '',
				likeMin: '',
				likeMax: '',
				priceMin: '',
				priceMax: '',
				nort_key: '',
				volume: '',
				detailQuery: true,

       // msg系
        msg: '',
        success: null,

       // 時間系
				start: '',
				end: '',
				DatePickerFormat: 'yyyy-MM-dd',
      	ja: vdp_translation_ja.js
			},
			watch: {
				keyword() {
					this.filterNote();
				},
				budget() {
					this.filterNote();
				},
				likeMin() {
					this.filterNote();
				},
				likeMax() {
					this.filterNote();
				},
				priceMin() {
					this.filterNote();
				},
				priceMax() {
					this.filterNote();
				},
				start() {
					this.filterNote();
				},
				end() {
					this.filterNote();
				},
				msg() {
					setTimeout(this.resetMsg, 3000);
				}
			},
			created: function() {
				this.loadColumns();
				this.results = []
				this.series = []
			},
			methods: {
				searchNote(url) {
					const data = {
						search: this.nort_key,
						};
					this.loadData(url, data);
				},
				loadColumns() {
					this.columns = {
						count: '#',
						user_name: 'USER NAME',
						date: 'SUBMISSION DATE',
						title: 'TITLE',
						like: 'LIKE',
						price: 'PRICE',
						search_word: 'SEARCH WORD'
					};
				},
				loadData(url, data) {
					axios.post(url, data)
					.then(res => {
						vm.results = res.data.results
						vm.series = res.data.results
						vm.msg = res.data.msg.msg
						vm.success = this.toBoolenan(res.data.success)
						vm.volume = res.data.volume
					})
					.catch(err => {
          console.log(err);
          });
				},
				sortBy(key) {
					this.sort_key === key
						? (this.sort_asc = !this.sort_asc)
						: (this.sort_asc = true);
					this.sort_key = key;
					this.sort()
				},
				sort() {
					if (this.sort_key !== '') {
						let set = 1;
						this.sort_asc ? (set = 1) : (set = -1);
						this.results.sort((a, b) => {
							if (a[this.sort_key] < b[this.sort_key]) return -1 * set;
							if (a[this.sort_key] > b[this.sort_key]) return 1 * set;
							return 0;
						});
						return this.results;
					}
				},
				sortClass(key) {
					return {
						asc: this.sort_key === key && this.sort_asc,
						desc: this.sort_key === key && !this.sort_asc,
					};
				},
				customformat(value) {
					return moment(value).format('YYYY/MM/DD');
				},
				filterUser(series) {
					const notes = series;
					this.results = notes.filter(note => note.user_name.includes(this.keyword))
				},
				filterLike(filterSeries) {
					const notes = filterSeries;
					this.results = notes.filter(note => {
						if(this.likeMin === '' && this.likeMax === '') return this.results;
						if(this.likeMin !== '' && this.likeMax === '') return (this.likeMin <= note.like);
						if(this.likeMin === '' && this.likeMax !== '') return (this.likeMax >= note.like);
						if(this.likeMin !== '' && this.likeMax !== '') return (this.likeMin <= note.like && note.like <= this.likeMax);
					})
				},
				filterPrice(filterSeries) {
					const notes = filterSeries;
					this.results = notes.filter(note => {
						if(this.priceMin === '' && this.priceMax === '') return this.results;
						if(this.priceMin !== '' && this.priceMax === '') return (this.priceMin <= note.price);
						if(this.priceMin === '' && this.priceMax !== '') return (this.priceMax >= note.price);
						if(this.priceMin !== '' && this.priceMax !== '') return (this.priceMin <= note.price && note.price <= this.priceMax);
					})
				},
				filterDate(filterSeries) {
					const notes = filterSeries;
					const start = this.customformat(this.start);
					const end = this.customformat(this.end);
					this.results = notes.filter(note => {
						if(this.start === '' && this.end === '') return this.results;
						if(this.start !== '' && this.end === '') return (start <= note.date);
						if(this.start === '' && this.end !== '') return (end >= note.date);
						if(this.start !== '' && this.end !== '') return (start <= note.date && note.date <= end);
					})
				},
				filterNote() {
					this.filterUser(this.series);
					this.filterLike(this.results);
					this.filterPrice(this.results);
					this.filterDate(this.results);
					this.sort();
				},
				writeExcel(url) {
					const data = {
						data: this.results,
					};
          this.requestGet(url, data);
				},
        requestGet(url, data) {
          axios.post(url, data)
          .then(req =>{
           this.msg = req.data.msg
           this.success = this.toBoolenan(req.data.success)
          })
          .catch(err => {
            console.log(err);
          });
        },
        toBoolenan(str) {
          return str.toLowerCase() === 'true';
        },
        resetMsg() {
          this.msg = '';
          this.success = null;
        }
			},
		});
	</script>
</body>
</html>
from flask import jsonify
from flask import Flask, render_template

import pandas as pd
from flask import request

from sklearn import datasets
from function import Func

app = Flask(__name__)

@app.route('/')
def hello():
	name = 'Hello World'
	return name

@app.route('/index', methods = ['GET'])
def index():
	return render_template('index.html')


@app.route('/json',  methods = ['GET'])
def test_json():
	boston = datasets.load_boston()
	df_boston = pd.DataFrame(boston.data, columns = boston.feature_names)
	df_head = df_boston.head()
	df_dicts = df_head.to_dict(orient='records')
	list = []
	for i, df in enumerate(df_dicts, 1):
		json = {
			'number': i,
			'age'    : df['AGE'],
			'b'      : df['B'],
			'chas'   : df['CHAS'],
			'crim'   : df['CRIM'],
			'dis'    : df['DIS'],
			'indus'  : df['INDUS'],
			'lstat'  : df['LSTAT'],
			'nox'    : df['NOX'],
			'ptratio': df['PTRATIO'],
			'rad'    : df['RAD'],
			'rm'     : df['RM'],
			'tax'    : df['TAX'],
			'zn'     : df['ZN']
		}
		list.append(json)
	return jsonify(list)

@app.route('/excel', methods=['POST'])
def excel():
	get_request = request.get_json()
	data = get_request['data']
	data_list = Func.write(data)
	if not data_list:
		msg = {
			'msg': '書き込みに失敗しました',
			'success': 'false'
				}
	else:
		msg = {
			'msg': '書き込みに成功しました',
			'success': 'true'
				}
	return jsonify(msg)

if __name__ == '__main__':
    app.run()

csvの書き込みも多分同じ要領でfunctionを作れば余裕だと思います。
参考になれば幸いです。

Flaskカテゴリの最新記事