文章来源:EarlyPython作者:陈曦前言大家好,又到了Python办公自动化系列。今天继续分享一个真实的办公自动化需求:如何让Python+Excel+Word批量生成指定格式的合同。涉及的主要知识点有:openpyxl模块的综合应用和Word文档的两种遍历逻辑。需求描述你是乙方的建筑公司,你有一份空白合同模板的Word文件,如下图:另外,还有一份Excel合同信息表,里面包含了甲方需要的所有内容(承包商)需要填写合同。对于一个公司的所有信息,现在我们需要将Excel中各个公司的信息填入到空白的Word合同模板中,生成各个公司的合同。最终结果按以下步骤进行分析。本来我们需要将Excel汇总表中每一行的信息填写到word模板中,生成相应的合同。现在需要交给Python来实现,这就引出了一个问题:程序是如何知道某个信息填入哪个下划线的?为了解决这个问题,我们需要修改模板。把下划线改成某种标识,这样程序就能看到标识,明白这里应该放什么信息。这里采用的策略是:把需要填写的下划线改成汇总表中的列名,也就是下图所示的程序就可以识别出需要填写的内容。所谓识别可以这里用一个特别简单的词来代替,就是文本替换。只要检索#xxxx#(excel中的列名),将其替换为具体信息即可。对于这种策略,列名需要使用#xxxx#的格式,否则正常的无关文本中的信息会被替换掉,破坏了原有的需求。最后,模板修改如下:通过Excel表格,我们可以看到,其中一行是一家公司的信息,每一列的列名都存在于模板中。用每个公司的实际信息替换模板中的列名(程序标识和文本替换的依据)。完成这个要求。整个大需求的实现可以按照以下步骤进行:分析后的步骤:将空白合同调整为合同模板,将需要填写的下划线改为专用栏名,打开Excel表格,逐行循环,然后逐个单元格循环遍历每条信息,对于每条信息,在模板中找到对应的列名并替换(如果不明白,下面有解释)。每次循环遍历一行中的所有单元格后保存合同,并保留每个公司的个人合同。分析清楚之后,逻辑就很简单了,但是有个隐含的知识点没有讲到,边写代码边聊吧!代码实现首先导入模块,设置路径,创建文件夹。本例中涉及到Excel表格的打开和Word的创建,所以需要从openpyxl导入load_workbook,而无论是打开还是创建Word,docx模块的Document可以是`fromdocximportDocumentfromopenpyxlimportload_workbook使用os模块创建文件夹存放生成的合约importos给出合约模板和汇总表所在的文件夹路径,便于复用path=r'C:\Users\chenx\Desktop\contract'结合路径判断生成文件夹,避免程序出错和不存在则终止的风险os.path.exists(path+'/'+'allcontracts'):os.mkdir(path+'/'+'allcontracts')`然后打开Excel文件`workbook=load_workbook(path+'/'+'合同信息表.xlsx')sheet=workbook.active`现在遍历Excel生成合同。前面反复提到,Excel的每一行都是一个具体合约的信息,所以Word文件的docx的实例化和存储必须在循环体中,不像Excel的实例化是在循环体外`#有效信息行从第二行开始。第二行是表头,包括列名,也是文本替换的基础fortable_rowinrange(2,sheet.max_row+1):#每次循环实例化一个新行wordfilewordfile=Document(path+'/'+'contracttemplate.docx')#需要一个一个遍历单元格,每个单元格都包含对table_col有用的信息inrange(1,sheet.max_column+1):#旧文本是列名,其中已填入模板,用于文本替换。行限定为第一行后就是列名old_text=str(sheet.cell(row=1,column=table_col).value)#新的文本就是实际的信息。当table_col循环到某个值时,实际的单元格和列名就确定了。new_text=str(sheet.cell(row=table_row,column=table_col).value)#添加这个判断是因为读入程序的日期信息是“日期时间”的格式。如果想保留日期信息,可以使用string方法或者使用time/datetime模块处理if''innew_text:new_text=new_text.split()[0]`通过下图进一步理解这个替换:比如程序已经进入了第三次循环(循环到第三家公司),对于单元格的循环进入第四次循环,那么此时得到的实际值就是C园的建设,对应的列名是#工程内容#。至此,需要替换的内容就清楚了,只要在模板中找到#工程内容#,替换为施工C园即可!理解了这个替换,接下来就是遍历Word模板,找到对应的列名替换!我们之前在docx模块中提到过,Word文本具有Document-paragraphParagraph-textblockRun的三层结构。要遍历文本,可以使用以下代码:`all_paragraphs=wordfile.paragraphsforparagraphinall_paragraphs:print(paragraph.text)forruninparagraph.runs:print(run.text)`对于段落和文本块,.text可用于获取文本信息。这个需求的隐藏陷阱就在这里。注意合约最后需要填写的内容:这部分内容如果使用上面的代码是无法遍历的。为什么?因为这是Word文档中的表格!遍历表格需要特殊的遍历逻辑:documentDocument-tableTable-rowRow/columnColumn-cellCell,遍历表格中文本的代码如下:`all_tables=wordfile.tablesfortableinall_tables:#也可以按列遍历forrowintable.rows:forcellinrow.cells:print(cell.text)`有了这些补充知识,本例的核心代码可以这样写`fortable_rowinrange(2,sheet.max_row+1):wordfile=Document(path+'/'+'contracttemplate.docx')fortable_colinrange(1,sheet.max_column+1):old_text=str(sheet.cell(row=1,column=table_col).value)new_text=str(sheet.cell(row=table_row,column=table_col).value)if''innew_text:aranew_text=new_text.split()[0]pallgraph-ParagraphParagraph=wordfile.all_paragraphs中的段落段落:用于段落中的run.runs:run.text=run.text.replace(old_text,new_text,new_text)#文档文档-表格table/行table/行row/列列-单元格cecellall_tables=wordfile。ta的桌子bleinall_tables:forrowintable.rows:forcellinrow.cells:cell.text=cell.text.replace(old_text,new_text)#获取公司名称生成合同名称(companyc(heet.ell=strrow=table_row,column=1).value)wordfile.save(path+'/'+f'allcontracts/{company}contract.docx')`写在最后这个案例具有很强的实用性,并且需求可以扩展为:将信息汇总表Excel中的每个个体信息(每一行或每一列都是个人、公司或其他信息)填写到指定模板Eord中生成单独的文档,但是在写自动化脚本之前,任务先分好,思路清楚再进行!
