python-docx插入可编辑图表

本文使用软件库版本:

python: 3.8

python-docx 1.1.2

python-pptx 0.6.23

引言

笔者最近在使用python-docx库时,需要在word文档中插入可编辑的图表,在查阅python-docx库官方文档后未找到此功能。

使用示例

目前支持以下类型的图表,其实还挺多的。

  • AREA: 基础区域图,用于展示数据随时间或分类的变化趋势。
  • AREA_STACKED: 堆叠区域图,强调各个数据系列之间的累积贡献。
  • AREA_STACKED_100: 百分比堆叠区域图,每个系列占总和的百分比,适合比较各部分占比。
  • BAR_CLUSTERED: 集群条形图,比较不同类别间的数值差异。
  • BAR_STACKED: 堆叠条形图,显示每个类别中各系列值的累加,适合展示总量随类别变化。
  • BAR_STACKED_100: 百分比堆叠条形图,显示每个系列占总和的百分比。
  • BUBBLE: 气泡图,通过气泡的大小和位置展示三个变量间的关系。
  • BUBBLE_THREE_D_EFFECT: 三维效果气泡图,增加视觉深度,同样用于展示三维数据关系。
  • COLUMN_CLUSTERED: 集群柱状图,水平方向比较不同类别的数据。
  • COLUMN_STACKED: 堆叠柱状图,柱状图的垂直累加形式,展示累计值。
  • COLUMN_STACKED_100: 百分比堆叠柱状图,显示每个系列占总和的百分比。
  • DOUGHNUT: 环形图,类似于饼图,用于展示比例关系,中心可空出以增加设计感。
  • DOUGHNUT_EXPLODED: 爆炸式环形图,部分扇区突出显示,以强调特定数据。
  • LINE: 折线图,显示数据随时间或顺序的连续变化趋势。
  • LINE_MARKERS: 标记折线图,在数据点处有明显标记,便于识别具体值。
  • LINE_MARKERS_STACKED: 堆叠标记折线图,每个系列的值累加,适用于展示各部分对整体的贡献。
  • LINE_MARKERS_STACKED_100: 百分比堆叠标记折线图,各系列按百分比累加,强调占比。
  • LINE_STACKED: 堆叠折线图,不带单独标记,展示各系列的累加趋势。
  • LINE_STACKED_100: 百分比堆叠折线图,折线图中各系列占总和的百分比。
  • PIE: 饼图,用于展示各类别占总体的比例。
  • PIE_EXPLODED: 爆炸式饼图,部分扇区被分离,突出显示特定部分。
  • RADAR: 雷达图,用于比较多个变量间的相对数值,适用于多维度评估。
  • RADAR_FILLED: 填充雷达图,连接各点并填充内部,增强视觉效果。
  • RADAR_MARKERS: 标记雷达图,仅在数据点处做标记,适合简洁展示。
  • XY_SCATTER: 散点图,用于展示两个变量之间的关系,寻找潜在的相关性。
  • XY_SCATTER_LINES: 带线的散点图,通过线条连接点,清晰展示趋势。
  • XY_SCATTER_LINES_NO_MARKERS: 无标记散点图,仅用线条表示数据趋势,适合密集数据。
  • XY_SCATTER_SMOOTH: 平滑散点图,线条平滑连接点,更直观地展示趋势。
  • XY_SCATTER_SMOOTH_NO_MARKERS: 无标记平滑散点图,仅展示平滑趋势线,适合强调趋势而非单个数据点。

基础代码

必要的修改:

修改python-docx库docx\oxml\shape.py

修改前:

 class CT_GraphicalObjectData(BaseOxmlElement):
     """``<a:graphicData>`` element, container for the XML of a DrawingML object."""
 
     pic: CT_Picture = ZeroOrOne("pic:pic")  # pyright: ignore[reportAssignmentType]
     uri: str = RequiredAttribute("uri", XsdToken)  # pyright: ignore[reportAssignmentType]

修改后:

 class CT_GraphicalObjectData(BaseOxmlElement):
     """``<a:graphicData>`` element, container for the XML of a DrawingML object."""
 
     pic: CT_Picture = ZeroOrOne("pic:pic")  # pyright: ignore[reportAssignmentType]
     uri: str = RequiredAttribute("uri", XsdToken)  # pyright: ignore[reportAssignmentType]
     cChart = ZeroOrOne('c:chart')  # 新增部分

  

 

使用方法,在自己的项目代码中导入我封装好的docx_patch_chart.py模块:

from typing import IO, Union

import docx.oxml
from docx.document import Document as _Document
from docx.opc.constants import RELATIONSHIP_TYPE as RT
from docx.opc.package import OpcPackage
from docx.opc.packuri import PackURI
from docx.opc.pkgwriter import PackageWriter
from docx.oxml.ns import nsdecls
from docx.oxml.parser import parse_xml
from docx.oxml.shape import CT_GraphicalObjectData, CT_Inline
from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrOne
from docx.parts.document import DocumentPart
from docx.text.run import Run
from lxml import etree
from pptx.parts.chart import ChartPart as PptChart


# 扩展Document类以添加图表至文档
def add_chart_document(self, chart_type, x, y, cx, cy, chart_data, border: bool = False, style=None, ):
"""
在文档中添加一个新的图表。


:param chart_type: 图表类型,来自pptx.enum.chart.XL_CHART_TYPE
:param x: 图表左上角X坐标
:param y: 图表左上角Y坐标
:param cx: 图表宽度
:param cy: 图表高度
:param chart_data: 图表数据,ChartData实例
:param border: 是否显示边框
:param style: 样式风格
:return: 创建的图表对象
"""
run = self.add_paragraph(style=style).add_run()
chart = run.add_chart(chart_type, x, y, cx, cy, chart_data)
if not border:
xml = '<c:spPr><a:ln><a:noFill/></a:ln></c:spPr>'
parser = etree.XMLParser(recover=True)
element = etree.fromstring(xml, parser)
chart._chartSpace.append(element)
return chart


# 添加方法到Document类
_Document.add_chart = add_chart_document


# 修改OpcPackage类以支持新的part命名规则
def next_partname(self, tmpl):
"""
修改part名称生成逻辑,适应从PPT到DOCX的转换。
"""
tmpl = tmpl.replace("/ppt", "/word") # 更改路径以适应Word文档结构
partnames = [part.partname for part in self.iter_parts()]
for n in range(1, len(partnames) + 2): # 生成唯一的新part名称
candidate_partname = tmpl % n
if candidate_partname not in partnames:
return PackURI(candidate_partname)
raise Exception("ProgrammingError: ran out of candidate_partnames")


OpcPackage.next_partname = next_partname

# 扩展CT_GraphicalObjectData类以包含图表元素
CT_GraphicalObjectData.cChart = ZeroOrOne("c:chart")


# CT_Inline类增加静态方法用于创建新的图表内联对象
def new_chart(cls, shape_id, rId, x, y, cx, cy):
"""
创建一个新的图表内联对象。
"""
inline = parse_xml(cls._chart_xml()) # 解析内联XML模板
inline.extent.cx = cx # 设置宽度
inline.extent.cy = cy # 设置高度
chart = CT_Chart.new(rId) # 创建图表元素
inline.graphic.graphicData.append(chart) # 将图表元素插入图形数据
return inline


CT_Inline.new_chart_inline = classmethod(new_chart)


# 提供_chart_xml的静态方法用于生成图表内联XML模板
def _chart_xml(cls):
"""
返回图表内联元素的XML字符串模板。
"""
return (
"<wp:inline %s>\n"
" <wp:extent cx='0' cy='0'/>\n"
' <wp:effectExtent l="0" t="0" r="0" b="0"/>\n'
' <wp:docPr id="1" name="Chart 1"/>\n'
" <wp:cNvGraphicFramePr/>\n"
" <a:graphic %s>\n"
' <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/chart"/>\n'
" </a:graphic>\n"
"</wp:inline>" % (nsdecls("wp", "a"), nsdecls("a"))
)


CT_Inline._chart_xml = classmethod(_chart_xml)


# 定义CT_Chart类以处理图表元素
class CT_Chart(BaseOxmlElement):
@classmethod
def new(cls, rId):
"""
创建一个新的图表元素,关联给定的关系ID。
"""
chart = parse_xml(cls._chart_xml(rId)) # 解析图表XML模板
chart.id = rId # 设置关系ID
return chart

@classmethod
def _chart_xml(cls, rId):
"""
返回图表元素的XML字符串模板。
"""
return '<c:chart %s r:id="%s"/>\n' % (nsdecls("c", "r"), rId)


docx.oxml.register_element_cls("c:chart", CT_Chart) # 注册CT_Chart类到oxml解析器


# 扩展DocumentPart类以获取或添加图表
def get_or_add_chart(self, chart_type, x, y, cx, cy, chart_data):
"""
获取已存在的图表Part,或创建并添加新图表Part。
"""
chart_part = PptChart.new(chart_type, chart_data, self.package) # 创建图表Part
rId = self.relate_to(chart_part, RT.CHART) # 建立与图表Part的关系
return rId, chart_part.chart # 返回关系ID和图表对象


DocumentPart.get_or_add_chart = get_or_add_chart


# 添加新方法以创建新的图表内联对象
def new_chart_inline(self, chart_type, x, y, cx, cy, chart_data):
"""
创建新的图表内联对象,并关联图表Part。
"""
rId, chart = self.get_or_add_chart(chart_type, x, y, cx, cy, chart_data)
shape_id = self.next_id # 获取下一个形状ID
return CT_Inline.new_chart_inline(shape_id, rId, x, y, cx, cy), chart


DocumentPart.new_chart_inline = new_chart_inline


# 扩展Run类以直接在运行对象中添加图表
def add_chart(self, chart_type, x, y, cx, cy, chart_data):
"""
在当前运行对象中添加图表。
"""
inline, chart = self.part.new_chart_inline(chart_type, x, y, cx, cy, chart_data)
self._r.add_drawing(inline) # 将图表内联对象添加到当前运行的绘图元素中
return chart


Run.add_chart = add_chart


# 修改OpcPackage的保存方法以处理可能的before_marshal异常
def save(self, pkg_file: Union[str, IO[bytes]]):
"""
保存OPC包到指定的文件路径或二进制流。
"""
for part in self.parts:
try:
part.before_marshal() # 尝试调用各Part的预处理方法
except AttributeError: # 如果Part没有此方法则忽略异常
pass
PackageWriter.write(pkg_file, self.rels, self.parts) # 执行实际的保存操作


OpcPackage.save = save

  

 

posted @ 2025-11-22 07:40  Ailion  阅读(57)  评论(0)    收藏  举报