作用及应用场景
根据给定的word文档模版,按照给定的数据进行填充,动态生成word文档;
例如:电子合同、服务报告等
生成样例
word模版文件:
生成后的word文件:
相关知识点:
-
docx格式的word文档 docx文件是一个基于Office Open XML标准的压缩文件格式,它取代了早期的Microsoft Word文档格式(如.doc)。所以docx本质上是一个压缩文件,其中包含了一个或多个XML文件和一个关系表,这些XML文件存储了文档的文本、样式、图片等内容。在这些文件中,word/document.xml文件扮演着至关重要的角色,它包含了通过Word程序打开docx文件时所看到的内容及结构。可以将其类比为HTML,其中任何内容或结构的变更都会直接反映在用户所看到的文档上。我们在文档中编辑的主要内容都存储在该文件中。
-
在Go语言中,text/template包提供了一个强大的模板机制,可以用于生成文本输出。通过占位符替换的方式利用给定的数据和模版文件,动态生成最终的文本内容。
难点及解决思路:
为了方便模版的二次编辑,要求模版文件必须能够在word环境中进行编辑,不能破坏word文档的文件格式;text/template模版进行表格数据替换,需要在模版前后增加{{range}} {{end}}占位符, 这样会破坏xml文件的格式。需要保证在不破坏word文档的文件格式提前下识别表格数据,并进行数据替换。
解决思路是在占位符上做文章,如果是表格中的数据项,占位符采用{{.[列表字段名].[子字段名]}};通过解析及字符串匹配判断获得包含{{.[列表字段名]关键字的tr元素,然后将该tr元素作为该列表项的模版,进行数据填充操作;
需要引用的库:
"github.com/beevik/etree"
核心方法
// GetDocumentXmlStr 读取docx文件中的word/document.xml文件
func GetDocumentXmlStr(templateDocxFile string) (string, error) {
xmlFile := "word/document.xml"
// 读取document.xml文件
r, err := zip.OpenReader(templateDocxFile)
if err != nil {
return "", err
}
defer r.Close()
for _, f := range r.File {
if f.Name == xmlFile {
// Open the file
rc, err := f.Open()
if err != nil {
return "", err
}
defer rc.Close()
var buffer bytes.Buffer
_, err = io.Copy(&buffer, rc)
if err != nil {
return "", err
}
// Convert buffer to string
wordTemplate := buffer.String()
return wordTemplate, nil
}
}
return "", fmt.Errorf("word/document.xml not found in the docx file")
}
// TemplateReplace 替换模板中的数据
func TemplateReplace(templateStr string, data interface{}) (string, error) {
doc := etree.NewDocument()
if err := doc.ReadFromString(templateStr); err != nil {
return "", fmt.Errorf("error reading template string: %v", err)
}
trElements := doc.FindElements("//w:tr")
// 使用反射遍历data中的字段
v := reflect.ValueOf(data)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
t := v.Type()
buf := new(bytes.Buffer)
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
fieldType := t.Field(i)
// 获取字段名称或alias标签值
fieldName := fieldType.Name
if alias := fieldType.Tag.Get("alias"); alias != "" {
fieldName = alias
}
// 检查字段是否为切片类型
if field.Kind() == reflect.Slice {
for _, trElement := range trElements {
subDoc := etree.NewDocument()
subDoc.SetRoot(trElement.Copy())
trText, err := subDoc.WriteToString()
if err != nil {
return "", fmt.Errorf("error writing element to string: %v", err)
}
// 检查trElement是否包含 {{.[fieldName]
if strings.Contains(trText, "{{."+fieldName) {
trText = strings.ReplaceAll(trText, "{{."+fieldName, "{{")
// 处理匹配的元素
if !field.IsNil() && field.Len() > 0 {
for j := 0; j < field.Len(); j++ {
item := field.Index(j).Interface()
tmpl, err := template.New("word").Parse(strings.TrimSpace(trText))
if err != nil {
return "", fmt.Errorf("error parsing template: %v", err)
}
buf.Reset() // 重置 buffer
if err = tmpl.Execute(buf, item); err != nil {
return "", fmt.Errorf("error executing template: %v", err)
}
subStr := strings.Trim(buf.String(), "\r\n")
newElement := etree.NewDocument()
if err = newElement.ReadFromString(subStr); err != nil {
return "", fmt.Errorf("error reading new element: %v", err)
}
trElement.Parent().AddChild(newElement.Root())
}
}
trElement.Parent().RemoveChild(trElement)
break
}
}
}
}
// 处理非切片字段的模板替换
templateStr2, err := doc.WriteToString()
if err != nil {
return "", fmt.Errorf("error writing document to string: %v", err)
}
templateStr2 = strings.Trim(templateStr2, "\r\n")
tmpl, err := template.New("word").Parse(strings.TrimSpace(templateStr2))
if err != nil {
return "", fmt.Errorf("error parsing final template: %v", err)
}
buf.Reset() // 重置 buffer
if err = tmpl.Execute(buf, data); err != nil {
return "", fmt.Errorf("error executing final template: %v", err)
}
return strings.Trim(buf.String(), "\r\n"), nil
}
// SaveDocx 保存docx文件
func SaveDocx(sourceDocxFile string, targetDocxFile string, documentXmlStr string) error {
// 将 XML 内容保存在内存中
xmlContent := []byte(documentXmlStr)
xmlFile := "word/document.xml"
// 创建一个读取器,直接从内存中读取 XML 内容
dataReader := bytes.NewReader(xmlContent)
// Create a new zip file
zipFile, err := os.Create(targetDocxFile)
if err != nil {
return err
}
defer zipFile.Close()
// Create a new zip writer
w := zip.NewWriter(zipFile)
defer w.Close()
r, err := zip.OpenReader(sourceDocxFile)
if err != nil {
panic(err)
}
defer r.Close()
for _, file := range r.File {
if file.Name == xmlFile {
continue
}
// Open the file
rc, err := file.Open()
if err != nil {
return err
}
// Create a new file in the new archive
wc, err := w.Create(file.Name)
if err != nil {
return err
}
// Copy the file data to the new file
_, err = io.Copy(wc, rc)
if err != nil {
return err
}
rc.Close()
}
// Create a writer for data.xml in the zip file
dataWriter, err := w.Create(xmlFile)
if err != nil {
return err
}
// Copy the data.xml file content to the zip file
_, err = io.Copy(dataWriter, dataReader)
return err
}
// 测试数据及测试方法
type DataObject struct {
Title string
DataList []SubDataObject `alias:"L1"` // 支持设定占位符别名
DataList2 []SubDataObject
}
type SubDataObject struct {
C1 string
C2 string
C3 string
C4 string
}
func TestWord(t *testing.T) {
// word模版
templateDocxFile := "./t1.docx"
// 生成后的word文件路径
targetDocxFile := "./t1_c.docx"
wordTemplate, err := GetDocumentXmlStr(templateDocxFile)
if err != nil {
panic(err)
}
// 模版替换
data := &DataObject{
Title: "测试",
DataList: []SubDataObject{
{C1: "Q11", C2: "Q12", C3: "Q13", C4: "Q14"},
{C1: "Q21", C2: "Q22", C3: "Q23", C4: "Q24"},
{C1: "Q31", C2: "Q32", C3: "Q33", C4: "Q34"},
{C1: "Q41", C2: "Q42", C3: "Q43", C4: "Q44"},
{C1: "Q51", C2: "Q52", C3: "Q53", C4: "Q54"},
},
DataList2: []SubDataObject{
{C1: "qqq11", C2: "dQ12", C3: "dQ13", C4: "dQ14"},
{C1: "qqq21", C2: "dQ22", C3: "dQ23", C4: "dQ24"},
{C1: "qqq31", C2: "dQ32", C3: "dQ33", C4: "dQ34"},
},
}
str, err := TemplateReplace(wordTemplate, data)
if err != nil {
panic(err)
}
err = SaveDocx(templateDocxFile, targetDocxFile, str)
if err != nil {
panic(err)
}
fmt.Println("success")
}