# -*- coding: utf-8 -*- """ @author: yq @time: 2024/11/8 @desc: """ import os from typing import Dict import pandas as pd from docx import Document from docx.enum.table import WD_ALIGN_VERTICAL from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.oxml import OxmlElement from docx.oxml.ns import qn from docx.shared import Inches, Cm, Pt from commom import GeneralException, f_get_datetime from config import BaseConfig from entitys import MetricFucEntity from enums import ResultCodesEnum, PlaceholderPrefixEnum class Report(): @staticmethod def _set_cell_width(table, table_cell_width): for column in table.columns: if table_cell_width is not None: column.width = Cm(table_cell_width) continue max_text_len = 0 for cell in column.cells: max_text_len = len(cell.text) if len(cell.text) > max_text_len else max_text_len if max_text_len >= 10: column.width = Cm(2) elif max_text_len >= 15: column.width = Cm(2.5) elif max_text_len >= 25: column.width = Cm(3) else: column.width = Cm(1.5) @staticmethod def _set_cell_format(cell, font_size=None): for paragraph in cell.paragraphs: # paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER for run in paragraph.runs: # 判断文本是否包含中文 if any('\u4e00' <= char <= '\u9fff' for char in run.text): run.font.name = '宋体' # 设置中文字体为宋体 else: run.font.name = 'Times New Roman' # 设置英文字体为Times New Roman if font_size is not None: run.font.size = Pt(font_size) cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER @staticmethod def _merge_cell_column(pre_cell, curr_cell, table_font_size, table_cell_width): if curr_cell.text == pre_cell.text: column_name = curr_cell.text pre_cell.merge(curr_cell) pre_cell.text = column_name for run in pre_cell.paragraphs[0].runs: run.bold = True Report._set_cell_format(pre_cell, table_font_size) @staticmethod def _set_table_singleBoard(table): # 将table 的所有单元格四个边设置为 0.5 镑, 黑色, 实线 def _set_table_boarder(table, **kwargs): """ Set table`s border Usage: set_table_border( cell, top={"sz": 12, "val": "single", "color": "#FF0000"}, bottom={"sz": 12, "color": "#00FF00", "val": "single"}, left={"sz": 24, "val": "dashed"}, right={"sz": 12, "val": "dashed"}, ) """ borders = OxmlElement('w:tblBorders') for tag in ('bottom', 'top', 'left', 'right', 'insideV', 'insideH'): edge_data = kwargs.get(tag) if edge_data: any_border = OxmlElement(f'w:{tag}') for key in ["sz", "val", "color", "space", "shadow"]: if key in edge_data: any_border.set(qn(f'w:{key}'), str(edge_data[key])) borders.append(any_border) table._tbl.tblPr.append(borders) return _set_table_boarder( table, top={"sz": 4, "val": "single", "color": "#000000"}, bottom={"sz": 4, "val": "single", "color": "#000000"}, left={"sz": 4, "val": "single", "color": "#000000"}, right={"sz": 4, "val": "single", "color": "#000000"}, insideV={"sz": 4, "val": "single", "color": "#000000"}, insideH={"sz": 4, "val": "single", "color": "#000000"} ) @staticmethod def _get_placeholder(placeholder_prefix_enum: PlaceholderPrefixEnum, metric_code: str): return "{{" + f"{placeholder_prefix_enum.value}{metric_code}" + "}}" @staticmethod def _fill_value_placeholder(doc: Document, metric_value_dict: Dict[str, MetricFucEntity]): # 替换指标 for paragraph in doc.paragraphs: text = paragraph.text for metric_code, metric_fuc_entity in metric_value_dict.items(): placeholder = Report._get_placeholder(PlaceholderPrefixEnum.VALUE, metric_code) metric_value = metric_fuc_entity.value if metric_value is None: continue text = text.replace(placeholder, str(metric_value)) # 段落中多个runs时执行,最后一个run改成替换好的文本,其他run置空 if len(paragraph.runs[:-1]) > 0: for run in paragraph.runs[:-1]: run.text = '' paragraph.runs[-1].text = text @staticmethod def _get_text_length(text): return sum(3 if '\u4e00' <= char <= '\u9fff' else 1 for char in text) @staticmethod def _fill_table_placeholder(doc: Document, metric_value_dict: Dict[str, MetricFucEntity]): # 替换表格 for paragraph in doc.paragraphs: for metric_code, metric_fuc_entity in metric_value_dict.items(): placeholder = Report._get_placeholder(PlaceholderPrefixEnum.TABLE, metric_code) metric_table = metric_fuc_entity.table table_font_size = metric_fuc_entity.table_font_size table_autofit = metric_fuc_entity.table_autofit table_cell_width = metric_fuc_entity.table_cell_width if metric_table is None: continue if not placeholder in paragraph.text: continue # 清除占位符 for run in paragraph.runs: run.text = run.text.replace(placeholder, "") table = doc.add_table(rows=metric_table.shape[0] + 1, cols=metric_table.shape[1]) table.alignment = WD_ALIGN_PARAGRAPH.CENTER paragraph._element.addnext(table._element) # 根据列名计算单元格宽度,对不符合最小宽度的情况,重新调整 # TODO:根据列名和内容综合调整单元格宽度 a4_width = 21 - 2 * 3.18 total_columns = metric_table.shape[1] col_lengthes = [Report._get_text_length(c) for c in metric_table.columns] cell_width_unit = a4_width / sum(col_lengthes) cell_widths = [c * cell_width_unit for c in col_lengthes] min_cell_width = 1 adjusted_cell_widths = [max(c, min_cell_width) for c in cell_widths] adjusted_width = sum(adjusted_cell_widths) if adjusted_width > a4_width: excess_width = adjusted_width - a4_width excess_width_per_column = excess_width / total_columns adjusted_cell_widths = [max(min_cell_width, c - excess_width_per_column) for c in adjusted_cell_widths] # 列名 for column_idx, column_name in enumerate(metric_table.columns): cell = table.cell(0, column_idx) cell.text = str(column_name) for run in cell.paragraphs[0].runs: run.bold = True Report._set_cell_format(cell, table_font_size) table.columns[column_idx].width = Cm(adjusted_cell_widths[column_idx]) # 合并相同的列名 if column_idx != 0 and BaseConfig.merge_table_column: pre_cell = table.cell(0, column_idx - 1) Report._merge_cell_column(pre_cell, cell, table_font_size, table_cell_width) # 值 for row_idx, row in metric_table.iterrows(): for column_idx, value in enumerate(row): cell = table.cell(row_idx + 1, column_idx) if "率" in metric_table.columns[column_idx] or ( "率" in str(row[0]) and pd.notna(value) and (column_idx != 0)): value = f"{float(value) * 100:.2f}%" if pd.notna(value) else '/' else: value = str(value) if pd.notna(value) else '/' cell.text = str(value) Report._set_cell_format(cell, table_font_size) # 合并第一行数据也为列的情况 if row_idx == 0: Report._merge_cell_column(table.cell(0, column_idx), cell, table_font_size, table_cell_width) # Report._set_cell_width(table, table_cell_width) Report._set_table_singleBoard(table) # 禁止自动调整表格 if len(metric_table.columns) <= 12 or not table_autofit: table.autofit = False @staticmethod def _fill_image_placeholder(doc: Document, metric_value_dict: Dict[str, MetricFucEntity]): # 替换图片 for paragraph in doc.paragraphs: for metric_code, metric_fuc_entity in metric_value_dict.items(): placeholder = Report._get_placeholder(PlaceholderPrefixEnum.IMAGE, metric_code) image_path = metric_fuc_entity.image_path image_size = metric_fuc_entity.image_size if image_path is None: continue if not placeholder in paragraph.text: continue if isinstance(image_path, str): image_path = [image_path] for path in image_path: if not os.path.exists(path): raise GeneralException(ResultCodesEnum.NOT_FOUND, message=f"文件【{image_path}】不存在") # 清除占位符 for run in paragraph.runs: if placeholder not in run.text: continue run.text = run.text.replace(placeholder, "") for path in image_path: run.add_picture(path, width=Inches(image_size)) @staticmethod def generate_report(metric_value_dict: Dict[str, MetricFucEntity], template_path: str, save_path=None): if os.path.exists(template_path): doc = Document(template_path) else: raise GeneralException(ResultCodesEnum.NOT_FOUND, message=f"监控模板文件【{template_path}】不存在") Report._fill_value_placeholder(doc, metric_value_dict) Report._fill_table_placeholder(doc, metric_value_dict) Report._fill_image_placeholder(doc, metric_value_dict) new_path = template_path.replace(".docx", f"{f_get_datetime()}.docx") if save_path is not None: new_path = save_path doc.save(f"./{new_path}") if __name__ == "__main__": pass