123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357 |
- import os
- import platform
- import tempfile
- from typing import List, Dict, Tuple, Optional
- import matplotlib.pyplot as plt
- import numpy as np
- from docx import Document
- from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
- from docx.oxml.ns import qn
- from docx.oxml.shared import OxmlElement
- from docx.shared import Inches, Pt, RGBColor
- from matplotlib import font_manager
- class DocxGenerator:
- def __init__(self, filename: str = "output.docx"):
- """
- 初始化文档生成器
- :param filename: 输出文件名
- """
- self.document = Document()
- self.filename = filename
- self.temp_files = [] # 用于存储临时文件路径
- # 设置默认字体
- self._set_default_font()
- # 设置matplotlib中文字体
- self._set_matplotlib_font()
- def _set_default_font(self):
- """设置文档默认字体"""
- style = self.document.styles['Normal']
- font = style.font
- font.name = '微软雅黑'
- font.size = Pt(10.5)
- def _set_matplotlib_font(self):
- """设置matplotlib中文字体"""
- # 根据操作系统选择字体
- system = platform.system()
- if system == 'Windows':
- # Windows系统通常有微软雅黑
- plt.rcParams['font.sans-serif'] = ['Microsoft YaHei'] # 设置字体为微软雅黑
- elif system == 'Darwin':
- # Mac系统通常有PingFang SC
- plt.rcParams['font.sans-serif'] = ['PingFang SC'] # 设置字体为苹方
- else:
- # Linux系统尝试使用文泉驿微米黑
- plt.rcParams['font.sans-serif'] = ['WenQuanYi Micro Hei'] # 设置字体为文泉驿微米黑
- plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
- # 如果上述字体不存在,尝试查找系统中可用的中文字体
- try:
- # 查找系统中可用的中文字体
- chinese_fonts = [f.name for f in font_manager.fontManager.ttflist
- if 'Hei' in f.name or 'hei' in f.name or
- 'YaHei' in f.name or 'yahei' in f.name or
- 'Song' in f.name or 'song' in f.name or
- 'Kai' in f.name or 'kai' in f.name or
- 'Fang' in f.name or 'fang' in f.name]
- if chinese_fonts:
- plt.rcParams['font.sans-serif'] = [chinese_fonts[0]]
- except:
- pass
- def add_title(self, text: str, level: int = 1):
- """
- 添加标题
- :param text: 标题文本
- :param level: 标题级别 (1-3)
- """
- if level == 1:
- self.document.add_heading(text, level=0)
- elif level == 2:
- self.document.add_heading(text, level=1)
- elif level == 3:
- self.document.add_heading(text, level=2)
- else:
- self.document.add_heading(text, level=2)
- def add_paragraph(self, text: str, bold: bool = False, italic: bool = False,
- underline: bool = False, color: Optional[Tuple[int, int, int]] = None,
- alignment: str = "left"):
- """
- 添加段落文本
- :param text: 文本内容
- :param bold: 是否加粗
- :param italic: 是否斜体
- :param underline: 是否下划线
- :param color: 文字颜色 (R, G, B)
- :param alignment: 对齐方式 ("left", "center", "right", "justify")
- """
- paragraph = self.document.add_paragraph()
- # 设置对齐方式
- if alignment == "center":
- paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
- elif alignment == "right":
- paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.RIGHT
- elif alignment == "justify":
- paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.JUSTIFY
- else:
- paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.LEFT
- run = paragraph.add_run(text)
- run.bold = bold
- run.italic = italic
- run.underline = underline
- if color:
- run.font.color.rgb = RGBColor(*color)
- def add_line_break(self):
- """添加换行"""
- self.document.add_paragraph()
- def add_page_break(self):
- """添加分页符"""
- self.document.add_page_break()
- def _save_plot_to_tempfile(self, plt_obj):
- """保存matplotlib图表到临时文件"""
- temp_file = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
- self.temp_files.append(temp_file.name)
- plt_obj.savefig(temp_file.name, dpi=300, bbox_inches='tight')
- plt.close()
- return temp_file.name
- def add_line_chart(self, title: str, categories: List[str], series: Dict[str, List[float]],
- width: float = 6, height: float = 4):
- """
- 添加折线图
- :param title: 图表标题
- :param categories: 类别列表 (x轴)
- :param series: 数据系列字典 {系列名: 数据列表}
- :param width: 图表宽度 (英寸)
- :param height: 图表高度 (英寸)
- """
- plt.figure(figsize=(width, height))
- for name, values in series.items():
- plt.plot(categories, values, marker='o', label=name)
- plt.title(title, fontproperties=self._get_chinese_font())
- plt.xlabel('分类', fontproperties=self._get_chinese_font())
- plt.ylabel('数值', fontproperties=self._get_chinese_font())
- plt.legend(prop=self._get_chinese_font())
- plt.grid(True)
- # 保存图表并插入到文档
- temp_file = self._save_plot_to_tempfile(plt)
- self._add_image_to_doc(temp_file, width, height, title)
- def add_pie_chart(self, title: str, data: Dict[str, float],
- width: float = 6, height: float = 4):
- """
- 添加饼图
- :param title: 图表标题
- :param data: 数据字典 {标签: 值}
- :param width: 图表宽度 (英寸)
- :param height: 图表高度 (英寸)
- """
- plt.figure(figsize=(width, height))
- labels = list(data.keys())
- sizes = list(data.values())
- plt.pie(sizes, labels=labels, autopct='%1.1f%%', startangle=90,
- textprops={'fontproperties': self._get_chinese_font()})
- plt.axis('equal') # 保持圆形
- plt.title(title, fontproperties=self._get_chinese_font())
- # 保存图表并插入到文档
- temp_file = self._save_plot_to_tempfile(plt)
- self._add_image_to_doc(temp_file, width, height, title)
- def add_rose_chart(self, title: str, data: Dict[str, float],
- width: float = 6, height: float = 4):
- """
- 添加玫瑰图 (极坐标条形图)
- :param title: 图表标题
- :param data: 数据字典 {标签: 值}
- :param width: 图表宽度 (英寸)
- :param height: 图表高度 (英寸)
- """
- plt.figure(figsize=(width, height))
- labels = list(data.keys())
- values = list(data.values())
- N = len(labels)
- theta = np.linspace(0.0, 2 * np.pi, N, endpoint=False)
- radii = values
- width = 2 * np.pi / N * 0.8
- ax = plt.subplot(111, polar=True)
- bars = ax.bar(theta, radii, width=width, bottom=0.0)
- # 设置标签
- ax.set_xticks(theta)
- ax.set_xticklabels(labels, fontproperties=self._get_chinese_font())
- ax.set_title(title, fontproperties=self._get_chinese_font())
- # 设置颜色
- for r, bar in zip(radii, bars):
- bar.set_facecolor(plt.cm.viridis(r / max(radii)))
- bar.set_alpha(0.8)
- # 保存图表并插入到文档
- temp_file = self._save_plot_to_tempfile(plt)
- self._add_image_to_doc(temp_file, height, height, title)
- def _get_chinese_font(self):
- """获取中文字体属性"""
- from matplotlib.font_manager import FontProperties
- system = platform.system()
- if system == 'Windows':
- return FontProperties(fname='C:/Windows/Fonts/msyh.ttc', size=12) # 微软雅黑
- elif system == 'Darwin':
- return FontProperties(fname='/System/Library/Fonts/PingFang.ttc', size=12) # 苹方
- else:
- return FontProperties(fname='/usr/share/fonts/wenquanyi/wqy-microhei/wqy-microhei.ttc', size=12) # 文泉驿微米黑
- def _add_image_to_doc(self, image_path: str, width: float, height: float, title: str = None):
- """将图片添加到文档"""
- paragraph = self.document.add_paragraph()
- run = paragraph.add_run()
- run.add_picture(image_path, width=Inches(width), height=Inches(height))
- # 添加图表标题
- if title:
- self.add_paragraph(f"图: {title}", alignment="center")
- def add_table(self, data: List[List[str]], header: bool = True):
- """
- 添加表格
- :param data: 表格数据 (二维列表)
- :param header: 第一行是否为表头
- """
- table = self.document.add_table(rows=len(data), cols=len(data[0]))
- for i, row in enumerate(data):
- for j, cell in enumerate(row):
- table.cell(i, j).text = cell
- # 设置表头样式
- if header and len(data) > 0:
- for j in range(len(data[0])):
- table.cell(0, j).paragraphs[0].runs[0].bold = True
- # 设置表头背景色
- shading_elm = OxmlElement('w:shd')
- shading_elm.set(qn('w:fill'), 'D9D9D9')
- table.cell(0, j)._tc.get_or_add_tcPr().append(shading_elm)
- def generate(self):
- """
- 生成并保存文档
- :return: 生成的文件路径
- """
- # 保存文档
- self.document.save(self.filename)
- # 清理临时文件
- for temp_file in self.temp_files:
- try:
- os.unlink(temp_file)
- except:
- pass
- return self.filename
- # 使用示例
- if __name__ == "__main__":
- # 创建文档生成器
- doc = DocxGenerator("示例文档1.docx")
- # 添加标题
- doc.add_title("年度销售报告", level=1)
- doc.add_line_break()
- # 添加段落
- doc.add_paragraph("本报告展示公司2023年度的销售情况分析。", bold=True)
- doc.add_paragraph("报告包含以下内容:")
- doc.add_paragraph("1. 各季度销售趋势", color=(0, 0, 255))
- doc.add_paragraph("2. 产品销售占比", color=(0, 0, 255))
- doc.add_paragraph("3. 区域销售分布", color=(0, 0, 255))
- doc.add_line_break()
- doc.add_paragraph("""
- 1. 各季度销售趋势
- 2. 产品销售占比
- 3. 区域销售分布
- """, color=(0, 0, 255))
- doc.add_line_break()
- # 添加折线图
- doc.add_title("季度销售趋势", level=2)
- categories = ["第一季度", "第二季度", "第三季度", "第四季度"]
- series = {
- "产品A": [120, 150, 180, 200],
- "产品B": [80, 90, 110, 130],
- "产品C": [50, 70, 90, 100]
- }
- doc.add_line_chart("季度销售趋势", categories, series)
- doc.add_line_break()
- # 添加饼图
- doc.add_title("产品销售占比", level=2)
- pie_data = {
- "产品A": 45,
- "产品B": 30,
- "产品C": 25
- }
- doc.add_pie_chart("产品销售占比", pie_data)
- doc.add_line_break()
- # 添加玫瑰图
- doc.add_title("区域销售分布", level=2)
- rose_data = {
- "华东地区": 35,
- "华北地区": 25,
- "华南地区": 20,
- "西部地区": 15,
- "东北地区": 5
- }
- doc.add_rose_chart("区域销售分布", rose_data)
- doc.add_line_break()
- # 添加表格
- doc.add_title("详细销售数据", level=2)
- table_data = [
- ["季度", "产品A", "产品B", "产品C", "总计"],
- ["第一季度", "120", "80", "50", "250"],
- ["第二季度", "150", "90", "70", "310"],
- ["第三季度", "180", "110", "90", "380"],
- ["第四季度", "200", "130", "100", "430"]
- ]
- doc.add_table(table_data)
- # 生成文档
- output_file = doc.generate()
- print(f"文档已生成: {output_file}")
|