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

366 阅读4分钟

作用及应用场景

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

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

生成样例

word模版文件:

image.png

生成后的word文件:

image.png

相关知识点:

  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")
}