Как запустить загрузку и вывести ответ без удара по диску?
Итак, у меня есть форма проверки научных данных Excel-файла в django, которая работает хорошо. Она работает итеративно. Пользователи могут загружать файлы по мере накопления новых данных, которые они добавляют в свое исследование. DataValidationView
каждый раз проверяет файлы и представляет пользователю отчет об ошибках, в котором перечислены проблемы в данных, которые он должен исправить.
Недавно мы поняли, что ряд ошибок (но не все) можно исправить автоматически, поэтому я работал над тем, чтобы создать копию файла с рядом исправлений. Поэтому мы переименовали страницу с формой "валидации" в "страницу создания заявки". Каждый раз, когда они загружают новый набор файлов, они должны получать отчет об ошибке, но также автоматически получать загруженный файл с рядом исправлений.
Только сегодня я узнал, что не существует способа одновременно отрисовать шаблон и запустить загрузку, что вполне логично. Однако я планировал не допускать попадания сгенерированного файла с исправлениями на диск.
Есть ли способ представить шаблон с ошибками и автоматически запустить загрузку без предварительного сохранения файла на диск?
Вот мой form_valid
метод на данный момент (без запуска загрузки, но я уже начал создавать файл, прежде чем понял, что и загрузка, и рендеринг шаблона не будут работать):
def form_valid(self, form):
"""
Upon valid file submission, adds validation messages to the context of
the validation page.
"""
# This buffers errors associated with the study data
self.validate_study()
# This generates a dict representation of the study data with fixes and
# removes the errors it fixed
self.perform_fixes()
# This sets self.results (i.e. the error report)
self.format_validation_results_for_template()
# HERE IS WHERE I REALIZED MY PROBLEM. I WANTED TO CREATE A STREAM HERE
# TO START A DOWNLOAD, BUT REALIZED I CANNOT BOTH PRESENT THE ERROR REPORT
# AND START THE DOWNLOAD FOR THE USER
return self.render_to_response(
self.get_context_data(
results=self.results,
form=form,
submission_url=self.submission_url,
)
)
До того, как я столкнулся с этой проблемой, я скомпилировал псевдокод для потоковой передачи файла... Это совершенно не проверено:
import pandas as pd
from django.http import HttpResponse
from io import BytesIO
def download_fixes(self):
excel_file = BytesIO()
xlwriter = pd.ExcelWriter(excel_file, engine='xlsxwriter')
df_output = {}
for sheet in self.fixed_study_data.keys():
df_output[sheet] = pd.DataFrame.from_dict(dfs_dict[sheet])
df_output[sheet].to_excel(xlwriter, sheet)
xlwriter.save()
xlwriter.close()
# important step, rewind the buffer or when it is read() you'll get nothing
# but an error message when you try to open your zero length file in Excel
excel_file.seek(0)
# set the mime type so that the browser knows what to do with the file
response = HttpResponse(excel_file.read(), content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
# set the file name in the Content-Disposition header
response['Content-Disposition'] = 'attachment; filename=myfile.xlsx'
return response
Так что я думаю, что либо мне нужно:
- Сохранить файл на диске, а затем придумать способ заставить страницу результатов начать его загрузку
- Каким-то образом отправить данные, встроенные в шаблон результатов, и отправить их обратно через javascript, чтобы они превратились в поток загрузки файла
- Сохранить файл каким-то образом в памяти и запустить его загрузку из шаблона результатов?
Как лучше всего этого добиться?
ОБНОВЛЕННЫЕ МЫСЛИ:
Недавно я проделал простой трюк с файлом tsv
, где я встроил содержимое файла в результирующий шаблон с кнопкой загрузки, которая использовала javascript для захвата innerHTML
тегов вокруг данных и начала "загрузки".
Я подумал, что если я закодирую данные, то, вероятно, смогу сделать нечто подобное и с содержимым файла excel. Я мог бы закодировать его в base64.
Я просмотрел прошлые исследования. Самое большое из них было 115 кб. Скорее всего, этот размер вырастет на порядок, но пока 115 Кб - это потолок.
Я погуглил, чтобы найти способ встроить данные в шаблон, и получил это:
import base64
with open(image_path, "rb") as image_file:
image_data = base64.b64encode(image_file.read()).decode('utf-8')
ctx["image"] = image_data
return render(request, 'index.html', ctx)
Недавно я играл с кодировкой base64 в javascript для некоторой несвязанной работы, что привело меня к мысли, что встраивание возможно. Я даже могу запускать его автоматически. У кого-нибудь есть какие-нибудь предостережения по поводу такого способа?
Для потомков, руководство по HTTP 1.1 multipart/byteranges Response
реализованное в Django. Более подробную информацию о multipart/byteranges смотрите в RFC 7233.
Формат полезной нагрузки multipart/byteranges выглядит следующим образом:
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=3d6b6a416f9b5
--3d6b6a416f9b5
Content-Type: application/octet-stream
Content-Range: bytes 0-999/2000
<octet stream data 1>
--3d6b6a416f9b5
Content-Type: application/octet-stream
Content-Range: bytes 1000-1999/2000
<octet stream data 2>
--3d6b6a416f9b5
Content-Type: application/json
Content-Range: bytes 0-441/442
<json data>
--3d6b6a416f9b5
Content-Type: text/html
Content-Range: bytes 0-543/544
<html string>
--3d6b6a416f9b5--
Вы поняли идею. Первые два - это одни и те же двоичные данные, разделенные на два потока, третий - строка JSON, отправленная в одном потоке, и четвертый - строка HTML, отправленная в одном потоке.
В вашем случае вы отправляете File
вместе с шаблоном HTML
.
from io import BytesIO, StringIO
from django.template.loader import render_to_string
from django.http import StreamingHttpResponse
def stream_generator(streams):
boundary = "3d6b6a416f9b5"
for stream in streams:
if isinstance(stream, BytesIO):
data = stream.getvalue()
content_type = 'application/octet-stream'
elif isinstance(stream, StringIO):
data = stream.getvalue().encode('utf-8')
content_type = 'text/html'
else:
continue
stream_length = len(data)
yield f'--{boundary}\r\n'
yield f'Content-Type: {content_type}\r\n'
yield f'Content-Range: bytes 0-{stream_length-1}/{stream_length}\r\n'
yield f'\r\n'
yield data
yield f'\r\n'
yield f'--{boundary}--\r\n'
def multi_stream_response(request):
streams = [
excel_file, # The File provided in the OP. It is a BytesIO object.
StringIO(render_to_string('index.html', request=request))
]
return StreamingHttpResponse(stream_generator(streams), content_type='multipart/byteranges; boundary=3d6b6a416f9b5')
Посмотрите этот пример [stackoverflow] по разбору multipart/byteranges на клиенте.
@Chukwujiobi_Canon ответ отличный, и масштабируемый, хотя мне потребовался целый день, чтобы почти заставить его работать, и это все еще не совсем так. Я ожидаю, что мне понадобится еще один день, чтобы довести его до совершенства, однако, учитывая, что мои файлы имеют размер менее 1 мб, я решил исследовать свою первоначальную идею: встроить содержимое файла в base64 в рендеринг страницы (скрытый), и запустить его загрузку автоматически в javascript.
На это ушло меньше часа, оно полностью функционально, и потребовалось совсем немного кода. Правда, часть кода была повторно использована при работе над другим решением.
Вот как я генерирую содержимое файла. Я включил метод, который принимает dict в стиле pandas и преобразует его в xlsxwriter (pip install xlsxwriter
).
import xlswriter
def form_valid(self, form):
# This buffers errors associated with the study data
self.validate_study()
# This generates a dict representation of the study data with fixes and
# removes the errors it fixed
self.perform_fixes()
# This sets self.results (i.e. the error report)
self.format_validation_results_for_template()
study_stream = BytesIO()
xlsxwriter = self.create_study_file_writer(study_stream)
xlsxwriter.close()
# Rewind the buffer so that when it is read(), you won't get an error about opening a zero-length file in Excel
study_stream.seek(0)
study_data = base64.b64encode(study_stream.read()).decode('utf-8')
study_filename = self.animal_sample_filename
if self.animal_sample_filename is None:
study_filename = "study.xlsx"
return self.render_to_response(
self.get_context_data(
results=self.results,
form=form,
submission_url=self.submission_url,
study_data=study_data,
study_filename=study_filename,
),
)
def create_study_file_writer(self, stream_obj: BytesIO):
xlsxwriter = pd.ExcelWriter(stream_obj, engine='xlsxwriter')
# This iterates over the desired order of the sheets and their columns
for order_spec in self.get_study_sheet_column_display_order():
sheet = order_spec[0]
columns = order_spec[1]
# Create a dataframe and add it as an excel object to an xlsxwriter sheet
pd.DataFrame.from_dict(self.dfs_dict[sheet]).to_excel(
excel_writer=xlsxwriter,
sheet_name=sheet,
columns=columns
)
return xlsxwriter
Это тег в шаблоне, в котором я отображаю данные
<pre style="display: none" id="output_study_file">{{study_data}}</pre>
Вот javascript, который "скачивает" файл:
document.addEventListener("DOMContentLoaded", function(){
// If there is a study file that was produced
if ( typeof study_file_content_tag !== "undefined" && study_file_content_tag ) {
browserDownloadExcel('{{ study_filename }}', study_file_content_tag.innerHTML)
}
})
function browserDownloadExcel (filename, base64_text) {
const element = document.createElement('a');
element.setAttribute(
'href',
'data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,' + encodeURIComponent(base64_text)
);
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}