如何修复Django导出Excel函数的内存占用过高问题?
解决Django导出Excel内存占用过高问题
原代码核心问题分析
- 全量数据加载到内存:使用tablib的Dataset存储所有数据后一次性导出,导致1GB内存占用。
- 冗余密码生成:循环10次生成密码但每次重置变量,最终仅保留最后一次结果,完全无效。
- 路径拼接不规范:直接字符串拼接路径易出错,且重复代码过多。
- 全量数据库查询:一次性加载所有Product数据到内存,未做分批处理。
- 手动内存回收冗余:手动删除变量并调用
gc.collect()属于多余操作,Python垃圾回收机制会自动处理。
优化方案
- 流式写入Excel:用
xlsxwriter逐行写入数据,避免全量数据占用内存。 - 优化密码生成:仅生成一次随机密码,移除无效循环。
- 规范路径处理:用
os.path.join拼接路径,提升代码稳定性和可读性。 - 分批查询数据:使用
iterator()分批获取数据库数据,减少内存占用。 - 简化文件操作:用
glob批量删除旧文件,替代重复的listdir操作。 - 移除手动内存回收:删除不必要的
del和gc.collect()调用。
优化后的完整代码
@sync_to_async @login_required def export_excel(request): import os import random import datetime import zipfile import xlsxwriter from django.conf import settings from glob import glob # 生成随机密码(仅生成一次) chars = 'abcdefghijklnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' password = ''.join(random.choice(chars) for _ in range(10)) # 规范生成路径 user_id = str(request.user.usrid.listusers_id) base_path = os.path.join(settings.MEDIA_ROOT, 'pricenew', user_id) paths = { 'zip': os.path.join(base_path, 'zip'), 'rar': os.path.join(base_path, 'rar'), 'xlsx': os.path.join(base_path, 'xlsx'), } # 创建目录(已存在则忽略) for path in paths.values(): os.makedirs(path, exist_ok=True) # 检查缓存文件是否有效 xlsx_files = glob(os.path.join(paths['xlsx'], '*')) zip_files = glob(os.path.join(paths['zip'], '*')) pricesecret = Price.objects.get(id=1) lock_file = os.path.join(paths['rar'], f"{pricesecret.url}.lock") if (os.path.exists(lock_file) and xlsx_files and os.stat(xlsx_files[0]).st_size != 0 and zip_files and os.stat(zip_files[0]).st_size != 0): zip_filename = os.path.basename(zip_files[0]) url = f"https://localhost/media/pricenew/{user_id}/zip/{zip_filename}" return JsonResponse({'pricesecret': zip_filename, 'url': url}) else: # 删除所有旧文件 for path in paths.values(): for file_path in glob(os.path.join(path, '*')): os.remove(file_path) # 创建锁文件 with open(lock_file, "wb"): pass # 生成Excel文件路径 xlsx_file_path = os.path.join(paths['xlsx'], f'price_{password}.xlsx') # 流式写入Excel workbook = xlsxwriter.Workbook(xlsx_file_path) worksheet = workbook.add_worksheet("Прайс") # 写入表头 headers = ( 'Код', 'Артикул', 'Наименование', 'Производитель', 'Ед', 'Цена,руб', 'Остаток', 'Заявка', 'Норма отпуска', 'Новинка!' ) for col_num, header in enumerate(headers): worksheet.write(0, col_num, header) # 分批获取产品数据(iterator避免全量加载) products = Product.objects.filter( stock__gt=0, published=True ).select_related('brand', 'product_sklad').prefetch_related( 'brand__clt1__client_id__discount_list_k1', 'category__parent' ).exclude(product_sklad__gte=1).iterator() # 获取禁用品牌列表 banned_brands = list(Salenban.objects.filter( client_id=request.user.usrid.listusers_id, published=1 ).values_list('mf_un', flat=True)) # 逐行写入数据 row_num = 1 for book in products: zayvka = "" artikul = book.artikul if book.artikul != "None" else "" brand = book.brand if book.brand != "None" else "" new_product = book.new_product if book.new_product != 0 else "" stock = ">50" if book.stock > 50 else book.stock row_data = ( book.id_p, artikul, book.name, brand, book.shtuk_e, round(book.user_max2(), 2), stock, zayvka, book.min_kol, new_product ) for col_num, value in enumerate(row_data): worksheet.write(row_num, col_num, value) row_num += 1 workbook.close() # 压缩Excel文件 zip_file_path = os.path.join(paths['zip'], f'price_{password}.zip') with zipfile.ZipFile(zip_file_path, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as zf: zf.write(xlsx_file_path, os.path.basename(xlsx_file_path)) # 返回下载链接 url = f"https://localhost/media/pricenew/{user_id}/zip/price_{password}.zip" return JsonResponse({'pricesecret': f"{password}.zip", 'url': url})
关键优化点说明
- 流式Excel写入:xlsxwriter直接操作文件句柄,逐行写入数据,内存仅占用单条记录的大小。
- 数据库迭代查询:
iterator()方法让Django不缓存所有查询结果,逐批从数据库获取数据。 - 路径规范:通过
os.path.join和settings.MEDIA_ROOT管理路径,避免硬编码错误。 - 简化文件操作:
glob批量匹配文件,删除旧文件的逻辑更简洁高效。 - 冗余代码移除:删除无效循环、手动内存回收等不必要代码,提升性能和可读性。
内容的提问来源于stack exchange,提问作者slavyan
相关产品推荐
相关产品推荐

