Go语言实现根据模版生成word

作用及应用场景

根据给定的word文档模版,按照给定的数据进行填充,动态生成word文档;

例如:电子合同、服务报告等

生成样例

word模版文件:

生成后的word文件:

相关知识点

  1. docx格式的word文档  docx文件是一个基于Office Open XML标准的压缩文件格式,它取代了早期的Microsoft Word文档格式(如.doc)。所以docx本质上是一个压缩文件,其中包含了一个或多个XML文件和一个关系表,这些XML文件存储了文档的文本、样式、图片等内容。在这些文件中,word/document.xml文件扮演着至关重要的角色,它包含了通过Word程序打开docx文件时所看到的内容及结构。可以将其类比为HTML,其中任何内容或结构的变更都会直接反映在用户所看到的文档上。我们在文档中编辑的主要内容都存储在该文件中。
  2. 在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")
    }


   


 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值