xadmin后台分段导出避免timeout
一、问题
xadmin后台功能很强大,特别在导出的时候格式有xls/xlsx、csv、xml、json。实际常用的还是前面2种。xls格式使用的xlwt,有个缺陷,导出数据过大时,会报ValueError: row index was 65536, not allowed by .xls format ...,使用xlsxwriter导出为xlsx格式,做个分页导出,十几万都不在话下(亲测)。
在xadmin/templates/xadmin/blocks/model_list.top_toolbar.importexport.export.html源码中可以看到这5种格式:

后台导出按钮下拉只有4种:
,xlsx格式在源码中可以找到答案。
在xadmin/plugins/export.py中,
,只有安装xlsxwriter依赖才会显示
这个下拉按钮。
二、思路
xadmin目前是在点下载等待所有符合条件数据下载到内存再生成文件交给浏览器下载,如果下载数据太大就会拖死这个请求。所以在下载数据在内存过程中分段下载,前后端配合传参,将参数存到redis(django session是存数据库django_session表中,存session会报错Packet for query is too large,需要修改mysql参数,max_allowed_packet,默认是1兆,所以存储到redis),下载完删除。页数自增,每次后端根据总数和每次下载数确定状态还需不需要下载,最后标记下载完成,生成文件响应。
三、实现效果图



四、前后端代码
4-1.xadmin/templates/xadmin/blocks/model_list.top_toolbar.importexport.export.html
{% load i18n %}
<div class="btn-group export">
<a class="dropdown-toggle btn btn-default btn-sm" data-toggle="dropdown" href="#">
<i class="fa fa-share"></i> {% trans "Export" %} <span class="caret"></span>
</a>
<ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
{% for et in export_types %}
<li><a data-toggle="modal" data-target="#export-modal-{{et.type}}"><i class="fa fa-arrow-circle-down"></i> {% trans "Export" %} {{et.name}}</a></li>
{% endfor %}
</ul>
{% for et in export_types %}
<div id="export-modal-{{et.type}}" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<form method="get" action="">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h4 class="modal-title">{% trans "Export" %} {{et.name}}</h4>
</div>
<div class="modal-body">
{{ form_params|safe }}
<input type="hidden" name="export_type" value="{{et.type}}">
<label class="checkbox">
{% if et.type == "xlsx" %}
<input type="checkbox" name="export_xlsx_header" checked="checked" value="on"> {% trans "Export all data." %}
{% endif %}
{% if et.type == "xls" %}
<input type="checkbox" name="export_xls_header" checked="checked" value="on"> {% trans "Export all data." %}
{% endif %}
{% if et.type == "csv" %}
<input type="checkbox" name="export_csv_header" checked="checked" value="on"> {% trans "Export all data." %}
{% endif %}
{% if et.type == "xml" %}
<input type="checkbox" name="export_xml_format" checked="checked" value="on"> {% trans "Export with format." %}
{% endif %}
{% if et.type == "json" %}
<input type="checkbox" name="export_json_format" checked="checked" value="on"> {% trans "Export with format." %}
{% endif %}
</label>
{# <label class="checkbox">#}
{# <input type="checkbox" name="all" value="on"> {% trans "Export all data." %}#}
{# </label>#}
<input id="export_id" name="export_id" type="hidden" />
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Close" %}</button>
<button class="btn btn-success" id="export" type="submit" style="display:none;"><i class="fa fa-share"></i> {% trans "Export" %}</button>
<button class="btn btn-success" id="export_new" type="button"><i class="fa fa-share"></i> {% trans "Export" %}</button>
</div>
</form>
</div><!-- /.modal-content -->
</div><!-- /.modal-dalog -->
</div><!-- /.modal -->
{% endfor %}
</div>
<script>
var lock = false;
var el = $('#export');
var elNew = $('#export_new');
var text = elNew.text();
callback = function(status){
lock = true;
if(status){
elNew.hide();
el.show();
}else{
elNew.text(text);
}
}
fetch = function(p,id,queryData){
baseData = {
'ajax': 1,
'_do_':'export',
'p': p,
'export_id': id,
'export_type':'xlsx',
}
queryData = queryData||{}
$.ajax({
'data': $.extend({},baseData,queryData),
'headers': {
'X-Requested-With':'-'
},
'success': function (res) {
if(res.status===0){
elNew.text('正在生成报表('+res.len+'/'+res.total+')');
fetch(++p,id)
}else{
callback(true)
}
},
'error':function(){
callback(false)
}
});
};
query = function(){
export_type = $("input[name='export_type']").val();
return {
'export_type':export_type,
}
}
elNew.click(function(){
if (lock){
return false
}
lock = true;
elNew.text('正在生成报表...');
var id =Math.random();
$('#export_id').val(id);
fetch(0,id,query());
});
</script>
4-2.xadmin/plugins/export.py
def _down(self,context):
# session中存储export_id {'dic':{'export_id':[]}} session_dic = {'export_id':[]} # params_obj = {'export_id': export_id,
# 'n': n,
# 'ajax': ajax}
from sms.channels import reids_db # if 'dic' in self.request.session:
# session_dic = self.request.session['dic']
# else:
# # 初始化dic
# self.request.session['dic'] = session_dic = {} if not reids_db.get('dic'):
reids_db.set('dic', {}, 24 * 3600)
# redis取出dic值为byte类型 使用eval转为字典
session_dic = eval(reids_db.get('dic')) # 从request对象获取export_id
if hasattr(self.request,'params_obj'):
export_id = self.request.params_obj['export_id']
else:
datas = self._get_datas(context)
return datas,{}
if export_id:
if export_id in session_dic:
# export_id 是否在redis中
global session_list
session_list = session_dic[export_id]
else:
# 初始化export_id
session_dic[export_id] = session_list = []
reids_db.set('dic', session_dic, 24 * 3600) # 初始化传给前端的数据
context_datas = {} if self.request.params_obj['ajax']:
if self.request.params_obj['p'] == '':
datas = self._get_datas(context)
else:
datas = self._get_datas(context)[1:]
session_list += datas
session_dic[export_id] = session_list
reids_db.set('dic', session_dic, 24 * 3600)
# 对比redis中data与总数 1:下载完,0:未下载完
# if len(session_list) + len(datas) >= context['result_count']:
if len(session_list) >= context['result_count']:
context_datas['status'] = 1
else:
context_datas['status'] = 0
# session_list += datas # 返回前端下载总数和当前进度
context_datas['total'] = context['result_count']
context_datas['len'] = len(session_list)
return '', context_datas
# 最后一次返回所有数据下载 # 不是ajax 删除redis中的export_id
if export_id:
datas = session_list
session_dic[export_id] = ''
reids_db.set('dic', session_dic, 24 * 3600)
else:
datas = self._get_datas(context)
return datas,context_datas def get_xlsx_export(self, context):
datas,context_datas = self._down(context)
output = io.BytesIO()
# export_header = (
# self.request.GET.get('export_xlsx_header', 'off') == 'on') model_name = self.opts.verbose_name
book = xlsxwriter.Workbook(output)
sheet = book.add_worksheet(
u"%s %s" % (_(u'Sheet'), force_text(model_name)))
styles = {'datetime': book.add_format({'num_format': 'yyyy-mm-dd hh:mm:ss'}),
'date': book.add_format({'num_format': 'yyyy-mm-dd'}),
'time': book.add_format({'num_format': 'hh:mm:ss'}),
'header': book.add_format({'font': 'name Times New Roman', 'color': 'red', 'bold': 'on', 'num_format': '#,##0.00'}),
'default': book.add_format()} # if not export_header:
# datas = datas[1:]
for rowx, row in enumerate(datas):
for colx, value in enumerate(row):
if rowx == 0:
cell_style = styles['header']
else:
if isinstance(value, datetime.datetime):
cell_style = styles['datetime']
elif isinstance(value, datetime.date):
cell_style = styles['date']
elif isinstance(value, datetime.time):
cell_style = styles['time']
else:
cell_style = styles['default']
sheet.write(rowx, colx, value, cell_style)
book.close() output.seek(0)
return output.getvalue(),context_datas def get_response(self, response, context, *args, **kwargs):
file_type = self.request.GET.get('export_type', 'csv')
content,context_datas = getattr(self, 'get_%s_export' % file_type)(context)
if 'status' in context_datas.keys():
response = HttpResponse(json.dumps(context_datas), content_type="application/json")
else:
response = HttpResponse(
content_type="%s; charset=UTF-8" % self.export_mimes[file_type]) file_name = self.opts.verbose_name.replace(' ', '_')
# response['Content-Disposition'] = ('attachment; filename=%s.%s' % (
# file_name, file_type)).encode('utf-8') # 修复导出时gunicorn报错ascii
from urllib.parse import quote
response["Content-Disposition"] = \
"attachment; " \
"filenane=%s.%s;" \
"filename*=UTF-8''%s.%s" %(
quote(file_name),file_type,
quote(file_name),file_type
) response.write(content)
return response
4-3.自己应用下的adminx.py中要做大量数据导出的model
class SMSLogAdmin(ReadonlyAdmin):
list_display = ['id', 'my_mobile', 'status', 'req_time', 'ret_time', 'account',
'my_tally', 'my_price']
list_filter = ['account', 'status', 'req_time',] @property
def list_per_page(self):
import re
path = self.request.get_full_path()
pattern_res = re.findall('ajax',path) export_id = self.request.GET.get('export_id')
p = self.request.GET.get('p')
ajax = self.request.GET.get('ajax') # 将前端传过来的参数放到request对象中
if not hasattr(self.request,'params_obj'):
self.request.params_obj = {'export_id': export_id,
'p': p,
'ajax': ajax} if pattern_res:
# 分段下载时,才每页显示500条
return 500
else:
return 50 @list_per_page.setter
def list_per_page(self,x):
return x model_icon = 'fa fa-commenting'
show_all_rel_details = False
4-4.xadmin/views/list.py
@filter_hook
def get_context(self):
"""
Prepare the context for templates.
"""
self.title = _('%s List') % force_text(self.opts.verbose_name)
model_fields = [(f, f.name in self.list_display, self.get_check_field_url(f))
for f in (list(self.opts.fields) + self.get_model_method_fields()) if f.name not in self.list_exclude] new_context = {
'model_name': force_text(self.opts.verbose_name_plural),
'title': self.title,
'cl': self,
'model_fields': model_fields,
'clean_select_field_url': self.get_query_string(remove=[COL_LIST_VAR]),
'has_add_permission': self.has_add_permission(),
'app_label': self.app_label,
'brand_name': self.opts.verbose_name_plural,
'brand_icon': self.get_model_icon(self.model),
'add_url': self.model_admin_url('add'),
'result_headers': self.result_headers(),
'results': self.results(),
'result_count':self.result_count,# 将查询总数携带在context中
}
context = super(ListAdminView, self).get_context()
context.update(new_context)
return context
xadmin后台分段导出避免timeout的更多相关文章
- xadmin后台导出时gunicorn报错ascii
django + xadmin + nginx + gunicorn部署后,xadmin后台导出model数据报错,gunicorn日志记录为:UnicodeEncodeError: 'ascii' ...
- 解决了好几天的关于django xadmin后台增加链接并执行函数的问题
由于xadmin后台封装的完整性,想要在后台做一些改动对于新手来说还是有点困难,目前解决的第一个问题: 在admin后台增加链接,使其改变上级签收状态 如图 点击签收按钮之后,改变其状态 代码展示: ...
- 第三百九十四节,Django+Xadmin打造上线标准的在线教育平台—Xadmin后台进阶开发配置2,以及目录结构说明
第三百九十四节,Django+Xadmin打造上线标准的在线教育平台—Xadmin后台进阶开发配置2,以及目录结构说明 设置后台列表页面可以直接修改字段内容 在当前APP里的adminx.py文件里的 ...
- 第三百九十三节,Django+Xadmin打造上线标准的在线教育平台—Xadmin后台进阶开发配置
第三百九十三节,Django+Xadmin打造上线标准的在线教育平台—Xadmin后台进阶开发配置 设置后台某个字段的排序规则 在当前APP里的adminx.py文件里的数据表管理器里设置 order ...
- 自定义xadmin后台首页
登陆xadmin后台,首页默认是空白,可以自己添加小组件,xadmin一切都是那么美好,但是添加小组件遇到了个大坑,快整了2个礼拜,最终实现想要的界面.初始的页面如图: 本机后台显示这个页面正常,do ...
- 第三百八十节,Django+Xadmin打造上线标准的在线教育平台—将所有app下的models数据库表注册到xadmin后台管理
第三百八十节,Django+Xadmin打造上线标准的在线教育平台—将所有app下的models数据库表注册到xadmin后台管理 将一个app下的models数据库表注册到xadmin后台管理 重点 ...
- 5.3 将users表添加到xadmin后台
在users模块中添加adminx.py文件,是xadmin后台管理默认的文件名,内容是: from .models import EmailVerifyRecord, Banner import x ...
- 安装xadmin后台管理插件
django自带的admin后台管理功能太少.使用国人开发的xadmin后台,使用pip install xadmin安装在线包时,会出错,其中的README.rst是utf8格式,我们win7系统默 ...
- Xadmin后台管理系统搭建基于Django1.11.11+Python3.6
安装python及Django百度即可 主要介绍Xadmin安装 访问地址:https://github.com/sshwsfc/xadmin 下载 安装好之后,将xamdin目录复制到项目 我放在 ...
随机推荐
- Eclipse启动Web项目 Tomcat中webapps中没有项目文件夹
原文出处:https://blog.csdn.net/JYH1314/article/details/51656233 1.eclipse不像MyEclipse默认将项目部署到tomcat安装目录下的 ...
- 力扣(LeetCode)226. 翻转二叉树
翻转一棵二叉树. 示例: 思想 递归 java版 /** * Definition for a binary tree node. * public class TreeNode { * int va ...
- Asp.net core 学习笔记 ( Router 路由 )
和之前的一样用法. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory log ...
- VNPY思维导图架构
VNPY是使用人数世界第三,国内第一的量化交易框架,封装的接口主要有ctp(期货),wind,xtp(股票)等.内部包含回测.实盘.模拟盘等模块.数据库默认为MongoDB的no-sql数据库,基于p ...
- (转)c# 属性与索引器
属性是一种成员,它提供灵活的机制来读取.写入或计算私有字段的值. 属性可用作公共数据成员,但它们实际上是称为“访问器”的特殊方法. 这使得可以轻松访问数据,还有助于提高方法的安全性和灵活性. 一个简单 ...
- python 读写TXT,安装pandas模块。
今天需要用python读TXT 文件,发现pandas库好用,所以就去下载,没想pythoncharm中的setting中下载失败,所以去下源文件,安装pandas 是提示得先装numpy库,于是又去 ...
- ubuntu12.04 安装CAJViewer-ubuntu(待解决)
ubuntu12.04测试通过 1.sudo apt-get install wine 2.unzip CAJViewer-ubuntu12.04版.zip 3.wine CAJVieweru.exe
- 扩大了一个逻辑卷,resize2fs 保错:没有这个超级块
检查发现,文件系统类型是xfs,应该使用 xfs_growfs命令刷新文件系统
- C# FTP上传文件至服务器代码
C# FTP上传文件至服务器代码 /// <summary> /// 上传文件 /// </summary> /// <param name="fileinfo ...
- android -------- 安装APK报错:Installation error: INSTALL_FAILED_UPDATE_INCOMPATIBLE解决方法
记录一个 DELETE_FAILED_INTERNAL_ERROR Error while Installing APK问题 之前遇到这个问题 方案1 将data/data/目录下该应用的包名的目录删 ...