作者:小小明

Pandas数据处理专家,帮助一万用户解决数据处理难题。

最近碰到一个需求:

image-20201231152501300

虽然我没完全看懂啥意思,但大意就是:

1.读取word文档,将其中所有的表格都写入到一个excel文件中

2.对写好的excel做出一些修改(包括改某几个单元格的值和删除行),然后将修改后的excel数据回填到word表对应的位置

对于第一个需求,直接用pandas写出即可

对于第二个需求,先生成模板,再用docxtpl模板渲染

关于docxtpl,我已经根据官方文档,制作了一份操作手册:https://blog.csdn.net/as604049322/article/details/112008531

好了,就按照这个大致理解的需求开始干:

读取word文档表格写入到excel

python代码:

from docx import Document
import pandas as pd


doc = Document(r"test.docx")
writer = pd.ExcelWriter("test.xlsx")
for i, table in enumerate(doc.tables):
    header = [cell.text for cell in table.rows[0].cells]
    result = []
    for row in table.rows[1:]:
        tmp = []
        for cell in row.cells:
            tmp.append(cell.text)
        result.append(tmp)
    df = pd.DataFrame(result, columns=header)
    df.to_excel(writer, sheet_name=f"{i}", index=False)

writer.save()

经过上面代码处理,就将这样一个word文档:

image-20201231155311116

提取出来了这样的一个excel文件:

image-20201231155425633

整体效果已经达到,但是我觉得如果能顺便设置好列宽就好看点,要设置好列宽,我的思路是计算出每列的字符串的最大长度,但不能直接用字符长度,每个中文字符会占用两个长度,所以我直接取gbk编码后的字节长度:

from docx import Document
import pandas as pd
import numpy as np

doc = Document(r"test.docx")
writer = pd.ExcelWriter("test.xlsx")
workbook = writer.book

for i, table in enumerate(doc.tables):
    header = [cell.text for cell in table.rows[0].cells]
    result = []
    for row in table.rows[1:]:
        tmp = []
        for cell in row.cells:
            tmp.append(cell.text)
        result.append(tmp)
    df = pd.DataFrame(result, columns=header)
    df.to_excel(writer, sheet_name=f"{i}", index=False)
    worksheet = writer.sheets[f"{i}"]
    #  计算表头的字符宽度
    column_widths = (
        df.columns.to_series()
        .apply(lambda x: len(x.encode('gbk'))).values
    )
    #  计算每列的最大字符宽度
    max_widths = (
        df.astype(str)
        .applymap(lambda x: len(x.encode('gbk')))
        .agg(max).values
    )
    # 计算整体最大宽度
    widths = np.max([column_widths, max_widths], axis=0)
    for i, width in enumerate(widths):
        worksheet.set_column(i, i, width)

writer.save()

结果:

image-20201231163620090

有了一个合适的列宽,我看的舒服多了,至少我自己是满意了,要用代码加什么好看的样式也简单。

好了,现在开始处理需求2:

读取修改过的excel回填到word文档中

读取word并生成word模板

要回填到word文档中,我们应该事先生成能够被doctpl解析的模板,我的思路是每个表格除了表头以外全部删除,然后动态生成以下格式的模板:

xxxxxxxxx
{%tr for cells in rows0 %}
{{ cells[0] }}{{ cells[1] }}{{ cells[2] }}
{%tr endfor %}
{{ footers0[0] }}{{ footers0[1] }}{{ footers0[2] }}

到时候再直接根据excel的数据渲染就行,那么如何生成word模板呢?

直接看看我的代码吧:

from docx import Document

def set_font_style(after_font_style, before_font_style):
    after_font_style.bold = before_font_style.bold
    after_font_style.italic = before_font_style.italic
    after_font_style.underline = before_font_style.underline
    after_font_style.strike = before_font_style.strike
    after_font_style.shadow = before_font_style.shadow
    after_font_style.size = before_font_style.size
    after_font_style.color.rgb = before_font_style.color.rgb
    after_font_style.name = before_font_style.name

doc = Document("test.docx")
for i, table in enumerate(doc.tables):
    # 缓存最后一行的行对象
    last_row = table.rows[-1]._tr
    # 删除除表头外的所有行
    for row in table.rows[1:]:
        table._tbl.remove(row._tr)
    # 表格添加一行,第一个单元格文本指定为指定的内容
    table.add_row().cells[0].text = '{%tr for cells in rows'+str(i)+' %}'
    # 再添加一行用于保存中间的模板
    row = table.add_row()
    for j, cell in enumerate(row.cells):
        cell.text = '{{ cells[%d] }}' % j
    # 再添加一行用于保存endfor模板
    table.add_row().cells[0].text = '{%tr endfor %}'
    # 将直接缓存的最后一行添加到行尾
    table._tbl.append(last_row)
    # 表格的行尾修改完样式后,还原回以前的样式
    for j, cell in enumerate(table.rows[-1].cells):
        before_font_style = cell.paragraphs[0].runs[0].font
        cell.text = '{{ footers%d[%d] }}' % (i, j)
        after_font_style = cell.paragraphs[0].runs[0].font
        set_font_style(after_font_style, before_font_style)

doc.save("test_template.docx")

模板生成的效果:

image-20201231165752271

根据word模板回填word

有了模板就可以开始根据模板回填word了,首先我把excel修改成这样:

image-20201231170411064

就是一个表改了两个值,另一个表删了两行,保存后,执行以下代码:

from docxtpl import DocxTemplate
import pandas as pd

tpl = DocxTemplate('test_template.docx')
excel = pd.ExcelFile("test.xlsx")
context = {}
for sheet_name in excel.sheet_names:
    data = pd.read_excel(excel, sheet_name).values
    context[f'rows{sheet_name}'] = data[:-1].tolist()
    context[f'footers{sheet_name}'] = data[-1].tolist()
tpl.render(context)
tpl.save('result.docx')

回填结果:

image-20201231171022970

好了,到现在为止,我个人觉得已经大体上完成效果了。当然还不够完美,很多样式适配都还没有去做,首先是行高丢失,然后是修改数值后的千分符丢失,这都要等正式开发后去适配,我这里不公布正式开发的代码。

总结

你对doctpl怎么看呢?欢迎你在下方评论或留言表达你的看法。


本文转载:CSDN博客