书接上回,使用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
文件夹下图表则是分了两部分数据分布放在了
charts
和embeddings
文件夹下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的文件结构,最后发现,数据主要实在cat
和val
标签下
同时他们又位于ser
标签下,一组数据对应一个ser标签。因此共有三组。
在我的工作场景中只有一组数据,因此我没有完成ser
标签的解析逻辑(即多组数据的解析逻辑,之后空闲时可能会补上)
因此直接修改cat
和val
的数据代码如下:
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
对于不支持的图表格式,如果您有需要,支持定做。🧑💻