Khi phát triển ứng dụng web bằng Django, việc cung cấp khả năng tải về tài liệu như báo cáo Excel hay PDF là nhu cầu phổ biến. Framework này cung cấp nhiều lớp phản hồi (response classes) để xử lý vấn đề này một cách hiệu quả tùy theo tình huống cụ thể.
Xử lý tập tin có sẵn trên hệ thống lưu trữ
Tình huống thường gặp nhất là file đã tồn tại trên đĩa cứng của server và cần chuyển đến người dùng qua kết nối HTTP.
Sử dụng HttpResponse với dữ liệu nhị phân
Cách tiếp cận cơ bản là đọc toàn bộ nội dung file vào bộ nhớ và gán vào đối tượng HttpResponse. Dưới đây là ví dụ sử dụng chế độ đọc nhị phân (binary mode):
from django.http import HttpResponse
from urllib.parse import quote
def download_file_simple(request):
file_path = '/absolute/path/to/template.xlsx'
try:
with open(file_path, 'rb') as f:
content = f.read()
response = HttpResponse(content)
response['Content-Type'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
# Mã hóa tên file an toàn cho header
safe_filename = quote('my_export.xlsx', safe='')
response['Content-Disposition'] = f'attachment; filename="{safe_filename}"'
return response
except FileNotFoundError:
return HttpResponse("Không tìm thấy tệp", status=404)
Một biến thể khác là đóng gói dữ liệu trong đối tượng BytesIO. Cách này có thể hữu ích nếu bạn cần xử lý luồng trước khi trả về, tuy nhiên với file tĩnh đơn thuần, việc đọc trực tiếp cũng tương đương:
import io
from django.http import HttpResponse
from urllib.parse import quote
def download_with_io(request):
file_path = '/absolute/path/to/template.xlsx'
with open(file_path, 'rb') as f:
file_content = f.read()
stream = io.BytesIO(file_content)
response = HttpResponse(stream.getvalue())
response['Content-Type'] = 'application/octet-stream'
response['Content-Disposition'] = 'attachment; filename="data.xlsx"'
return response
Phản hồi FileResponse chuyên biệt
Lớp FileResponse kế thừa từ StreamedHttpResponse và tối ưu hơn cho việc gửi file vật lý, tự động xử lý việc đóng tệp và các header liên quan:
from django.http import FileResponse
from urllib.parse import quote
def download_by_file_response(request):
file_path = '/absolute/path/to/report.pdf'
filename = 'report_final.pdf'
try:
f = open(file_path, 'rb')
response = FileResponse(f, as_attachment=True, filename=filename)
response['Content-Type'] = 'application/pdf'
return response
finally:
f.close()
Sử dụng StreamingHttpResponse cho file lớn
Nếu kích thước tệp quá lớn gây tràn RAM, nên sử dụng phương thức streaming. Điều này giúp trình duyệt nhận dữ liệu từng phần nhỏ (chunks) thay vì chờ tải xong mới hiển thị:
from django.http import StreamingHttpResponse
def large_file_stream_iterator(filename, chunk_size=4096):
with open(filename, 'rb') as f:
while True:
data = f.read(chunk_size)
if not data:
break
yield data
def stream_big_file(request):
target_file = '/var/data/large_dump.zip'
output_name = 'dump_2023.zip'
def generate_chunks():
return large_file_stream_iterator(target_file)
response = StreamingHttpResponse(generate_chunks(), content_type='application/zip')
response['Content-Length'] = str(os.path.getsize(target_file))
response['Content-Disposition'] = f'attachment; filename="{output_name}"'
return response
Tạo và trả về tập tin động
Trong một số trường hợp, file chưa tồn tại trên ổ cứng mà được sinh ra từ dữ liệu trong bộ nhớ (ví dụ: xuất báo cáo tức thời).
Dưới đây là quy trình sử dụng thư viện openpyxl để tạo file Excel ngay lập tức rồi đóng gói vào phản hồi HTTP thông qua bộ đệm BytesIO:
from django.http import HttpResponse
import io
from openpyxl import Workbook
from urllib.parse import quote
def generate_excel_report(request):
wb = Workbook()
ws = wb.active
# Thêm dữ liệu mẫu vào sheet
ws.cell(row=1, column=1, value='ID')
ws.cell(row=1, column=2, value='Tên sản phẩm')
ws.cell(row=2, column=1, value=101)
ws.cell(row=2, column=2, value='Laptop Gaming')
# Ghi nội dung xuống bộ nhớ đệm
buffer = io.BytesIO()
wb.save(buffer)
buffer.seek(0) # Di chuyển con trỏ về đầu file
# Tạo response
response = HttpResponse(buffer.getvalue(), content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
encoded_name = quote('dynamic_export.xlsx', safe='')
response['Content-Disposition'] = f'attachment; filename="{encoded_name}"'
return response
Tương tự như vậy, đối với các loại file động khác, bạn cũng có thể thay thế HttpResponse ở trên bằng FileResponse hoặc StreamingHttpResponse tùy thuộc vào khối lượng dữ liệu được tạo ra.