文档生成器.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. import os
  2. import platform
  3. import tempfile
  4. from typing import List, Dict, Tuple, Optional
  5. import matplotlib.pyplot as plt
  6. import numpy as np
  7. from docx import Document
  8. from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
  9. from docx.oxml.ns import qn
  10. from docx.oxml.shared import OxmlElement
  11. from docx.shared import Inches, Pt, RGBColor
  12. from matplotlib import font_manager
  13. class DocxGenerator:
  14. def __init__(self, filename: str = "output.docx"):
  15. """
  16. 初始化文档生成器
  17. :param filename: 输出文件名
  18. """
  19. self.document = Document()
  20. self.filename = filename
  21. self.temp_files = [] # 用于存储临时文件路径
  22. # 设置默认字体
  23. self._set_default_font()
  24. # 设置matplotlib中文字体
  25. self._set_matplotlib_font()
  26. def _set_default_font(self):
  27. """设置文档默认字体"""
  28. style = self.document.styles['Normal']
  29. font = style.font
  30. font.name = '微软雅黑'
  31. font.size = Pt(10.5)
  32. def _set_matplotlib_font(self):
  33. """设置matplotlib中文字体"""
  34. # 根据操作系统选择字体
  35. system = platform.system()
  36. if system == 'Windows':
  37. # Windows系统通常有微软雅黑
  38. plt.rcParams['font.sans-serif'] = ['Microsoft YaHei'] # 设置字体为微软雅黑
  39. elif system == 'Darwin':
  40. # Mac系统通常有PingFang SC
  41. plt.rcParams['font.sans-serif'] = ['PingFang SC'] # 设置字体为苹方
  42. else:
  43. # Linux系统尝试使用文泉驿微米黑
  44. plt.rcParams['font.sans-serif'] = ['WenQuanYi Micro Hei'] # 设置字体为文泉驿微米黑
  45. plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
  46. # 如果上述字体不存在,尝试查找系统中可用的中文字体
  47. try:
  48. # 查找系统中可用的中文字体
  49. chinese_fonts = [f.name for f in font_manager.fontManager.ttflist
  50. if 'Hei' in f.name or 'hei' in f.name or
  51. 'YaHei' in f.name or 'yahei' in f.name or
  52. 'Song' in f.name or 'song' in f.name or
  53. 'Kai' in f.name or 'kai' in f.name or
  54. 'Fang' in f.name or 'fang' in f.name]
  55. if chinese_fonts:
  56. plt.rcParams['font.sans-serif'] = [chinese_fonts[0]]
  57. except:
  58. pass
  59. def add_title(self, text: str, level: int = 1):
  60. """
  61. 添加标题
  62. :param text: 标题文本
  63. :param level: 标题级别 (1-3)
  64. """
  65. if level == 1:
  66. self.document.add_heading(text, level=0)
  67. elif level == 2:
  68. self.document.add_heading(text, level=1)
  69. elif level == 3:
  70. self.document.add_heading(text, level=2)
  71. else:
  72. self.document.add_heading(text, level=2)
  73. def add_paragraph(self, text: str, bold: bool = False, italic: bool = False,
  74. underline: bool = False, color: Optional[Tuple[int, int, int]] = None,
  75. alignment: str = "left"):
  76. """
  77. 添加段落文本
  78. :param text: 文本内容
  79. :param bold: 是否加粗
  80. :param italic: 是否斜体
  81. :param underline: 是否下划线
  82. :param color: 文字颜色 (R, G, B)
  83. :param alignment: 对齐方式 ("left", "center", "right", "justify")
  84. """
  85. paragraph = self.document.add_paragraph()
  86. # 设置对齐方式
  87. if alignment == "center":
  88. paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
  89. elif alignment == "right":
  90. paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.RIGHT
  91. elif alignment == "justify":
  92. paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.JUSTIFY
  93. else:
  94. paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.LEFT
  95. run = paragraph.add_run(text)
  96. run.bold = bold
  97. run.italic = italic
  98. run.underline = underline
  99. if color:
  100. run.font.color.rgb = RGBColor(*color)
  101. def add_line_break(self):
  102. """添加换行"""
  103. self.document.add_paragraph()
  104. def add_page_break(self):
  105. """添加分页符"""
  106. self.document.add_page_break()
  107. def _save_plot_to_tempfile(self, plt_obj):
  108. """保存matplotlib图表到临时文件"""
  109. temp_file = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
  110. self.temp_files.append(temp_file.name)
  111. plt_obj.savefig(temp_file.name, dpi=300, bbox_inches='tight')
  112. plt.close()
  113. return temp_file.name
  114. def add_line_chart(self, title: str, categories: List[str], series: Dict[str, List[float]],
  115. width: float = 6, height: float = 4):
  116. """
  117. 添加折线图
  118. :param title: 图表标题
  119. :param categories: 类别列表 (x轴)
  120. :param series: 数据系列字典 {系列名: 数据列表}
  121. :param width: 图表宽度 (英寸)
  122. :param height: 图表高度 (英寸)
  123. """
  124. plt.figure(figsize=(width, height))
  125. for name, values in series.items():
  126. plt.plot(categories, values, marker='o', label=name)
  127. plt.title(title, fontproperties=self._get_chinese_font())
  128. plt.xlabel('分类', fontproperties=self._get_chinese_font())
  129. plt.ylabel('数值', fontproperties=self._get_chinese_font())
  130. plt.legend(prop=self._get_chinese_font())
  131. plt.grid(True)
  132. # 保存图表并插入到文档
  133. temp_file = self._save_plot_to_tempfile(plt)
  134. self._add_image_to_doc(temp_file, width, height, title)
  135. def add_pie_chart(self, title: str, data: Dict[str, float],
  136. width: float = 6, height: float = 4):
  137. """
  138. 添加饼图
  139. :param title: 图表标题
  140. :param data: 数据字典 {标签: 值}
  141. :param width: 图表宽度 (英寸)
  142. :param height: 图表高度 (英寸)
  143. """
  144. plt.figure(figsize=(width, height))
  145. labels = list(data.keys())
  146. sizes = list(data.values())
  147. plt.pie(sizes, labels=labels, autopct='%1.1f%%', startangle=90,
  148. textprops={'fontproperties': self._get_chinese_font()})
  149. plt.axis('equal') # 保持圆形
  150. plt.title(title, fontproperties=self._get_chinese_font())
  151. # 保存图表并插入到文档
  152. temp_file = self._save_plot_to_tempfile(plt)
  153. self._add_image_to_doc(temp_file, width, height, title)
  154. def add_rose_chart(self, title: str, data: Dict[str, float],
  155. width: float = 6, height: float = 4):
  156. """
  157. 添加玫瑰图 (极坐标条形图)
  158. :param title: 图表标题
  159. :param data: 数据字典 {标签: 值}
  160. :param width: 图表宽度 (英寸)
  161. :param height: 图表高度 (英寸)
  162. """
  163. plt.figure(figsize=(width, height))
  164. labels = list(data.keys())
  165. values = list(data.values())
  166. N = len(labels)
  167. theta = np.linspace(0.0, 2 * np.pi, N, endpoint=False)
  168. radii = values
  169. width = 2 * np.pi / N * 0.8
  170. ax = plt.subplot(111, polar=True)
  171. bars = ax.bar(theta, radii, width=width, bottom=0.0)
  172. # 设置标签
  173. ax.set_xticks(theta)
  174. ax.set_xticklabels(labels, fontproperties=self._get_chinese_font())
  175. ax.set_title(title, fontproperties=self._get_chinese_font())
  176. # 设置颜色
  177. for r, bar in zip(radii, bars):
  178. bar.set_facecolor(plt.cm.viridis(r / max(radii)))
  179. bar.set_alpha(0.8)
  180. # 保存图表并插入到文档
  181. temp_file = self._save_plot_to_tempfile(plt)
  182. self._add_image_to_doc(temp_file, height, height, title)
  183. def _get_chinese_font(self):
  184. """获取中文字体属性"""
  185. from matplotlib.font_manager import FontProperties
  186. system = platform.system()
  187. if system == 'Windows':
  188. return FontProperties(fname='C:/Windows/Fonts/msyh.ttc', size=12) # 微软雅黑
  189. elif system == 'Darwin':
  190. return FontProperties(fname='/System/Library/Fonts/PingFang.ttc', size=12) # 苹方
  191. else:
  192. return FontProperties(fname='/usr/share/fonts/wenquanyi/wqy-microhei/wqy-microhei.ttc', size=12) # 文泉驿微米黑
  193. def _add_image_to_doc(self, image_path: str, width: float, height: float, title: str = None):
  194. """将图片添加到文档"""
  195. paragraph = self.document.add_paragraph()
  196. run = paragraph.add_run()
  197. run.add_picture(image_path, width=Inches(width), height=Inches(height))
  198. # 添加图表标题
  199. if title:
  200. self.add_paragraph(f"图: {title}", alignment="center")
  201. def add_table(self, data: List[List[str]], header: bool = True):
  202. """
  203. 添加表格
  204. :param data: 表格数据 (二维列表)
  205. :param header: 第一行是否为表头
  206. """
  207. table = self.document.add_table(rows=len(data), cols=len(data[0]))
  208. for i, row in enumerate(data):
  209. for j, cell in enumerate(row):
  210. table.cell(i, j).text = cell
  211. # 设置表头样式
  212. if header and len(data) > 0:
  213. for j in range(len(data[0])):
  214. table.cell(0, j).paragraphs[0].runs[0].bold = True
  215. # 设置表头背景色
  216. shading_elm = OxmlElement('w:shd')
  217. shading_elm.set(qn('w:fill'), 'D9D9D9')
  218. table.cell(0, j)._tc.get_or_add_tcPr().append(shading_elm)
  219. def generate(self):
  220. """
  221. 生成并保存文档
  222. :return: 生成的文件路径
  223. """
  224. # 保存文档
  225. self.document.save(self.filename)
  226. # 清理临时文件
  227. for temp_file in self.temp_files:
  228. try:
  229. os.unlink(temp_file)
  230. except:
  231. pass
  232. return self.filename
  233. # 使用示例
  234. if __name__ == "__main__":
  235. # 创建文档生成器
  236. doc = DocxGenerator("示例文档1.docx")
  237. # 添加标题
  238. doc.add_title("年度销售报告", level=1)
  239. doc.add_line_break()
  240. # 添加段落
  241. doc.add_paragraph("本报告展示公司2023年度的销售情况分析。", bold=True)
  242. doc.add_paragraph("报告包含以下内容:")
  243. doc.add_paragraph("1. 各季度销售趋势", color=(0, 0, 255))
  244. doc.add_paragraph("2. 产品销售占比", color=(0, 0, 255))
  245. doc.add_paragraph("3. 区域销售分布", color=(0, 0, 255))
  246. doc.add_line_break()
  247. doc.add_paragraph("""
  248. 1. 各季度销售趋势
  249. 2. 产品销售占比
  250. 3. 区域销售分布
  251. """, color=(0, 0, 255))
  252. doc.add_line_break()
  253. # 添加折线图
  254. doc.add_title("季度销售趋势", level=2)
  255. categories = ["第一季度", "第二季度", "第三季度", "第四季度"]
  256. series = {
  257. "产品A": [120, 150, 180, 200],
  258. "产品B": [80, 90, 110, 130],
  259. "产品C": [50, 70, 90, 100]
  260. }
  261. doc.add_line_chart("季度销售趋势", categories, series)
  262. doc.add_line_break()
  263. # 添加饼图
  264. doc.add_title("产品销售占比", level=2)
  265. pie_data = {
  266. "产品A": 45,
  267. "产品B": 30,
  268. "产品C": 25
  269. }
  270. doc.add_pie_chart("产品销售占比", pie_data)
  271. doc.add_line_break()
  272. # 添加玫瑰图
  273. doc.add_title("区域销售分布", level=2)
  274. rose_data = {
  275. "华东地区": 35,
  276. "华北地区": 25,
  277. "华南地区": 20,
  278. "西部地区": 15,
  279. "东北地区": 5
  280. }
  281. doc.add_rose_chart("区域销售分布", rose_data)
  282. doc.add_line_break()
  283. # 添加表格
  284. doc.add_title("详细销售数据", level=2)
  285. table_data = [
  286. ["季度", "产品A", "产品B", "产品C", "总计"],
  287. ["第一季度", "120", "80", "50", "250"],
  288. ["第二季度", "150", "90", "70", "310"],
  289. ["第三季度", "180", "110", "90", "380"],
  290. ["第四季度", "200", "130", "100", "430"]
  291. ]
  292. doc.add_table(table_data)
  293. # 生成文档
  294. output_file = doc.generate()
  295. print(f"文档已生成: {output_file}")