report_generate.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. # -*- coding: utf-8 -*-
  2. """
  3. @author: yq
  4. @time: 2024/11/8
  5. @desc:
  6. """
  7. import os
  8. from typing import Dict
  9. import pandas as pd
  10. from docx import Document
  11. from docx.enum.table import WD_ALIGN_VERTICAL
  12. from docx.enum.text import WD_ALIGN_PARAGRAPH
  13. from docx.oxml import OxmlElement
  14. from docx.oxml.ns import qn
  15. from docx.shared import Inches, Cm
  16. from docx.shared import Pt
  17. from commom import GeneralException, f_get_datetime
  18. from config import BaseConfig
  19. from entitys import MetricFucEntity
  20. from enums import ResultCodesEnum, PlaceholderPrefixEnum
  21. class Report():
  22. @staticmethod
  23. def _set_cell_width(cell):
  24. text = cell.text
  25. if len(text) >= 10:
  26. cell.width = Cm(2)
  27. elif len(text) >= 15:
  28. cell.width = Cm(2.5)
  29. elif len(text) >= 25:
  30. cell.width = Cm(3)
  31. else:
  32. cell.width = Cm(1.5)
  33. @staticmethod
  34. def _set_cell_format(cell, pt=11):
  35. cell.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
  36. cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
  37. # 设置字体
  38. for paragraph in cell.paragraphs:
  39. for run in paragraph.runs:
  40. # 判断文本是否包含中文
  41. if any('\u4e00' <= char <= '\u9fff' for char in run.text):
  42. run.font.name = '宋体' # 设置中文字体为宋体
  43. else:
  44. run.font.name = 'Times New Roman' # 设置英文字体为Times New Roman
  45. run.font.size = Pt(pt)
  46. @staticmethod
  47. def _merge_cell_column(pre_cell, curr_cell):
  48. if curr_cell.text == pre_cell.text:
  49. column_name = curr_cell.text
  50. pre_cell.merge(curr_cell)
  51. pre_cell.text = column_name
  52. for run in pre_cell.paragraphs[0].runs:
  53. run.bold = True
  54. Report._set_cell_format(pre_cell)
  55. Report._set_cell_width(pre_cell)
  56. @staticmethod
  57. def _set_table_singleBoard(table):
  58. # 将table 的所有单元格四个边设置为 0.5 镑, 黑色, 实线
  59. def _set_table_boarder(table, **kwargs):
  60. """
  61. Set table`s border
  62. Usage:
  63. set_table_border(
  64. cell,
  65. top={"sz": 12, "val": "single", "color": "#FF0000"},
  66. bottom={"sz": 12, "color": "#00FF00", "val": "single"},
  67. left={"sz": 24, "val": "dashed"},
  68. right={"sz": 12, "val": "dashed"},
  69. )
  70. """
  71. borders = OxmlElement('w:tblBorders')
  72. for tag in ('bottom', 'top', 'left', 'right', 'insideV', 'insideH'):
  73. edge_data = kwargs.get(tag)
  74. if edge_data:
  75. any_border = OxmlElement(f'w:{tag}')
  76. for key in ["sz", "val", "color", "space", "shadow"]:
  77. if key in edge_data:
  78. any_border.set(qn(f'w:{key}'), str(edge_data[key]))
  79. borders.append(any_border)
  80. table._tbl.tblPr.append(borders)
  81. return _set_table_boarder(
  82. table,
  83. top={"sz": 4, "val": "single", "color": "#000000"},
  84. bottom={"sz": 4, "val": "single", "color": "#000000"},
  85. left={"sz": 4, "val": "single", "color": "#000000"},
  86. right={"sz": 4, "val": "single", "color": "#000000"},
  87. insideV={"sz": 4, "val": "single", "color": "#000000"},
  88. insideH={"sz": 4, "val": "single", "color": "#000000"}
  89. )
  90. @staticmethod
  91. def _get_placeholder(placeholder_prefix_enum: PlaceholderPrefixEnum, metric_code: str):
  92. return "{{" + f"{placeholder_prefix_enum.value}{metric_code}" + "}}"
  93. @staticmethod
  94. def _fill_value_placeholder(doc: Document, metric_value_dict: Dict[str, MetricFucEntity]):
  95. # 替换指标
  96. for paragraph in doc.paragraphs:
  97. text = paragraph.text
  98. for metric_code, metric_fuc_entity in metric_value_dict.items():
  99. placeholder = Report._get_placeholder(PlaceholderPrefixEnum.VALUE, metric_code)
  100. metric_value = metric_fuc_entity.value
  101. if metric_value is None:
  102. continue
  103. text = text.replace(placeholder, metric_value)
  104. # 段落中多个runs时执行,最后一个run改成替换好的文本,其他run置空
  105. if len(paragraph.runs[:-1]) > 0:
  106. for run in paragraph.runs[:-1]:
  107. run.text = ''
  108. paragraph.runs[-1].text = text
  109. @staticmethod
  110. def _get_text_length(text):
  111. return sum(3 if '\u4e00' <= char <= '\u9fff' else 1 for char in text)
  112. @staticmethod
  113. def _fill_table_placeholder(doc: Document, metric_value_dict: Dict[str, MetricFucEntity]):
  114. # 替换表格
  115. for paragraph in doc.paragraphs:
  116. for metric_code, metric_fuc_entity in metric_value_dict.items():
  117. placeholder = Report._get_placeholder(PlaceholderPrefixEnum.TABLE, metric_code)
  118. metric_table = metric_fuc_entity.table
  119. if metric_table is None:
  120. continue
  121. if not placeholder in paragraph.text:
  122. continue
  123. # 清除占位符
  124. for run in paragraph.runs:
  125. run.text = run.text.replace(placeholder, "")
  126. table = doc.add_table(rows=metric_table.shape[0] + 1, cols=metric_table.shape[1])
  127. table.alignment = WD_ALIGN_PARAGRAPH.CENTER
  128. paragraph._element.addnext(table._element)
  129. # 根据列名计算单元格宽度,对不符合最小宽度的情况,重新调整
  130. # TODO:根据列名和内容综合调整单元格宽度
  131. a4_width = 21 - 2 * 3.18
  132. total_columns = metric_table.shape[1]
  133. col_lengthes = [Report._get_text_length(c) for c in metric_table.columns]
  134. cell_width_unit = a4_width / sum(col_lengthes)
  135. cell_widths = [c * cell_width_unit for c in col_lengthes]
  136. min_cell_width = 1
  137. adjusted_cell_widths = [max(c, min_cell_width) for c in cell_widths]
  138. adjusted_width = sum(adjusted_cell_widths)
  139. if adjusted_width > a4_width:
  140. excess_width = adjusted_width - a4_width
  141. excess_width_per_column = excess_width / total_columns
  142. adjusted_cell_widths = [max(min_cell_width, c - excess_width_per_column) for c in
  143. adjusted_cell_widths]
  144. # 列名
  145. for column_idx, column_name in enumerate(metric_table.columns):
  146. cell = table.cell(0, column_idx)
  147. cell.text = str(column_name)
  148. for run in cell.paragraphs[0].runs:
  149. run.bold = True
  150. Report._set_cell_format(cell, 11)
  151. Report._set_cell_width(cell)
  152. table.columns[column_idx].width = Cm(adjusted_cell_widths[column_idx])
  153. # Report._set_cell_width(cell, cell_widths[column_idx])
  154. # 合并相同的列名
  155. if column_idx != 0 and BaseConfig.merge_table_column:
  156. pre_cell = table.cell(0, column_idx - 1)
  157. Report._merge_cell_column(pre_cell, cell)
  158. # 值
  159. for row_idx, row in metric_table.iterrows():
  160. for column_idx, value in enumerate(row):
  161. cell = table.cell(row_idx + 1, column_idx)
  162. if "率" in metric_table.columns[column_idx] or (
  163. "率" in str(row[0]) and pd.notna(value) and (column_idx != 0)):
  164. value = f"{float(value) * 100:.2f}%" if pd.notna(value) else '/'
  165. else:
  166. value = str(value) if pd.notna(value) else '/'
  167. cell.text = value
  168. Report._set_cell_format(cell, 10.5)
  169. # Report._set_cell_width(cell)
  170. # 合并第一行数据也为列的情况
  171. if row_idx == 0:
  172. Report._merge_cell_column(table.cell(0, column_idx), cell)
  173. Report._set_table_singleBoard(table)
  174. # 禁止自动调整表格
  175. if len(metric_table.columns) <= 12:
  176. table.autofit = False
  177. @staticmethod
  178. def _fill_image_placeholder(doc: Document, metric_value_dict: Dict[str, MetricFucEntity]):
  179. # 替换图片
  180. for paragraph in doc.paragraphs:
  181. for metric_code, metric_fuc_entity in metric_value_dict.items():
  182. placeholder = Report._get_placeholder(PlaceholderPrefixEnum.IMAGE, metric_code)
  183. image_path = metric_fuc_entity.image_path
  184. if image_path is None:
  185. continue
  186. if not placeholder in paragraph.text:
  187. continue
  188. if not os.path.exists(image_path):
  189. raise GeneralException(ResultCodesEnum.NOT_FOUND, message=f"文件【{image_path}】不存在")
  190. # 清除占位符
  191. for run in paragraph.runs:
  192. if placeholder not in run.text:
  193. continue
  194. run.text = run.text.replace(placeholder, "")
  195. run.add_picture(image_path, width=Inches(6))
  196. @staticmethod
  197. def generate_report(metric_value_dict: Dict[str, MetricFucEntity], template_path: str):
  198. if os.path.exists(template_path):
  199. doc = Document(template_path)
  200. else:
  201. raise GeneralException(ResultCodesEnum.NOT_FOUND, message=f"监控模板文件【{template_path}】不存在")
  202. Report._fill_value_placeholder(doc, metric_value_dict)
  203. Report._fill_table_placeholder(doc, metric_value_dict)
  204. Report._fill_image_placeholder(doc, metric_value_dict)
  205. new_path = template_path.replace(".docx", f"{f_get_datetime()}.docx")
  206. doc.save(f"./{new_path}")
  207. if __name__ == "__main__":
  208. pass