书接上回,使用docxtpl可以依据模板快速生成文档,但是当我的模板文件内容变得复杂之后,它就不支持了。

在我的实际生产过程中,碰到了以下两个问题:

  • SmartArt 表格数据中的tag不会被渲染

  • 统计图表数据,无法渲染

于是我对这两个问题进行了分析和尝试解决,现在勉强能用了,于是进行记载。

0x01 问题复现

创建一个示例文档

使用docxtpl渲染后发现文档无变化,用过的都知道,遂不具体演示,直接进行问题分析解决。

0x02 问题分析

在docxtpl的源码文件template.py中是这样初始化文件的:

class DocxTemplate(object):    
	def init_docx(self, reload: bool = True):
        if not self.docx or (self.is_rendered and reload):
            self.docx = Document(self.template_file)
            self.is_rendered = False

这是基于docx库加载Document对象。这个对象有什么问题呢?

测试一下:

首先给表格丰富一下内容:

然后使用docx读取表格内容:

from docx import Document

document = Document('template.docx')

# 遍历文档中的段落并输出文本内容
for paragraph in document.paragraphs:
    print(paragraph.text)

# 遍历文档中的表格并输出表格内容
for table in document.tables:
    for row in table.rows:
        for cell in row.cells:
            print(cell.text)

输出结果如下:

这是为什么呢?SmartArt图形和图表并没有被读取到。因此需要解析docx文件的文件结构。直接解压示例文档,得到了如下的目录结构:

│  [Content_Types].xml
│
├─docProps
│      app.xml
│      core.xml
│
├─word
│  │  document.xml
│  │  fontTable.xml
│  │  numbering.xml
│  │  settings.xml
│  │  styles.xml
│  │  webSettings.xml
│  │
│  ├─charts
│  │  │  chart1.xml
│  │  │  colors1.xml
│  │  │  style1.xml
│  │  │
│  │  └─_rels
│  │          chart1.xml.rels
│  │
│  ├─diagrams
│  │      colors1.xml
│  │      data1.xml
│  │      drawing1.xml
│  │      layout1.xml
│  │      quickStyle1.xml
│  │
│  ├─embeddings
│  │      Microsoft_Excel_Worksheet.xlsx
│  │
│  ├─theme
│  │      theme1.xml
│  │
│  └─_rels
│          document.xml.rels
│
└─_rels
        .rels

docx库读取的文件实际上是document.xml 文件,但是该文件只写了一般内容,对于高级内容,则是放在了其他地方。

  • SmartArt图形则是放在了diagrams文件夹下

  • 图表则是分了两部分数据分布放在了chartsembeddings 文件夹下

    • charts 放的是渲染数据,在word文档中直接展示的数据

    • embeddings 放的则是源数据,使用者可以便捷的编辑数据源渲染图形而不是直接修改图形。

问题原因分析完成,开始解决问题。

0x03 SmartArt

直接去分析diagrams 文件夹可知,数据是在data1.xml文件中

因此实现tag修改只需要单独修改该文件即可。像这样:

context = open(xml_file_path, 'r', encoding='utf-8').read()
for key, value in chart_data.items():
	replace_str = '{{%s}}' % key
	context = context.replace(replace_str, str(value))
open(xml_file_path, 'w', encoding='utf-8').write(context)

读取文件,采用字符方式替换tag,写入,搞定。

但是这里会有一个问题,有的时候tag的大括号会被xml文件写成这样:

产生这样的原因主要是因为英文和中文字符的属性不同导致的。

因此需要解决这样的问题,最简单的方法的是:

  • tag完全使用英文字符

  • 大括号间的空格取消

0x04 图表(Charts)

图表的数据主要分为两部分,其实在docxtpl库中也有对应的功能实现,文档内容如下:

但是它同样存在问题

这个问题的核心在于你修改的图表的源数据,但是你word中看到的图形数据并没用变化,知道你点击编辑它,才会触发渲染,修改图形。

因此我要做的就是修改渲染后的数据。也就是charts 文件夹下的数据。

这里需要分析xml的文件结构,最后发现,数据主要实在catval标签下

同时他们又位于ser标签下,一组数据对应一个ser标签。因此共有三组。

在我的工作场景中只有一组数据,因此我没有完成ser 标签的解析逻辑(即多组数据的解析逻辑,之后空闲时可能会补上)

因此直接修改catval 的数据代码如下:

    def renderChartXml(tag_element, data, tag_type, namespaces):
        if tag_type == 'cat':
            cache_str = 'strCache'
            sheet_path = 'Sheet1!$A$2:$A$'
        elif tag_type == 'val':
            cache_str = 'numCache'
            sheet_path = 'Sheet1!$B$2:$B$'
        else:
            raise ValueError('unknown tag type')

        count = len(data)
        cache = None
        remove_list = []
        for element in tag_element.iter():
            if element.tag.split('}')[1] == 'f':
                element.text = sheet_path + str(count + 1)
            if element.tag.split('}')[1] == cache_str:
                cache = element
            if element.tag.split('}')[1] == 'ptCount':
                element.set('val', str(count))
            # 删除原来的值
            if element.tag.split('}')[1] == 'pt':
                remove_list.append(element)
        # 删除原来的值
        if cache is None:
            raise Exception('analysis xml file error: cache not find')
        for element in remove_list:
            cache.remove(element)
        # 生成新的值
        for i in range(len(data)):
            value = data[i]
            pt = etree.SubElement(cache, '{%s}pt' % namespaces['c'], {'idx': str(i)})
            v = etree.SubElement(pt, '{%s}v' % namespaces['c'])
            v.text = str(value)

当然这不是全部的代码,这只是最后的处理内容。

0x05 使用方法

在完成了解析逻辑代码之后,即可开始测试功能。给定数据:

data = {
        "smart-art1": {
            "description": "替换成功",
        },
        "chart1": {
            "categories": ["类别1", "类别2", "类别3", "类别4"],
            "series": [{"系列1": [1, 2, 3, 4]}],
        },
    }

参数解释:

  • smart-art1: smart-art是我设置的关键字,后门数字则是需要替换的id,这个id是根据创建顺序自增的。

    • smart-atr的id是可选参数,不选则是替换全部文件,但是在文件较多时耗时可能会增加。

  • chart1:chart也是关键字,后面则是id(必选参数)。

    • categories:这是图表的横坐标,在表格中体现则是首列。

    • series:具体值,多列数据则使用多个json对象。

需要注意的是:

  • 关键字不要修改。

  • 对于图表(chart),一定要知道它的id,这很重要,否则无法修改到对应的表格。

使用方法同doxctpl:

from docxtplr import myDocxTemplate

template = myDocxTemplate('template.docx')

data = {
        "smart-art1": {
            "description": "替换成功",
        },
        "chart1": {
            "categories": ["类别1", "类别2", "类别3", "类别4"],
            "series": [{"系列1": [1, 2, 3, 4]}],
        },
    }

template.render(data)
template.save("result.docx")

结果如下:

完美渲染并删除了多余数据。nice。

0x06 更多示例

增加数据

直接增加json对象即可

"chart1": {
            "categories": ["类别1", "类别2", "类别3", "类别4"],
            "series": [{"系列 1": [1, 2, 3, 4]},
                       {"系列 2": [2, 3, 4, 5]},
                       {"系列 3": [3, 4, 5, 6]},
                       {"系列 4": [4, 5, 6, 7]}],
        },

更多图表

可以直接修改图形为饼图,多组数据选择复合饼图,否则只会显示第一组数据。

数据结构不变,直接渲染:


代码目前还不完善,但是已经可以基本使用。目前支持修改常见的图表类型。

如果你也喜欢,点击这里获取: docxtplr.py

对于不支持的图表格式,如果您有需要,支持定做。🧑‍💻