dvlyadmin_pro/backend/utils/export_excel2.py
2025-03-18 08:46:50 +08:00

308 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 # 宽度上增加162*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