report_generate.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  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 openpyxl
  10. import pandas as pd
  11. from docx import Document
  12. from docx.enum.table import WD_TABLE_ALIGNMENT, WD_CELL_VERTICAL_ALIGNMENT
  13. from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
  14. from docx.oxml import OxmlElement
  15. from docx.oxml.ns import qn
  16. from docx.shared import Inches, Cm, Pt
  17. from openpyxl.worksheet.worksheet import Worksheet
  18. from commom import GeneralException, f_get_datetime
  19. from config import BaseConfig
  20. from entitys import MetricFucResultEntity
  21. from enums import ResultCodesEnum, PlaceholderPrefixEnum
  22. class ReportWord():
  23. @staticmethod
  24. def _set_cell_width(table, table_cell_width):
  25. # 固定宽度
  26. for column in table.columns:
  27. if table_cell_width is not None:
  28. column.width = Cm(table_cell_width)
  29. continue
  30. # 自动调整宽度
  31. max_text_len_list = []
  32. a4_width = 21 - 2 # * 3.18
  33. for column in table.columns:
  34. max_text_len = 0
  35. for cell in column.cells:
  36. cell_text_len = ReportWord._get_text_length(cell.text)
  37. max_text_len = cell_text_len if cell_text_len > max_text_len else max_text_len
  38. max_text_len_list.append(max_text_len)
  39. # 按比例分配宽度
  40. cell_width_unit = a4_width / sum(max_text_len_list)
  41. cell_widths = [c * cell_width_unit for c in max_text_len_list]
  42. min_cell_width = 1
  43. # 限制最小宽度
  44. adjusted_cell_widths = [max(c, min_cell_width) for c in cell_widths]
  45. adjusted_width = sum(adjusted_cell_widths)
  46. if adjusted_width > a4_width:
  47. excess_width = adjusted_width - a4_width
  48. excess_width_per_column = excess_width / len(table.columns)
  49. # 减去多的宽度
  50. adjusted_cell_widths = [max(min_cell_width, c - excess_width_per_column) for c in
  51. adjusted_cell_widths]
  52. for idx, column in enumerate(table.columns):
  53. column.width = Cm(adjusted_cell_widths[idx])
  54. @staticmethod
  55. def _set_cell_format(cell, font_size=None):
  56. for paragraph in cell.paragraphs:
  57. paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
  58. # 首行缩进改0
  59. paragraph_format = paragraph.paragraph_format
  60. paragraph_format.first_line_indent = 0
  61. paragraph_format.element.pPr.ind.set(qn("w:firstLineChars"), '0')
  62. for run in paragraph.runs:
  63. # 判断文本是否包含中文
  64. if any('\u4e00' <= char <= '\u9fff' for char in run.text):
  65. run.font.name = '宋体' # 设置中文字体为宋体
  66. else:
  67. run.font.name = 'Times New Roman' # 设置英文字体为Times New Roman
  68. if font_size is not None:
  69. run.font.size = Pt(font_size)
  70. cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
  71. @staticmethod
  72. def _merge_cell_column(pre_cell, curr_cell, table_font_size, table_cell_width):
  73. if curr_cell.text == pre_cell.text:
  74. column_name = curr_cell.text
  75. pre_cell.merge(curr_cell)
  76. pre_cell.text = column_name
  77. for run in pre_cell.paragraphs[0].runs:
  78. run.bold = True
  79. ReportWord._set_cell_format(pre_cell, table_font_size)
  80. @staticmethod
  81. def _set_table_singleBoard(table):
  82. # 将table 的所有单元格四个边设置为 0.5 镑, 黑色, 实线
  83. def _set_table_boarder(table, **kwargs):
  84. """
  85. Set table`s border
  86. Usage:
  87. set_table_border(
  88. cell,
  89. top={"sz": 12, "val": "single", "color": "#FF0000"},
  90. bottom={"sz": 12, "color": "#00FF00", "val": "single"},
  91. left={"sz": 24, "val": "dashed"},
  92. right={"sz": 12, "val": "dashed"},
  93. )
  94. """
  95. borders = OxmlElement('w:tblBorders')
  96. for tag in ('bottom', 'top', 'left', 'right', 'insideV', 'insideH'):
  97. edge_data = kwargs.get(tag)
  98. if edge_data:
  99. any_border = OxmlElement(f'w:{tag}')
  100. for key in ["sz", "val", "color", "space", "shadow"]:
  101. if key in edge_data:
  102. any_border.set(qn(f'w:{key}'), str(edge_data[key]))
  103. borders.append(any_border)
  104. table._tbl.tblPr.append(borders)
  105. return _set_table_boarder(
  106. table,
  107. top={"sz": 4, "val": "single", "color": "#000000"},
  108. bottom={"sz": 4, "val": "single", "color": "#000000"},
  109. left={"sz": 4, "val": "single", "color": "#000000"},
  110. right={"sz": 4, "val": "single", "color": "#000000"},
  111. insideV={"sz": 4, "val": "single", "color": "#000000"},
  112. insideH={"sz": 4, "val": "single", "color": "#000000"}
  113. )
  114. @staticmethod
  115. def _get_placeholder(placeholder_prefix_enum: PlaceholderPrefixEnum, metric_code: str):
  116. return "{{" + f"{placeholder_prefix_enum.value}{metric_code}" + "}}"
  117. @staticmethod
  118. def _fill_value_placeholder(doc: Document, metric_value_dict: Dict[str, MetricFucResultEntity]):
  119. # 替换指标
  120. for paragraph in doc.paragraphs:
  121. text = paragraph.text
  122. for metric_code, metric_fuc_entity in metric_value_dict.items():
  123. placeholder = ReportWord._get_placeholder(PlaceholderPrefixEnum.VALUE, metric_code)
  124. metric_value = metric_fuc_entity.value
  125. if metric_value is None:
  126. continue
  127. text = text.replace(placeholder, str(metric_value))
  128. # 段落中多个runs时执行,最后一个run改成替换好的文本,其他run置空
  129. if len(paragraph.runs[:-1]) > 0:
  130. for run in paragraph.runs[:-1]:
  131. run.text = ''
  132. paragraph.runs[-1].text = text
  133. @staticmethod
  134. def _get_text_length(text):
  135. return sum(2 if '\u4e00' <= char <= '\u9fff' else 1 for char in text)
  136. @staticmethod
  137. def _fill_table_placeholder(doc: Document, metric_value_dict: Dict[str, MetricFucResultEntity]):
  138. # 替换表格
  139. for paragraph in doc.paragraphs:
  140. for metric_code, metric_fuc_entity in metric_value_dict.items():
  141. placeholder = ReportWord._get_placeholder(PlaceholderPrefixEnum.TABLE, metric_code)
  142. metric_table = metric_fuc_entity.table
  143. table_font_size = metric_fuc_entity.table_font_size
  144. table_autofit = metric_fuc_entity.table_autofit
  145. table_cell_width = metric_fuc_entity.table_cell_width
  146. if metric_table is None:
  147. continue
  148. if not placeholder in paragraph.text:
  149. continue
  150. # 清除占位符
  151. for run in paragraph.runs:
  152. run.text = run.text.replace(placeholder, "")
  153. table = doc.add_table(rows=metric_table.shape[0] + 1, cols=metric_table.shape[1])
  154. table.alignment = WD_TABLE_ALIGNMENT.CENTER
  155. paragraph._element.addnext(table._element)
  156. # 列名
  157. for column_idx, column_name in enumerate(metric_table.columns):
  158. cell = table.cell(0, column_idx)
  159. cell.text = str(column_name)
  160. for run in cell.paragraphs[0].runs:
  161. run.bold = True
  162. ReportWord._set_cell_format(cell, table_font_size)
  163. # 合并相同的列名
  164. if column_idx != 0 and BaseConfig.merge_table_column:
  165. pre_cell = table.cell(0, column_idx - 1)
  166. ReportWord._merge_cell_column(pre_cell, cell, table_font_size, table_cell_width)
  167. # 值
  168. for row_idx, row in metric_table.iterrows():
  169. for column_idx, value in enumerate(row):
  170. cell = table.cell(row_idx + 1, column_idx)
  171. value = str(value) if pd.notna(value) else '/'
  172. cell.text = str(value)
  173. ReportWord._set_cell_format(cell, table_font_size)
  174. # 合并第一行数据也为列的情况
  175. if row_idx == 0:
  176. ReportWord._merge_cell_column(table.cell(0, column_idx), cell, table_font_size,
  177. table_cell_width)
  178. ReportWord._set_table_singleBoard(table)
  179. ReportWord._set_cell_width(table, table_cell_width)
  180. # 禁止自动调整表格
  181. if len(metric_table.columns) <= 20 and not table_autofit:
  182. table.autofit = False
  183. @staticmethod
  184. def _fill_image_placeholder(doc: Document, metric_value_dict: Dict[str, MetricFucResultEntity]):
  185. # 替换图片
  186. for paragraph in doc.paragraphs:
  187. for metric_code, metric_fuc_entity in metric_value_dict.items():
  188. placeholder = ReportWord._get_placeholder(PlaceholderPrefixEnum.IMAGE, metric_code)
  189. image_path = metric_fuc_entity.image_path
  190. image_size = metric_fuc_entity.image_size
  191. if image_path is None:
  192. continue
  193. if not placeholder in paragraph.text:
  194. continue
  195. if isinstance(image_path, str):
  196. image_path = [image_path]
  197. for path in image_path:
  198. if not os.path.exists(path):
  199. raise GeneralException(ResultCodesEnum.NOT_FOUND, message=f"文件【{image_path}】不存在")
  200. # 清除占位符
  201. for run in paragraph.runs:
  202. if placeholder not in run.text:
  203. continue
  204. run.text = run.text.replace(placeholder, "")
  205. for path in image_path:
  206. run.add_picture(path, width=Inches(image_size))
  207. @staticmethod
  208. def generate_report(metric_value_dict: Dict[str, MetricFucResultEntity], template_path: str, save_path=None):
  209. if os.path.exists(template_path):
  210. doc = Document(template_path)
  211. else:
  212. raise GeneralException(ResultCodesEnum.NOT_FOUND, message=f"监控模板文件【{template_path}】不存在")
  213. ReportWord._fill_value_placeholder(doc, metric_value_dict)
  214. ReportWord._fill_table_placeholder(doc, metric_value_dict)
  215. ReportWord._fill_image_placeholder(doc, metric_value_dict)
  216. new_path = template_path.replace(".docx", f"{f_get_datetime()}.docx")
  217. if save_path is not None:
  218. new_path = save_path
  219. doc.save(f"./{new_path}")
  220. class ReportExcel():
  221. @staticmethod
  222. def _fill_value_placeholder(worksheet: Worksheet, metric_value_dict: Dict[str, MetricFucResultEntity]):
  223. # 替换指标,检查每个单元格并替换
  224. for metric_code, metric_fuc_entity in metric_value_dict.items():
  225. metric_value = metric_fuc_entity.value
  226. if metric_value is None:
  227. continue
  228. placeholder = ReportWord._get_placeholder(PlaceholderPrefixEnum.VALUE, metric_code)
  229. for row in worksheet.rows:
  230. for cell in row:
  231. if placeholder in str(cell.value):
  232. cell.value = str(cell.value).replace(placeholder, str(metric_value))
  233. @staticmethod
  234. def _fill_table_placeholder(worksheet: Worksheet, metric_value_dict: Dict[str, MetricFucResultEntity]):
  235. # 替换表格
  236. for metric_code, metric_fuc_entity in metric_value_dict.items():
  237. metric_table = metric_fuc_entity.table
  238. if metric_table is None:
  239. continue
  240. placeholder = ReportWord._get_placeholder(PlaceholderPrefixEnum.TABLE, metric_code)
  241. # 定位占位符位置
  242. start_row = 1
  243. start_col = 1
  244. end_flag = False
  245. for row in worksheet.rows:
  246. start_col = 1
  247. for cell in row:
  248. if placeholder in str(cell.value):
  249. end_flag = True
  250. break
  251. start_col += 1
  252. if end_flag:
  253. break
  254. start_row += 1
  255. # 无占位符则跳过
  256. if not end_flag:
  257. continue
  258. for row_idx, row in metric_table.iterrows():
  259. for column_idx, value in enumerate(row):
  260. worksheet.cell(row=start_row + row_idx, column=start_col + column_idx, value=str(value))
  261. @staticmethod
  262. def generate_report(metric_value_dict: Dict[str, MetricFucResultEntity], template_path: str, save_path=None):
  263. if os.path.exists(template_path):
  264. workbook = openpyxl.load_workbook(template_path)
  265. sheet_names = workbook.sheetnames
  266. worksheet = workbook[sheet_names[0]]
  267. else:
  268. raise GeneralException(ResultCodesEnum.NOT_FOUND, message=f"监控模板文件【{template_path}】不存在")
  269. ReportExcel._fill_value_placeholder(worksheet, metric_value_dict)
  270. ReportExcel._fill_table_placeholder(worksheet, metric_value_dict)
  271. new_path = template_path.replace(".xlsx", f"{f_get_datetime()}.xlsx")
  272. if save_path is not None:
  273. new_path = save_path
  274. workbook.save(f"./{new_path}")
  275. if __name__ == "__main__":
  276. pass