读取excel和word模板,然后批量生成word

黄鹏宇 1,030 2022-11-29

项目介绍

我朋友要批量写一堆《告知用户函》,他们有word模板,但是里面的数据是从内部系统中导出来的excel。
问我能不能实现这个需求,我第一反应想到的是office的邮件合并,但是还是略显麻烦,所以自己动手用python做了一个。

这个项目有两个版本,一个是python封装为exe,一个是在线web。
image-1669712121744
image-1669797624697

打包的exe,由于依赖问题,会出各种各样的bug,后来又改为了web形式。
我们先来看看线上版的。
地址:https://treeworld.com.cn/doc_batch/

效果展示与使用说明

  1. 上传excel
  2. 上传word模板
  3. 根据模板,及excel中的数据,批量生成word
  4. 压缩并下载

场景

  • 邀请函
  • 工资条
  • 告知用户书啥的

开发流程

好了,我们来看一下怎么实现的。
项目分为前后端,前端就是简单的html,没用到vue之类的。
先说前端,其实就是怎么读取文件、发送,我用了一个叫fileinput的库,他可以读取文件,校验拓展名。
https://github.com/kartik-v/bootstrap-fileinput/
后端因为之前用的python-docx做的文档转换,所以干脆就用flask提供web服务。

整体目录

├─outputFileFolder - 存转化好的word (定时删除)
├─receiveFileFolder - 存接收的文件
├─zipFileFolder - 存压缩文件
├─core.py - 核心业务逻辑
├─server.py - flask
└─web
  ├─css
  ├─js
  ├index.html  - 前端页面


暴露三个接口

  1. 接收上传的文件;
  2. 开始生成
  3. 提供压缩文件的下载

流程

  1. 接收文件
  2. 逐行读取excel readExcel()
  3. 搜索word模板的关键词,替换
  4. 批量生成
  5. 压缩文件夹

主要讲一下怎么搜索与替换 {数字}这个过程吧。
在python-docx这个库里,doc文档分为 paragraghy和table

核心源代码

import os
import openpyxl
import re
import copy
# pip install docxcompose
from docxcompose.composer import Composer
# pip install python-docx
from docx import Document


def generate(key,isMerge):
    receiveFileFolder =  f"receiveFileFolder/{key}/"
    outputFolderPath = f"outputFileFolder/{key}/"
    wordFilePath = ""
    excelFilePath = ""
    for file in os.listdir(receiveFileFolder):
        if(file.split(".")[0] == "word"):
            wordFilePath = "/www/wwwroot/q-server.qdu.life/"+receiveFileFolder + file
        else:
            excelFilePath = "/www/wwwroot/q-server.qdu.life/"+receiveFileFolder + file
    if (not os.path.exists(outputFolderPath)):
        print("mkdir")
        os.mkdir(outputFolderPath)
        
    # 读取excel
    excelData = readExcel(excelFilePath)
    startReplace(excelData,wordFilePath,key)
    if(isMerge):
        # 合并成一个doc
        mergeAllDoc(outputFolderPath,f"zipFileFolder/{key}.docx")

def readExcel(excelFileName):
    print("readExcel")
    #获取文档对象
    # 读取excel
    excel = openpyxl.load_workbook(excelFileName)
    ws = excel.active

    # 是否跳过第一行
    skipFirstRow = True

    if(skipFirstRow):
        firstLine = 2
    else:
        firstLine = 1

    # 获取sheet的最大行数和列数
    cols = ws.max_column
    rows = 0
    for row in ws:
        if not all([cell.value == None for cell in row]):
            rows += 1
    print(rows)
    excelData = [{} for _ in range(rows+1)]
    # 把他们读到一个表里去
    for r in range(firstLine,rows+1):
        tmpMap = {}
        for c in range(1,cols+1):   
            tmpMap["{"+str(c)+"}"] = ws.cell(r,c).value         
            excelData[r] = tmpMap
    
    # $几就是第几行
    return excelData

def startReplace(excelData,wordFileName,key):
    document = Document(wordFileName)
    # 遍历excelData了
    for excelDataRow in excelData[2:]:
        tmpTemplateDoc  = copy.deepcopy(document)
        outputFileName = f"outputFileFolder/{key}/{str(excelDataRow['{1}'])}.docx"
        replaceKeywordInDoc(excelDataRow,tmpTemplateDoc,outputFileName)


def replaceKeywordInDoc(excelDataRow,wordFile,saveFileName):
    _doc = wordFile
    for _p in _doc.paragraphs:
        runs = _p.runs
        # 定义一个空的匹配词
        _temp = ''
        for run in runs:
            # 若替换词的开头在 run.text 中,结尾不在,且匹配词为空,则取出替换词的开头放入匹配词
            if '{' in run.text and '}' not in run.text and _temp == '':
                _ext = '{' + run.text.split('{')[1]
                _temp += _ext
                run.text = run.text.replace(_ext, '')
                continue

            if _temp:
                # 如果匹配词不为空 且 替换词的结尾在 run.text 中,则取出替换词的结尾放入匹配词
                if '}' in run.text:
                    _ext = run.text.split('}')[0] + '}'
                    _temp += _ext
                    # 说明已经将替换词完整取出,根据词映射关系进行替换
                    run.text = run.text.replace(_ext, str(excelDataRow[_temp]))
                    _temp = ''
                else:
                    # 否则 将 run.text 追加放入匹配词
                    _temp += run.text
                    run.text = ''
                continue

            for _key, _val in excelDataRow.items():
                if _key in run.text:
                    run.text = run.text.replace(_key, str(_val))
    # table
    for _table in _doc.tables:
        for _row in _table.rows:
            for _cell in _row.cells:
                for _p in _cell.paragraphs:
                    runs = _p.runs
                    # 定义一个空的匹配词
                    _temp = ''
                    for run in runs:
                        # 若替换词的开头在 run.text 中,结尾不在,且匹配词为空,则取出替换词的开头放入匹配词
                        if '{' in run.text and '}' not in run.text and _temp == '':
                            _ext = '{' + run.text.split('{')[1]
                            _temp += _ext
                            run.text = run.text.replace(_ext, '')
                            continue

                        if _temp:
                            # 如果匹配词不为空 且 替换词的结尾在 run.text 中,则取出替换词的结尾放入匹配词
                            if '}' in run.text:
                                _ext = run.text.split('}')[0] + '}'
                                _temp += _ext
                                # 说明已经将替换词完整取出,根据词映射关系进行替换
                                run.text = run.text.replace(_ext, str(excelDataRow[_temp]))
                                _temp = ''
                            else:
                                # 否则 将 run.text 追加放入匹配词
                                _temp += run.text
                                run.text = ''
                            continue

                        for _key, _val in excelDataRow.items():
                            if _key in run.text:
                                run.text = run.text.replace(_key, str(_val))

    _doc.save(saveFileName)

# 定义合并文档的函数
def mergeAllDoc(sourcePath, target_file_path):
    """
    :param source_file_path_list: 源文件路径列表
    :param target_file_path: 目标文件路径
    """
    # 获取源文件夹内内文件列表
    source_file_list_all = []
    new_list = []
    source_file_list = os.listdir(sourcePath)
    new_file_list = sorted(source_file_list, key=lambda file: os.path.getctime(os.path.join(sourcePath, file)))

    for item in new_file_list:
        if item.endswith('.docx'):
            new_list.append(item)

    # 获取源文件夹内内文件列表
    for file in new_list:
        source_file_list_all.append(sourcePath + '/' + file)

    # 填充分页符号文档
    page_break_doc = Document()
    page_break_doc.add_page_break()
    # 定义新文档
    target_doc = Document(source_file_list_all[0])
    target_composer = Composer(target_doc)
    for i in range(len(source_file_list_all)):
        if i == 0:
            continue
        try:
            target_composer.append(page_break_doc)
        except Exception as err:
            print(err)
            return
        # 拼接文档内容
        f = source_file_list_all[i]
        target_composer.append(Document(f))
    # 保存目标文档
    target_composer.save(target_file_path)