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目录复制到项目 我放在 ...
随机推荐
- Linux下boost库的编译、安装详解
下载boost源码 boost下载地址 解压到一个目录 tar -zxvf boost_1_66_0.tar.gz 编译boost库 进入boost_1_66_0目录中 cd boost_1_66_0 ...
- ssh REMOTE HOST IDENTIFICATION HAS CHANGED!
连接到docker的时候,有时因为image重新buid过,就提示 It is also possible that a host key has just been changed. 不让连接. 解 ...
- Java创建WebService
从java 6之后就提供了简单快速的创建WebService的方法,这里将这种简单的方法记录下来方便以后回看.第一步:首先创建一个java Project,然后创建一个类HelloWorldImpl如 ...
- xpath是什么(入门教程)
xpath是什么(入门教程) 一.总结 一句话总结:一句话,XPath 是一门在 XML 文档中查找信息的语言.简单来说,html类似于xml结构,但是没有xml格式那么严格. 在xml中查找信息 包 ...
- android 趟坑记
又是一个伤感的故事,但阿古好像已经习以为常了. 大半年的辛苦又泡汤了,故事是这样. 帝都某高端小区,封闭局域网,做一个可视对讲+门禁的APP,之前那一版因为使用了商业代码,又不想花钱,于是找阿古换一个 ...
- 在远程连接一个 Wndows 10的情况下,重启远程机器
如果你从菜单找的话,是找不到这个菜单的!!! 你应该直接按 alt + F4 , 就会出现这个选项了. 参考: https://tommynation.com/shut-windows-10-remo ...
- 【消息队列】从各方面比较下kafka、activemq、rabbitmq、rocketmq之间的区别
一.单机吞吐量ActiveMQ:万级,吞吐量比RocketMQ和Kafka要低了一个数量级RabbitMQ:万级,吞吐量比RocketMQ和Kafka要低了一个数量级RocketMQ:10万级,Roc ...
- LeetCode--014--最长公共前缀(java)
编写一个函数来查找字符串数组中的最长公共前缀. 如果不存在公共前缀,返回空字符串 "". 示例 1: 输入: ["flower","flow" ...
- Xor-MST CodeForces - 888G (最小生成树,分治)
大意: n结点无向完全图, 给定每个点的点权, 边权为两端点异或值, 求最小生成树
- implode
$names = implode('|', array_column($categoryBackNameArr, 'name'));