bottleでCSVファイルをダウンロードさせるときにつまづいたところ

やろうとしたこと

前回(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を用いてフロント側の機能を実装していきます。
その後は、いよいよデプロイします。

コメント

タイトルとURLをコピーしました