123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244 |
- # -*- 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
|