You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Plotly Dash多Excel文件上传合并的持久化问题求助

解决Dash多Excel上传合并的数据持久化问题

问题背景

使用Plotly Dash开发交互式仪表盘,单Excel工作簿上传、格式化展示柱状图功能正常。添加多工作簿上传并合并为单个DataFrame功能后,出现数据持久化异常:即便将dcc.Store的storage_type设为memory,浏览器刷新后数据仍会保留。

当前困境:全局变量dfmeans是实现多文件合并的唯一方式,但会导致服务器端数据持久化;若将其放入parse_contents函数,每次上传新文件时数据都会被覆盖。

问题根源

  1. 全局变量的副作用:服务器端的全局变量dfmeans会在所有用户会话间共享,且服务器重启前不会自动清空。浏览器刷新后,服务器仍会使用之前存储在dfmeans中的数据,导致刷新后数据保留。
  2. dcc.Store使用错误:当前代码在每个文件的parse_contents中都生成一个dcc.Store组件,会造成重复赋值,无法正确管理全局合并数据。

解决方案

核心思路是用dcc.Store替代全局变量存储合并数据,将合并逻辑移至回调中,基于Store的现有状态实现增量合并,避免服务器端全局变量的持久化问题:

  • 移除全局变量dfmeans,仅在回调中处理单文件数据和合并逻辑
  • 在update_output回调中,收集所有上传文件的处理结果,合并后更新到dcc.Store
  • 图表和表格展示完全依赖dcc.Store中的数据,确保浏览器刷新后数据清空(符合memory存储类型的预期)

完整修正代码

import base64
import datetime
import io
import re

import dash
from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html
import dash_table
import plotly.express as px

import pandas as pd
from read_workbook import *

suppress_callback_exceptions=True

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

app.layout = html.Div([
    dcc.Store(id='stored-data', storage_type='memory'),
    dcc.Upload(
        id='upload-data',
        children=html.Div([
            'Drag and Drop or ',
            html.A('Select Files')
        ]),
        style={
            'width': '100%',
            'height': '60px',
            'lineHeight': '60px',
            'borderWidth': '1px',
            'borderStyle': 'dashed',
            'borderRadius': '5px',
            'textAlign': 'center',
            'margin': '10px'
        },
        multiple=True
    ),
    html.Div(id='output-div'),
    html.Div(id='output-datatable'),
])

def parse_single_file(contents, filename):
    """仅处理单个Excel文件,返回该文件的聚合后DataFrame"""
    content_type, content_string = contents.split(',')
    decoded = base64.b64decode(content_string)
    try:
        workbook_xl = pd.ExcelFile(io.BytesIO(decoded))
        
        def get_all_months(workbook_xl):
            months = ['July', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'June']
            xl_file = pd.ExcelFile(workbook_xl)
            months_data = []
            for month in months:
                months_data.append(get_month_dataframe(xl_file, month))
            return pd.concat(months_data)
        
        df = get_all_months(workbook_xl)
        df['value'] = df['value'].astype(float)
        dfmean = df.groupby(['Date', 'variable'], sort=False)['value'].mean().round(2).reset_index()
        
        # 返回处理后的DataFrame和文件名,用于后续展示
        return dfmean, filename
    
    except Exception as e:
        print(e)
        return None, filename

@app.callback(
    [Output('stored-data', 'data'), Output('output-datatable', 'children')],
    Input('upload-data', 'contents'),
    State('upload-data', 'filename'),
    State('stored-data', 'data')
)
def update_output(list_of_contents, list_of_names, stored_data):
    # 初始化合并后的DataFrame
    merged_df = pd.DataFrame()
    
    # 如果已有存储数据,先加载
    if stored_data is not None:
        merged_df = pd.DataFrame(stored_data)
    
    # 处理新上传的文件
    if list_of_contents is not None:
        processed_results = [parse_single_file(c, n) for c, n in zip(list_of_contents, list_of_names)]
        # 过滤处理失败的文件
        valid_dfs = [df for df, _ in processed_results if df is not None]
        
        if valid_dfs:
            # 合并新数据到已有数据
            new_merged_df = pd.concat([merged_df] + valid_dfs, ignore_index=True)
            merged_df = new_merged_df
    
    # 生成表格展示内容
    table_children = []
    if not merged_df.empty:
        table_children.append(
            html.Div([
                html.H5('合并后数据'),
                dash_table.DataTable(
                    data=merged_df.to_dict('records'),
                    columns=[{'name': i, 'id': i} for i in merged_df.columns],
                    page_size=15
                ),
                html.Hr()
            ])
        )
        # 添加每个上传文件的提示
        for _, name in processed_results:
            table_children.append(html.Div([html.H6(f"已处理文件: {name}")]))
    
    return merged_df.to_dict('records'), table_children

@app.callback(Output('output-div', 'children'),
              Input('stored-data','data'))
def make_graphs(data):
    df_agg = pd.DataFrame(data)
    
    if df_agg.empty:
        return html.Div("请上传Excel文件生成图表")
    else:
        bar_fig = px.bar(df_agg, x='Date', y='value', color='variable', barmode='group')
        return dcc.Graph(figure=bar_fig)
    
if __name__ == '__main__':
    app.run_server(debug=True)

关键修改说明

  1. 拆分处理逻辑:将单个文件处理和多文件合并分离,parse_single_file仅负责解析单个Excel并返回聚合结果,避免全局变量依赖。
  2. 基于Store状态合并:update_output回调接收stored-data的现有状态,将新上传文件的数据与已有数据合并,再更新回Store,实现增量合并。
  3. 统一Store管理:仅保留一个全局的dcc.Store组件,确保所有数据流转都基于前端状态,服务器端无持久化的全局数据,刷新浏览器后Store数据清空,符合预期。

内容的提问来源于stack exchange,提问作者John Conor

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 01:40:22