308 lines
13 KiB
Python
308 lines
13 KiB
Python
#!/bin/python
|
||
#coding: utf-8
|
||
# +-------------------------------------------------------------------
|
||
# | django-vue-lyadmin 专业版
|
||
# +-------------------------------------------------------------------
|
||
# | Author: lybbn
|
||
# +-------------------------------------------------------------------
|
||
# | QQ: 1042594286
|
||
# +-------------------------------------------------------------------
|
||
# | EditDate: 2024-03-25
|
||
# +-------------------------------------------------------------------
|
||
# | 版本: 2.2
|
||
# +-------------------------------------------------------------------
|
||
|
||
# ------------------------------
|
||
# 导出excel数据,支持导出图片(单张url网络地址),支持嵌套(1层)数据 如 : {'a':'b','c':{'d':'e'}} 要获取d的值,field_data 直接{'c.d':"嵌套1层获取"}即可
|
||
# 网络图片不存在会直接写入图片url网络地址
|
||
# ------------------------------
|
||
import os
|
||
import re
|
||
import time
|
||
import json
|
||
import random
|
||
import math
|
||
from io import BytesIO
|
||
import requests
|
||
from django.conf import settings
|
||
from utils.common import getfulldomian
|
||
from rest_framework.request import Request
|
||
from urllib.request import pathname2url
|
||
from urllib.parse import urlparse
|
||
from openpyxl import Workbook
|
||
from openpyxl.drawing.image import Image
|
||
from openpyxl.styles import Alignment
|
||
from openpyxl.utils import get_column_letter
|
||
from openpyxl.worksheet.table import Table, TableStyleInfo
|
||
from openpyxl.drawing.xdr import XDRPositiveSize2D
|
||
from openpyxl.utils.units import pixels_to_EMU
|
||
from openpyxl.drawing.spreadsheet_drawing import OneCellAnchor, AnchorMarker
|
||
from django.utils.encoding import escape_uri_path # 中文表名称转译。
|
||
from django.http import FileResponse
|
||
from django.core.validators import URLValidator
|
||
|
||
class LyExportExcel:
|
||
"""
|
||
通用导出excel2.0
|
||
"""
|
||
default_file_name = "ly"+time.strftime('%Y%m%d%H%M%S')+ str(random.randint(10, 99))+".xlsx"
|
||
|
||
def __init__(self,request: Request,downloadMode="temp",fileName=default_file_name):
|
||
# 表格表头最大宽度,默认80个字符
|
||
self.export_column_width = 80
|
||
# 下载模式:temp 内存型临时下载(系统不保存文件,内存文件流直接下载)、url 下载链接(系统保存文件,返回http/https下载链接地址)
|
||
self.download_mode = downloadMode
|
||
# 保存文件名,默认为自定义时间字符串
|
||
self.file_name = fileName
|
||
# 保存目录
|
||
self.save_dir = os.path.join('systemexport', time.strftime('%Y-%m-%d', time.localtime(time.time())))
|
||
# 保存位置,默认保存到media/systemexport/日期 目录
|
||
self.save_path = os.path.join(settings.MEDIA_ROOT, self.save_dir,self.file_name)
|
||
# 下载URL
|
||
self.download_url = getfulldomian(request) + settings.MEDIA_URL + pathname2url(self.save_dir) + "/" + self.file_name
|
||
# 是否显示图片,否的话为地址
|
||
self.show_image = True
|
||
|
||
def is_image(self,urlstr):
|
||
"""
|
||
是否为图片地址
|
||
"""
|
||
val = URLValidator(schemes=('http', 'https'))
|
||
try:
|
||
val(urlstr)
|
||
white_list = ['.png','.jpg','.jpeg','.gif']
|
||
a = urlparse(urlstr)
|
||
file_path = a.path
|
||
file_name = os.path.basename(file_path)
|
||
_,file_suffix = os.path.splitext(file_name)
|
||
if file_suffix.lower() in white_list:
|
||
return True
|
||
return False
|
||
except:#非图片地址
|
||
return False
|
||
def is_number(self,num):
|
||
"""
|
||
是否为数字
|
||
"""
|
||
try:
|
||
float(num)
|
||
return True
|
||
except ValueError:
|
||
pass
|
||
|
||
try:
|
||
import unicodedata
|
||
unicodedata.numeric(num)
|
||
return True
|
||
except (TypeError, ValueError):
|
||
pass
|
||
return False
|
||
|
||
def get_string_len(self, string):
|
||
"""
|
||
获取字符串最大长度
|
||
"""
|
||
length = 4
|
||
if string is None:
|
||
return length
|
||
if self.is_number(string):
|
||
string = str(string)
|
||
for char in string:
|
||
length += 2.1 if ord(char) > 256 else 1
|
||
return round(length, 1) if length <= self.export_column_width else self.export_column_width
|
||
|
||
def pixels_to_points(self,value, dpi=96):
|
||
"""96 dpi, 72i"""
|
||
return value * 72 / dpi
|
||
|
||
def inserImg(self,sheet,Imgpath,row_index,col_index):
|
||
"""
|
||
插入图片
|
||
"""
|
||
#下载imgpath网络地址到本地
|
||
response = requests.get(Imgpath)
|
||
img = BytesIO(response.content)
|
||
# 读取图像
|
||
imgToInsert = Image(img)
|
||
oriImgH, oriImgW = imgToInsert.height, imgToInsert.width
|
||
# 图像等比例缩放因子
|
||
resize_factor = 0.6
|
||
w_h_ratio = oriImgW/oriImgH
|
||
# 等比压缩大小
|
||
# resize_H = int(resize_factor * oriImgH)
|
||
# resize_W = int(resize_factor * resize_H * w_h_ratio)
|
||
# 固定大小
|
||
resize_H = 40
|
||
resize_W = 40
|
||
# 图像在Excel里面的大小
|
||
imgsize_excel = XDRPositiveSize2D(pixels_to_EMU(resize_W), pixels_to_EMU(resize_H))
|
||
# 设置单元格大小,单元格默认宽度单位:字符;高度单位:point(磅)
|
||
cell_height = int(self.pixels_to_points(resize_H+10, dpi=96)) #高度上增加10个像素放大单元格
|
||
# cell_width = int(resize_W/8) + 2 # 宽度上增加16(2*8)个像素放大单元格
|
||
# 注意这里的行、列索引从1开始
|
||
sheet.row_dimensions[row_index].height = cell_height
|
||
col_letter = get_column_letter(col_index)
|
||
# sheet.column_dimensions[col_letter].width = cell_width
|
||
x_pad = 8
|
||
y_pad = 5
|
||
# 注意这里的行、列索引从0开始
|
||
marker = AnchorMarker(col=col_index-1, colOff=pixels_to_EMU(x_pad), row=row_index-1, rowOff=pixels_to_EMU(y_pad))
|
||
imgToInsert.anchor = OneCellAnchor(_from=marker, ext=imgsize_excel)
|
||
sheet.add_image(imgToInsert)
|
||
return sheet
|
||
|
||
def cell_nomall_data(self,ws,index,h_index,df_len_max,val,left_center):
|
||
"""
|
||
处理cell正常数据
|
||
"""
|
||
#处理val是空列表报错
|
||
if isinstance(val,list) and not val:
|
||
val = ""
|
||
if isinstance(val,dict) or isinstance(val,list):
|
||
val = str(val)
|
||
ws.cell(row=index+2, column=h_index+1).value = val
|
||
ws.cell(row=index+2, column=h_index+1).alignment = left_center
|
||
# 计算最大列宽度
|
||
result_column_width = self.get_string_len(val)
|
||
if h_index !=0 and result_column_width > df_len_max[h_index]:
|
||
df_len_max[h_index] = result_column_width
|
||
return ws
|
||
|
||
def cell_image_data(self,ws,index,h_index,df_len_max,val):
|
||
"""
|
||
处理cell图像插入
|
||
"""
|
||
self.inserImg(ws,val,index+2,h_index+1)
|
||
# 计算图片最大列宽度
|
||
if h_index !=0 and 10 > df_len_max[h_index]:
|
||
df_len_max[h_index] = 10
|
||
return ws
|
||
|
||
def write_cell_data(self,ws,index,h_index,df_len_max,val,left_center):
|
||
"""
|
||
处理cell插入
|
||
"""
|
||
if self.show_image and self.is_image(val):
|
||
try:
|
||
self.inserImg(ws,val,index+2,h_index+1)
|
||
# 计算图片最大列宽度
|
||
if h_index !=0 and 10 > df_len_max[h_index]:
|
||
df_len_max[h_index] = 10
|
||
except:
|
||
self.cell_nomall_data(ws,index,h_index,df_len_max,val,left_center)
|
||
|
||
else:
|
||
self.cell_nomall_data(ws,index,h_index,df_len_max,val,left_center)
|
||
|
||
return ws
|
||
|
||
def export_data(self, field_data: dict, data: list):
|
||
"""
|
||
自定义导出头部和数据源
|
||
:param field_data: 首行数据源(表头) 如:field_data = {'name':'姓名','age':'年龄'} #字典形式key为导出字段名,value为导出excel表头名
|
||
:param data: 数据源
|
||
:return:
|
||
"""
|
||
assert field_data, "请配置对应的导出模板字段"
|
||
# assert data, "请配置对应的数据源"
|
||
wb = Workbook()
|
||
ws = wb.active
|
||
#左垂直居中
|
||
left_center = Alignment(
|
||
horizontal='left', # 水平对齐,可选general、left、center、right、fill、justify、centerContinuous、distributed
|
||
vertical='center', # 垂直对齐, 可选top、center、bottom、justify、distributed
|
||
text_rotation=0, # 字体旋转,0~180整数
|
||
wrap_text=False, # 是否自动换行
|
||
shrink_to_fit=False, # 是否缩小字体填充
|
||
indent=0, # 缩进值
|
||
)
|
||
#左垂直居中(允许自动换行)
|
||
left_center_wrap = Alignment(
|
||
horizontal='left', # 水平对齐,可选general、left、center、right、fill、justify、centerContinuous、distributed
|
||
vertical='center', # 垂直对齐, 可选top、center、bottom、justify、distributed
|
||
text_rotation=0, # 字体旋转,0~180整数
|
||
wrap_text=True, # 是否自动换行
|
||
shrink_to_fit=False, # 是否缩小字体填充
|
||
indent=0, # 缩进值
|
||
)
|
||
#表头名
|
||
header_data = ["序号",*field_data.values()]
|
||
#表头字段
|
||
header_key = ["#",*field_data.keys()]
|
||
df_len_max = [self.get_string_len(i) for i in header_data]
|
||
row = get_column_letter(len(field_data) + 1)
|
||
column = 1
|
||
# 表的首行
|
||
ws.append(header_data)
|
||
for index, results in enumerate(data):
|
||
for h_index, h_item in enumerate(header_key):
|
||
if h_index == 0:
|
||
ws.cell(row=index+2, column=h_index+1).value = index+1
|
||
ws.cell(row=index+2, column=h_index+1).alignment = left_center
|
||
else:
|
||
for key,val in results.items():
|
||
if '.' in h_item and isinstance(val,dict):#嵌套模式读取(1层嵌套)
|
||
nest_keys = h_item.split(".")
|
||
if key == nest_keys[0]:
|
||
try:
|
||
realval = val[nest_keys[1]]
|
||
self.write_cell_data(ws,index,h_index,df_len_max,realval,left_center)
|
||
except:
|
||
self.write_cell_data(ws,index,h_index,df_len_max,val,left_center)
|
||
elif isinstance(val,list) and len(val)>0:
|
||
if '.' in h_item and isinstance(val[0],dict):
|
||
nest_keys = h_item.split(".")
|
||
if key == nest_keys[0]:
|
||
try:
|
||
newVal = []
|
||
for n in val:
|
||
newVal.append(n[nest_keys[1]])
|
||
#列表多个数据换行处理
|
||
realval = "\n".join(newVal)
|
||
self.write_cell_data(ws,index,h_index,df_len_max,realval,left_center_wrap)
|
||
except:
|
||
self.write_cell_data(ws,index,h_index,df_len_max,json.dumps(val),left_center)
|
||
else:
|
||
if key == h_item:
|
||
if isinstance(val[0],str) or isinstance(val[0],int) or isinstance(val[0],float):
|
||
#列表多个数据换行处理
|
||
realval = "\n".join(val)
|
||
self.write_cell_data(ws,index,h_index,df_len_max,realval,left_center_wrap)
|
||
else:
|
||
realval = json.dumps(val)
|
||
self.write_cell_data(ws,index,h_index,df_len_max,realval,left_center)
|
||
else:
|
||
if key == h_item:
|
||
self.write_cell_data(ws,index,h_index,df_len_max,val,left_center)
|
||
column += 1
|
||
# 更新列宽
|
||
for index, width in enumerate(df_len_max):
|
||
ws.column_dimensions[get_column_letter(index + 1)].width = width
|
||
# 添加过滤头
|
||
tab = Table(displayName="Table", ref=f"A1:{row}{column}")
|
||
style = TableStyleInfo(
|
||
name="TableStyleLight9",# TableStyleLight 1-21 还有此样式 "TableStyleMedium9" 1-28 TableStyleDark1 1-11
|
||
showFirstColumn=True,
|
||
showLastColumn=True,
|
||
showRowStripes=True,
|
||
showColumnStripes=True,
|
||
)
|
||
tab.tableStyleInfo = style
|
||
ws.add_table(tab)
|
||
|
||
if self.download_mode == "temp":
|
||
# 返回文件给用户,用户操作浏览器对话框保存文件
|
||
output_buffer = BytesIO()
|
||
wb.save(output_buffer)
|
||
# wb.close()
|
||
output_buffer.seek(0)
|
||
# output_buffer.read() 不要read出来,否则seek就到文件最后了,导致下载的是空文件
|
||
response = FileResponse(output_buffer)
|
||
response['Access-Control-Expose-Headers'] = 'Content-Disposition' #允许Content-Disposition暴露给前端访问,不然axios无法获取此头部信息
|
||
response['Content-Type'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||
response['Content-Disposition'] = "attachment; filename=%s" % escape_uri_path(self.file_name)
|
||
return response
|
||
else:
|
||
wb.save(self.save_path)
|
||
return self.download_url
|