report_generate.py 9.1 KB

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