やろうとしたこと
前回(bottleでテンプレートを継承してみる。)からの続きで、
TogglのデータをPandasを用いてDataFrameにして、DataFrameをさらにCSVの形式にしてダウンロードさせようとしました。
結果として、1日以上かかりましたが、解決策が見えたのでまとめておきます。
つまづいた原因
最初は、DataFrameの形式をCSVでダウンロードするためにデータの型の変換がうまくいっていないのではないかと思い、試行錯誤していました。
以下のように、POSTで「/」のページにアクセスしたときに、CSVファイルをダウンロードさせようとしていました。
この『POSTで「/」のページにアクセスしたときに、CSVファイルをダウンロード』というのがそもそも無理っぽいことが分かりました
(僕のWebに関する知識が足りていなかったです)。
# coding:utf-8 import bottle#,jinja2 from bottle import route, run, template, get, post, request, response, HTTPResponse from bottle import TEMPLATE_PATH, jinja2_template as template import requests from requests.auth import HTTPBasicAuth import json from json import loads from datetime import datetime as dt import datetime import numpy as np import pandas as pd import csv TEMPLATE_PATH.append("./views") def data_export(email,password): (中略) #Pandasでデータを集計 pivot1 = df.pivot_table(index = ['Folder','Description'], values = 'Duration', aggfunc = 'sum', fill_value = 0) pivot2 = pd.to_datetime(pivot1['Duration']).map(lambda x: '{:%H:%M:%S}'.format(x)) #データフレームに変換 pivot3 = pivot2.to_frame() # HTTPReponseオブジェクトを作成 response = HTTPResponse(content_type='application/octet-stream') #'text/csv') # ダウンロードするファイル名を指定 response.headers['Content-Disposition'] = 'attachment; filename = filename' # HTTPResponseオブジェクトはファイルっぽいオブジェクトなので、csv.writerにそのまま渡せます。 writer = csv.writer(response).encode('shift-jis') for row in pivot3: writer.writerow(row) return response (中略) @post('/') # or @route('/', method='POST') def do_login(): """ POSTで/にアクセスした際の処理 """ # フォームからPOSTされたデータを取得する email = request.forms.get('email') password = request.forms.get('password') # ログイン判定を行う if check_status_code(email, password): title = "認証成功" # pivot = data_export(email,password) data_export(email,password) return template("form_success.html", title = title) else: title = "認証失敗" return template('form_failed.html', title = title)
以下のエラーコードが表示されました。
このエラーコードは、『TypeError( ‘引数1には “write”メソッドが必要です)』(Google翻訳より)という意味で、
このエラーコードから、DataFrameの形式をCSVでダウンロードするためにデータの型の変換がうまくいっていないのではないかと思いました。
(他にもいろいろ試して、他のエラーコードが表示されたような気もしますが…。また、エラーコードの解釈として正しいかどうかは分かりません…。)
TypeError('argument 1 must have a "write" method')
(参考ページ)
Django、CSVのインポート・エクスポート
解決策
DataFrameをCSVでダウンロードするには、Pandasのto_csv()でCSVに変換すればいけることが分かりました。
また、『POSTで「/」にアクセスしたとき』ではなく、ファイルをダウンロードするURLを別URLで定義してあげて、そのURLにアクセスするとファイルがダウンロードできることが分かりました。
試しに、「/」から「/download」にアクセスしたときにCSVファイルをダウンロードするようなテストコードを組んでみると問題なく動きました。
こういう小さなテストって大事ですね!
# coding: utf-8 import csv from io import StringIO from bottle import route, response, run, template, HTTPResponse from bottle import TEMPLATE_PATH, jinja2_template as template import pandas as pd TEMPLATE_PATH.append("./views") @route("/") def top(): return "<a href='/download'>ダウンロードはこちらから</a>" @route("/download") def download(): """ CSVファイルを作成してダウンロード """ df = pd.DataFrame([[1,2,3],[4,5,6],[7,8,9]]) response = HTTPResponse(body = df.to_csv()) # コンテンツタイプにapplication/octet-streamを指定 response.content_type = "application/octet-stream" # ダウンロードするファイル名を指定 response.headers["Content-Disposition"] = "attachment; filename='test.csv'" return response if __name__ == "__main__": run(host="localhost", port=8000)
以下のページが表示されるので、リンクをクリックするとファイルがダウンロードされます。
ここに至るまでにいろいろ試してみました(主に、DataFrameをCSVの形式でダウンロードするにはどうすればいいか、ということです)。
参考までに試してみたことを以下にまとめておきます。
試してみたことその1
いろいろ試していて、「データ型が文字型じゃない」とエラーが出たので、StringIOというパッケージを使ってみました。
# coding:utf-8 import bottle#,jinja2 from bottle import route, run, template, get, post, request, response, HTTPResponse from bottle import TEMPLATE_PATH, jinja2_template as template import requests from requests.auth import HTTPBasicAuth import json from json import loads from datetime import datetime as dt import datetime import numpy as np import pandas as pd import csv # from io import BytesIO as IO from io import StringIO as IO TEMPLATE_PATH.append("./views") def data_export(email,password): (中略) s = IO() pivot = pivot3.to_csv(s) writer = csv.writer(pivot) # ストリームの読み書きの位置を先頭に変更する pivot.seek(0) # csvファイルとして出力 pivot.to_csv("Toggl_data.csv",encoding = 'shift-jis') return pivot3 (中略) @post('/') # or @route('/', method='POST') def do_login(): """ POSTで/にアクセスした際の処理 """ # フォームからPOSTされたデータを取得する email = request.forms.get('email') password = request.forms.get('password') # ログイン判定を行う if check_status_code(email, password): title = "認証成功" # pivot = data_export(email,password) data_export(email,password) return template("form_success.html", title = title) else: title = "認証失敗" return template('form_failed.html', title = title)
同じエラーが出ます。
TypeError('argument 1 must have a "write" method')
(参考ページ)
【Python】Bottleを使ってCSVダウンロードする方法
Bottleでファイルをダウンロードさせる
Use Flask to convert a Pandas dataframe to CSV and serve a download
試してみたことその2
Excel形式にしてダウンロードしていたコードがあったので試してみました。
# coding:utf-8 import bottle#,jinja2 from bottle import route, run, template, get, post, request, response, HTTPResponse from bottle import TEMPLATE_PATH, jinja2_template as template import requests from requests.auth import HTTPBasicAuth import json from json import loads from datetime import datetime as dt import datetime import numpy as np import pandas as pd import csv # from io import BytesIO as IO from io import StringIO as IO TEMPLATE_PATH.append("./views") # TogglのAPIに接続 def toggl_auth(email,password): (中略) # StringIOを使う excel_file = IO() xlwriter = pd.ExcelWriter(excel_file, engine='xlsxwriter') pivot3.to_excel(xlwriter, sheet_name = filename) xlwriter.save() xlwriter.close() excel_file.seek(0) response = HTTPResponse(excel_file.read(), content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') return response (中略) @post('/') # or @route('/', method='POST') def do_login(): """ POSTで/にアクセスした際の処理 """ # フォームからPOSTされたデータを取得する email = request.forms.get('email') password = request.forms.get('password') # ログイン判定を行う if check_status_code(email, password): title = "認証成功" # pivot = data_export(email,password) data_export(email,password) return template("form_success.html", title = title) else: title = "認証失敗" return template('form_failed.html', title = title)
以下のエラーコードが表示されました。
TypeError("string argument expected, got 'bytes'")
(参考ページ)
Django Pandas to http response (download file)
試してみたことその3
bodyにto_csv()で書き出したCSVファイルを指定してみました。
# coding:utf-8 import bottle#,jinja2 from bottle import route, run, template, get, post, request, response, HTTPResponse from bottle import TEMPLATE_PATH, jinja2_template as template import requests from requests.auth import HTTPBasicAuth import json from json import loads from datetime import datetime as dt import datetime import numpy as np import pandas as pd import csv # from io import BytesIO as IO # from io import StringIO as IO TEMPLATE_PATH.append("./views") def data_export(email,password): (中略) #Pandasでデータを集計 pivot1 = df.pivot_table(index = ['Folder','Description'], values = 'Duration', aggfunc = 'sum', fill_value = 0) pivot2 = pd.to_datetime(pivot1['Duration']).map(lambda x: '{:%H:%M:%S}'.format(x)) #データフレームに変換 pivot3 = pivot2.to_frame() response = HTTPResponse(body = pivot3.to_csv(encoding = 'shift_jis')) response.headers["Content-Type"] = "text/csv" response.headers["Content-Disposition"] = "attachment; filename = export.csv"#.format(pivot3.to_csv(encoding = 'shift_jis')) return response (中略) @post('/') # or @route('/', method='POST') def do_login(): """ POSTで/にアクセスした際の処理 """ # フォームからPOSTされたデータを取得する email = request.forms.get('email') password = request.forms.get('password') # ログイン判定を行う if check_status_code(email, password): title = "認証成功" # pivot = data_export(email,password) data_export(email,password) return template("form_success.html", title = title) else: title = "認証失敗" return template('form_failed.html', title = title)
エラーは表示されないのですが、POSTで「/」でアクセスするとファイルがダウンロードされず、以下のページ(テンプレートのHTMLページ)が表示されます。
(参考ページ)
[技術][Python]Flask で CSV 出力(修正版)
その他参考記事
pandas.DataFrame.to_string
Django Class-based views でCSVダウンロードページの実装ではまったこと
Python + Bottle でファイルのダウンロードを実装
BottleのRequest/Responseオブジェクトをマスター
今後やること
今回の知見を基に、CSVファイルをダウンロードできるようにコードを書き換えます。
その後、BootStrapを用いて少しデザインをいじったり、JavaScriptを用いてフロント側の機能を実装していきます。
その後は、いよいよデプロイします。
コメント