Вопрос или проблема
Идея заключается в том, что несколько файлов Excel могут быть загружены и отображены, также есть возможность удалить каждый файл из представления и из папки загрузки. Удаление файла не выполняется. Вот код с ошибкой “Имя файла равно None, пропуск удаления”. Кнопка удаления активна и пытается удалить файл, но в конечном итоге не удается.
import os
from dash import dcc, html, dash_table, Input, Output, State, callback, callback_context
from dash.dependencies import ALL
import dash_bootstrap_components as dbc
import base64
import datetime
import io
import pandas as pd
# Импорт функций управления файлами
from constants.index import table_style, cell_style, header_style
from components.image_component import create_image
# Получить иконки загрузки
upload_icon = "assets/images/icons/upload-colored-icon.png"
download_icon = "assets/images/icons/download-icon.png"
# Директория загрузки
upload_directory = "uploaded_files"
# Убедитесь, что директория загрузки существует
if not os.path.exists(upload_directory):
os.makedirs(upload_directory)
# Определите стили пользовательского загрузчика
loader_style = {
"position": "fixed",
"top": "0",
"left": "0",
"width": "100%",
"height": "100%",
"backgroundColor": "rgba(255, 255, 255, 0.8)", # Полупрозрачный фон
"display": "flex",
"justifyContent": "center",
"alignItems": "center",
"zIndex": "9999", # Убедитесь, что он сверху всего
}
def file_uploader():
return html.Div(
[
dcc.Upload(
id="upload-data",
children=dbc.Button(
className="upload-button",
id="upload-button",
children=[
create_image(image_path=upload_icon, image_style="icon"),
html.Span("Загрузить файлы"),
],
),
multiple=True,
),
dbc.Alert("Загруженные файлы должны соответствовать заданным форматам.", className="upload-alert"),
dcc.Loading(
id="loading-spinner",
type="circle",
fullscreen=True,
style=loader_style,
children=html.Div(id="output-data-upload"),
),
]
)
def save_file(contents, filename):
"""Сохранить загруженный файл на сервере."""
content_type, content_string = contents.split(",")
decoded = base64.b64decode(content_string)
# Создать полный путь к файлу
file_path = os.path.join(upload_directory, filename)
# Записать файл в зависимости от его типа
try:
with open(file_path, "wb") as f:
f.write(decoded)
except Exception as e:
print(f"Ошибка при сохранении файла {filename}: {e}")
return html.Div([f"Произошла ошибка при сохранении файла: {e}"])
return file_path # Вернуть путь к сохранённому файлу
def load_saved_files():
"""Загрузить и отобразить сохранённые файлы с сервера."""
saved_files = []
for filename in os.listdir(upload_directory):
file_path = os.path.join(upload_directory, filename)
# Отобразить каждый сохранённый файл
if filename.endswith(".csv"):
df = pd.read_csv(file_path)
elif filename.endswith(".xls") or filename.endswith(".xlsx"):
df = pd.read_excel(file_path)
else:
continue
saved_files.append(
html.Div(
[
html.Div(
className="flex",
children=[
html.H5(filename),
dbc.Button(
"Удалить",
id={"type": "delete-button", "index": filename},
color="danger",
n_clicks=0,
),
],
),
dash_table.DataTable(
df.to_dict("records"), [{"name": i, "id": i} for i in df.columns],
page_size=10,
style_table=table_style,
style_cell=cell_style,
style_header=header_style,
fixed_rows={"headers": True},
),
html.Hr(),
]
)
)
return saved_files
def parse_contents(contents, filename, date):
"""Сохранить и разобрать загруженный файл."""
# Сохраните файл на сервере и получите его путь
file_path = save_file(contents, filename)
decoded = base64.b64decode(contents.split(",")[1])
try:
if filename.endswith(".csv"):
df = pd.read_csv(io.StringIO(decoded.decode("utf-8")))
elif filename.endswith(".xls") or filename.endswith(".xlsx"):
df = pd.read_excel(io.BytesIO(decoded))
except Exception as e:
print(e)
return html.Div(["Произошла ошибка при обработке этого файла."])
return html.Div(
[
html.Div(
className="flex",
children=[
html.H5(filename),
html.Div(f"Файл {filename} успешно сохранён по адресу {file_path}."),
],
),
html.H6(datetime.datetime.fromtimestamp(date)),
dash_table.DataTable(
df.to_dict("records"),
[{"name": i, "id": i} for i in df.columns],
page_size=10,
style_table=table_style,
style_cell=cell_style,
style_header=header_style,
fixed_rows={"headers": True},
),
html.Hr(),
html.Div("Сырые данные"),
html.Pre(
contents[0:200] + "...",
style={"whiteSpace": "pre-wrap", "wordBreak": "break-all"},
),
]
)
def delete_file(filename):
"""Удалить выбранный файл из директории загрузки."""
file_path = os.path.join(upload_directory, filename)
if os.path.exists(file_path):
os.remove(file_path)
print(f"Файл {filename} удалён.")
else:
print(f"Файл {filename} не найден.")
@callback(
Output("output-data-upload", "children"),
Input("upload-data", "contents"),
State("upload-data", "filename"),
State("upload-data", "last_modified"),
Input({"type": "delete-button", "index": ALL}, "n_clicks"),
State({"type": "delete-button", "index": ALL}, "index"),
)
def update_output(list_of_contents, list_of_names, list_of_dates, delete_clicks, delete_filenames):
ctx = callback_context
triggered = ctx.triggered
# Обрабатывать удаление файла
if triggered and "delete-button" in triggered[0]["prop_id"]:
for i, n_clicks in enumerate(delete_clicks):
if n_clicks > 0:
filename_to_delete = delete_filenames[i]
# Отладка: убедитесь, что имя файла передано
if filename_to_delete is not None:
print(f"Попытка удалить файл: {filename_to_delete}")
delete_file(filename_to_delete)
else:
print("Имя файла равно None, пропуск удаления")
break # Убедитесь, что мы удаляем только один файл за клик
# Отобразить сохранённые файлы или вновь загруженные файлы
if list_of_contents is not None:
children = [parse_contents(c, n, d) for c, n, d in zip(list_of_contents, list_of_names, list_of_dates)]
return children
else:
return load_saved_files()
Ответ или решение
Для решения проблемы с удалением загруженных файлов Excel из папки загрузок в вашем Dash-приложении, необходимо внести несколько изменений в ваш код, чтобы обеспечить правильное обращение с файлами.
Проблема, с которой вы столкнулись, связана с тем, что переменная filename_to_delete
становится None
, что приводит к сообщению об ошибке "Filename is None, skipping deletion". Это может произойти, если delete_filenames[i]
возвращает некорректное значение.
Шаги для решения проблемы:
-
Проверьте правильность получения имени файла для удаления.
Обратите внимание на то, что при обращении кState({"type": "delete-button", "index": ALL}, "index")
, возвращается список словарей с именами файлов. Убедитесь, что они правильно извлекаются. -
Корректировка функции удаления.
Убедитесь, что функцияdelete_file
корректно получает имя файла для удаления. -
Оптимизация кода.
Можно упростить логику обработки нажатия кнопки удаления и устранить лишние проверки.
Вот исправленный код подключения для функции удаления:
@callback(
Output("output-data-upload", "children"),
Input("upload-data", "contents"),
State("upload-data", "filename"),
State("upload-data", "last_modified"),
Input({"type": "delete-button", "index": ALL}, "n_clicks"),
State({"type": "delete-button", "index": ALL}, "id"),
)
def update_output(list_of_contents, list_of_names, list_of_dates, delete_clicks, delete_ids):
ctx = callback_context
# Используем флаг для отслеживания нажатий кнопок
if ctx.triggered:
button_id = ctx.triggered[0]["prop_id"].split('.')[0]
if "delete-button" in button_id:
index_to_delete = eval(button_id)["index"] # Получаем индекс файла для удаления
if index_to_delete:
filename_to_delete = index_to_delete
print(f"Attempting to delete file: {filename_to_delete}")
delete_file(filename_to_delete)
else:
print("Filename is None, skipping deletion")
# Если содержимое загружено, парсим файлы
if list_of_contents is not None:
children = [parse_contents(c, n, d) for c, n, d in zip(list_of_contents, list_of_names, list_of_dates)]
return children
else:
return load_saved_files()
Основные изменения:
- Мы изменили то, как мы получаем
filename_to_delete
изdelete_ids
. Теперь мы напрямую используемeval(button_id)
для извлечения имени файла, что должно устранить проблему сNone
. - Обработчик нажатий теперь более очевидный и понятный.
Надеюсь, данные рекомендации помогут вам устранить проблему с удалением файлов. Если возникнут дополнительные вопросы, не стесняйтесь обращаться за помощью.