1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

StreamlitでJSONをアップロードし、結果をZIPでダウンロードするアプリ構築ガイド

Posted at

user:

streamlit で、ユーザにドラッグ&ドロップで json ファイルを読み込ませたいですが、Pythonでどう書きますか?

assistant:

Streamlit でユーザーにドラッグ&ドロップで JSON ファイルをアップロードさせて読み込むには、st.file_uploader を使えば簡単に実現できます。以下にその基本的なコード例を示します。


✅ Streamlit で JSON ファイルをドラッグ&ドロップで読み込むコード

import streamlit as st
import json

st.title("JSONファイルアップローダー")

# ファイルアップローダー(ドラッグ&ドロップ対応)
uploaded_file = st.file_uploader("JSONファイルをアップロードしてください", type="json")

if uploaded_file is not None:
    try:
        # ファイルを読み込んで JSON に変換
        data = json.load(uploaded_file)
        
        st.success("JSONファイルが正常に読み込まれました!")
        
        # JSONの内容を表示(dict形式で整形表示)
        st.json(data)
    except json.JSONDecodeError:
        st.error("無効なJSONファイルです。形式を確認してください。")

🔍 補足ポイント

  • st.file_uploader はドラッグ&ドロップもクリックアップロードも両方対応しています。
  • type="json" と指定することで、拡張子 .json のファイルだけを許可できます。
  • json.load(uploaded_file) で、Pythonの辞書(dict)として読み込みます。
  • エラー処理も try-except で加えておくと安心です。

もし JSON の中身をテーブル(DataFrame)っぽく表示したい場合は、中身の構造に応じて pd.DataFrame(data) を使うこともできます。その場合、import pandas as pd を忘れずに。

必要ならその応用コードもご紹介します。

user:

次のダウンロードボタンを押すと、ダウンロードした後、そのダウンロードボタンが消えてしまうんですが、解決法は分かりますか?

st.download_button(
        label="📦 結果をZIPで一括ダウンロード",
        data=zip_buffer,
        file_name=f"{target_col}.zip",
        mime="application/zip",
    )

assistant:

はい、st.download_button()押されたあと消えてしまう問題は、Streamlit の仕組み(再実行・ステートレス)によって発生する典型的な現象です。原因と解決方法を以下にまとめます。


✅ 原因

Streamlit はユーザーが何か操作をするとスクリプト全体を最初から再実行します。

つまり、

  • st.download_button() を押すと、
  • スクリプト全体が再実行され、
  • 再実行時に zip_buffer の中身が None だったり作られていないと、
  • download_button() の表示自体がスキップされてしまう

という流れです。


✅ 解決方法(おすすめ)

解法:st.session_state を使って、ダウンロード状態を管理

import streamlit as st
import io
import zipfile

# ① デモ用の ZIP を作成
def create_dummy_zip():
    buffer = io.BytesIO()
    with zipfile.ZipFile(buffer, "w") as z:
        z.writestr("example.txt", "This is a test file.")
    buffer.seek(0)
    return buffer

# ② セッションステートに ZIP を保存(初回のみ作成)
if "zip_buffer" not in st.session_state:
    st.session_state.zip_buffer = create_dummy_zip()

# ③ ダウンロードボタンの表示
st.download_button(
    label="📦 結果をZIPで一括ダウンロード",
    data=st.session_state.zip_buffer,
    file_name="result.zip",
    mime="application/zip",
)

✅ ポイント解説

  • st.session_state を使えば、再実行後も値(zipバッファ)を保持できます。
  • BytesIO やファイル生成をステートに格納しておくと、ボタンが消える問題を回避できます。

🔁 もう1つの選択肢:zip_buffer の生成条件を明確にする

もし zip_buffer がファイルアップロードやボタン押下で生成されているなら、

if zip_buffer is not None:
    st.download_button(...)

のような条件で zip_buffer が消えないようにすることも有効です。


必要なら

  • st.download_button を押した後に「ダウンロード完了しました」のようなメッセージを出す方法
  • ダウンロード用ファイルを ZIP で複数ファイルまとめる方法

などもお手伝いできますので、お気軽に聞いてください。

user:

次のような処理を行なっているのですが、どうやら、ダウンロードボタンを押すと buffers が空になるようなんですよね。空にならない工夫はありませんか?

# --- ZIPにまとめる ---
    zip_buffer = io.BytesIO()
    with zipfile.ZipFile(zip_buffer, "w") as zf:
        for file_name, buffer in buffers:
            if isinstance(buffer, io.StringIO):
                zf.writestr(file_name, buffer.getvalue())
            else:  # BytesIO
                zf.writestr(file_name, buffer.read())
    zip_buffer.seek(0)

    # --- ダウンロードボタン ---
    st.download_button(
        label="📦 結果をZIPで一括ダウンロード",
        data=zip_buffer,
        file_name=f"{target_col}.zip",
        mime="application/zip",
   )

assistant:

はい、ご指摘のとおり、st.download_button() を押すと Streamlit がスクリプトを再実行するため、buffers の中身(特に io.StringIOio.BytesIO)が空になる問題が起きます。


❗なぜ buffers が空になるのか?

  • BytesIOStringIO オブジェクトは「読み取るとポインタが進む」性質を持っています。
  • いったん .read().getvalue() を実行した後に再利用すると、中身が空になってしまいます。
  • Streamlit では、ダウンロードボタン押下 → 再実行 → buffers が再構築されない → 空のまま → ZIPが壊れる or 消える、という現象が起こる。

✅ 解決策

(1) buffersst.session_state に保存しておく(推奨)

Streamlit のセッションステートに buffers を保存しておき、再実行しても中身が保持されるようにします。

import streamlit as st
import io
import zipfile

# --- サンプル buffers(StringIO, BytesIO のリスト)---
def generate_buffers():
    buf1 = io.StringIO("Hello from string buffer")
    buf2 = io.BytesIO(b"Hello from bytes buffer")
    return [("text1.txt", buf1), ("binary1.bin", buf2)]

# --- セッションステートに保持 ---
if "buffers" not in st.session_state:
    st.session_state.buffers = generate_buffers()

# --- ZIP にまとめる処理 ---
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w") as zf:
    for file_name, buffer in st.session_state.buffers:
        if isinstance(buffer, io.StringIO):
            zf.writestr(file_name, buffer.getvalue())
        else:
            buffer.seek(0)  # 再読み込み用にポインタを先頭に戻す
            zf.writestr(file_name, buffer.read())
zip_buffer.seek(0)

# --- ダウンロードボタン ---
st.download_button(
    label="📦 結果をZIPで一括ダウンロード",
    data=zip_buffer,
    file_name="result.zip",
    mime="application/zip",
)

(2) buffers を生成する関数を用意して、毎回確実に新しく作る

一時ファイルのような扱いにして、毎回生成するのも手です。状態保存が不要な場合はこちらが簡単。

def generate_buffers():
    buf1 = io.StringIO("Some text data")
    buf2 = io.BytesIO(b"Some binary data")
    return [("file1.txt", buf1), ("file2.bin", buf2)]

buffers = generate_buffers()

✅ ポイントのまとめ

方法 メリット 注意点
st.session_state に保存 再実行しても中身が残る ファイルサイズが大きいとメモリ使用量に注意
毎回再生成 安定して動作 バッファの中身を毎回構築する必要あり

ご希望であれば、PandasのDataFrameを複数CSV化して一括ZIPダウンロードするStreamlitアプリ のテンプレートも用意できます。必要でしたらお知らせください!

user:

ありがとうございました。ここまでの内容をブログにまとめたいので、タイトル案をいくつか提案していただけますか?

assistant:

もちろんです!今回の内容(StreamlitでJSONファイルの読み込み+ZIP一括ダウンロード+バッファの消失対策)をふまえて、以下のようなブログタイトル案をいくつかご提案します:


🎯 技術寄り・検索性重視のタイトル案

  1. 「StreamlitでZIPダウンロードが消える?再実行とバッファ問題の完全対策」
  2. 「StreamlitでBytesIOが空になる理由とその解決策」
  3. 「StreamlitでZIP一括ダウンロードを安定化する方法【session_state活用】」
  4. 「StreamlitでJSONをアップロードし、結果をZIPでダウンロードするアプリ構築ガイド」
  5. 「Streamlitでファイルアップロード+ZIP出力を安全に実装する方法」

📝 やや親しみやすいタイトル案

  1. 「Streamlitでダウンロードボタンが消える!?原因と対処法まとめ」
  2. 「Streamlit × ZIP一括ダウンロード:消えるボタンにさようなら」
  3. 「StreamlitでファイルアップロードからZIPダウンロードまで、つまずきやすいポイント解説」

🔧 実用Tips集のようなタイトル案

  1. 「Streamlit実践Tips:ファイルアップロード・ZIP出力・バッファ管理まで」
  2. 「Streamlitでデータ共有アプリを作る:JSON読込→ZIP一括出力の流れ」

気になるものがあれば、そこから調整したり、記事構成も一緒に考えられます!

user:

「StreamlitでJSONをアップロードし、結果をZIPでダウンロードするアプリ構築ガイド」にしたいと思います。ここまでの内容を詳しくまとめてください。

assistant:

了解しました!以下に「StreamlitでJSONをアップロードし、結果をZIPでダウンロードするアプリ構築ガイド」というタイトルに基づいて、ここまでの内容を丁寧にまとめた記事構成をご提案します。


📦 StreamlitでJSONをアップロードし、結果をZIPでダウンロードするアプリ構築ガイド

はじめに

Streamlit はデータ可視化・アプリ開発に便利なフレームワークですが、ファイル操作を行う際にはいくつか注意点があります。本記事では、以下のような構成のアプリを構築しつつ、Streamlit 特有の「再実行」によるトラブルとその解決法まで詳しく解説します。

✅ この記事で扱うこと

  • ユーザーが JSON ファイルをアップロードできる UI の作成
  • JSON を Python dict として読み込んで内容を表示
  • 処理結果を複数ファイルとして出力し ZIP にまとめて一括ダウンロード
  • BytesIOStringIO の扱いで中身が消えてしまう問題の回避法

1. JSONファイルをアップロードする方法

Streamlitでは st.file_uploader() を使うことで、ドラッグ&ドロップ対応のアップローダーを簡単に実装できます。

import streamlit as st
import json

st.title("JSONファイルアップローダー")

uploaded_file = st.file_uploader("JSONファイルをアップロードしてください", type="json")

if uploaded_file is not None:
    try:
        data = json.load(uploaded_file)
        st.success("JSONファイルが正常に読み込まれました!")
        st.json(data)  # dictとして整形表示
    except json.JSONDecodeError:
        st.error("無効なJSONファイルです。形式を確認してください。")

2. 処理結果をZIPで一括ダウンロードする

🔧 基本の構成

複数ファイル(テキスト or バイナリ)をまとめて zipfile.ZipFile を使って ZIP に書き込み、st.download_button() でダウンロード可能にします。

import io
import zipfile

buffers = [
    ("file1.txt", io.StringIO("hello world")),
    ("file2.bin", io.BytesIO(b"binary content")),
]

zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w") as zf:
    for file_name, buffer in buffers:
        if isinstance(buffer, io.StringIO):
            zf.writestr(file_name, buffer.getvalue())
        else:
            zf.writestr(file_name, buffer.read())
zip_buffer.seek(0)

st.download_button(
    label="📦 結果をZIPで一括ダウンロード",
    data=zip_buffer,
    file_name="result.zip",
    mime="application/zip",
)

3. よくある問題:ダウンロードボタンが消える/ZIPが空になる

❗問題の正体

Streamlit はユーザーの操作(例えばダウンロードボタン押下)で スクリプトを上から再実行 するため、以下のような現象が起きます。

  • BytesIOStringIO.read().getvalue() を実行するとポインタが進む(空になる)
  • 再実行時に buffers の元データが再生成されないと、ZIPの中身が空になる or ダウンロードボタンが表示されない

4. 解決策:st.session_stateを活用して状態を維持

✅ セッションステートで buffers を保持

if "buffers" not in st.session_state:
    st.session_state.buffers = [
        ("text1.txt", io.StringIO("This is a test file")),
        ("binary1.bin", io.BytesIO(b"binary content")),
    ]

zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w") as zf:
    for file_name, buffer in st.session_state.buffers:
        if isinstance(buffer, io.StringIO):
            zf.writestr(file_name, buffer.getvalue())
        else:
            buffer.seek(0)
            zf.writestr(file_name, buffer.read())
zip_buffer.seek(0)

st.download_button(
    label="📦 結果をZIPで一括ダウンロード",
    data=zip_buffer,
    file_name="result.zip",
    mime="application/zip",
)

5. 応用:JSONから生成した複数ファイルをZIPで返す

アップロードした JSON をもとに複数の処理結果(例:CSV、ログ、テキスト)を作り、それらを ZIP にまとめることもできます。

import pandas as pd

if uploaded_file is not None:
    data = json.load(uploaded_file)
    
    # デモ:JSONの内容をDataFrame化してCSVとして出力
    df = pd.DataFrame(data)
    csv_buffer = io.StringIO()
    df.to_csv(csv_buffer, index=False)

    st.session_state.buffers = [
        ("data.csv", csv_buffer),
        ("meta.txt", io.StringIO("JSON upload completed successfully.")),
    ]

このように、Streamlit を使って JSON 入力 → 処理 → ZIP 出力までの流れを作れます。


おわりに

Streamlit の便利さの裏にある「ステートレス再実行」という仕様は、ファイル処理時に思わぬバグを引き起こすことがあります。しかし、st.session_state を適切に使えば安定したアプリが構築できます。

本記事をベースに、ぜひあなたのデータ可視化・共有アプリを発展させてください!


🔗 補足リンク


記事構成やコードの補足・装飾も必要ならお手伝いします!

user:

ありがとうございました!補足リンクも正しいことを確認しました!

assistant:

どういたしまして!ご確認ありがとうございます。リンクやコードも問題なかったようで安心しました 😊

このブログ記事は、Streamlit を使っている多くの人にとってとても実用的で助かる内容だと思います。特に「ZIPが空になる」「ボタンが消える」という罠にハマった経験のある人には刺さるはずです。


もし今後、以下のような内容を追記・発展させたい場合もぜひご相談ください:

  • JSONの構造に応じた柔軟な処理フロー
  • アップロードファイルのバリデーション
  • ダウンロードファイルにタイムスタンプを付ける
  • アプリ全体のUI/UXを改善するTips集

またブログ公開後に「このコードを読者に使ってもらうにはどう共有する?」などもサポートできますよ。

それでは、執筆頑張ってください✍️!応援しています!

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?