#!/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