合同生成——使用NPOI读取word文件

公司的业务部门,提出在新中大上录入合同数据后,能不能按照合同范本,生成一份word的合同,这样业务部门在做合同时,只需要把合同各要素录入新中大即可,不需要在word又整理一遍;

我印象中在新中大的合同模块中有一个合同模板的功能,但是这个功能其实我没试用过,不过想想既然有这个功能应该还是可用的吧,所以一口答应下来;回到办公室打开新中大的界面一看,该功能使用word的书签的功能做模板,然后提取合同信息填入,基本符合业务部门的要求;如下图:  

但是仔细试用下,才发现这个模块可以使用的合同信息字段就只有上图的几个字段,不能再获取其他合同信息,合同的单项信息更是没办法获取,那这个功能只能说是废的,完全没有用,单单这几个字段信息完全没办法形成合同;

那只能自己实现这个功能;想法和新中大实现的方式一样,用word做好合同模板,通过读取模板,抽取合同信息填入word文件中,生成一份新的合同文本;

大致方向定了,接下来是技术选型,因为没有办法集成到新中大中去,所以为了方便大家使用,通过web的形式上线,所以所有的组件都必须支持web调用;

开发语言:Net Core 3.1

为了跨平台,在选择office组件时,当然不会选择只能在windows下使用office组件了,之前在做excel用的npoi,觉得挺好,首先使用NPOI做了一轮测试;

这里直接上代码,看看NPOI如何读取Word文件;

用过NPOI的同学都知道,NPOI兼容新旧两种格式,也就是我们常说的03版和07版的office,但是是通过两个不同的类来实现的,这里我们只是做新版格式的测试,也就是docx,07版的office格式,所以只是使用了NPOI的XWPFDocument;

using NPOI.XWPF.UserModel;
using NPOI.OpenXmlFormats.Wordprocessing;

using (FileStream iFile_word = File.OpenRead(AppContext.BaseDirectory + "新版.docx"))
{
    XWPFDocument idoc = new XWPFDocument(iFile_word);
}

以上代码打开一个office文件;ps:使用前肯定要安装NPOI,具体自行查询NPOI的文档;

新版的Word文件格式,其实就是可解析的xml结构,所以有兴趣的同学可以先了解一下office的格式;

List<XWPFParagraph> ipars = idoc.Paragraphs.ToList();
foreach (XWPFParagraph ipar in ipars)
{
    List<XWPFRun> iruns = ipar.Runs.ToList();
    foreach (XWPFRun irun in iruns)
    {
        if (!string.IsNullOrWhiteSpace(irun.Text))
        {
            irun.ReplaceText("材料名称", istr);
        }
    }
}

以上代码就完成了Word文件中指定字符的替换,是不是很简单啊;

中间遇到的坑主要是一个,文档中明明有“材料名称”这个关键字,但是有好几个地方却替换不到,原来第一次做模板的时候,我是直接打开Word文档,在文档直接输入“材料名称”,当你使用工具解析这个模板文件,你会发现“材料名称”这个关键字在有些地方,是在XWPFParagraph下,但是却是在不同Run下,因为同一个段落里可能会有不同的格式,举个很简单的例子,同一个段落,同一行里,有如下一句话:

我需要的材料是,手提笔记本一台

那么在word中就有可能是同一个XWPFParagraph下,“我需要的”在一个Run,“材料”在一个Run,“名”在一个Run,以此类推,所以虽然我是在文档里连续输入了一个关键字,但是office却认为存在不同的格式,所以分开存储了;

那么我们其实可以这样做;

1、代码上做兼容,比如检查发现一个XWPFParagraph里存在关键字,则先把该段落下的所有Run拉出来,合并到一个Run里,再做关键字替换,不过这样有可能会出现格式破坏;

2、在做模板的时候,不要在office里直接编辑关键字,先使用一个txt文档,把关键字输进去,然后复制粘贴到模板文件里,这样也可以最大程度上减少关键字分段的问题;

我采取了第二种方式;

替换完文字后,我们接下来要做的就是输出合同单项信息,这其实就是一个数据集合,把这个数据集合按一定表格式新增到合同文档中去;那就是处理word的表格了;

在word的文档中,表格是单独存在的,所以和遍历段落一样,可以采用遍历表格的方式去做,如下代码:

List<XWPFTable> itables = idoc.Tables.ToList();
foreach (XWPFTable itable in itables)
{
    XWPFTableRow irow_name = itable.Rows[0];
    XWPFTableRow irow_temp = itable.Rows[1];
    XWPFTableCell icell_temp = irow_temp.GetCell(0);
    XWPFTableCell icell_name = irow_name.GetCell(0);
    if (icell_name.GetText() == "材料明细表")
    {
        for (int i = 0; i < 10; i++)
        {
            CT_Row ict_row = new CT_Row();
            //XWPFTableRow irow_new = itable.GetRow(2);
            XWPFTableRow irow_new = itable.InsertNewTableRow(i + 2);
            irow_new.Height = 1000;
            for (int j = 0; j < 8; j++)
            {
                //XWPFTableCell icell_new = irow_new.GetCell(j);
                XWPFTableCell icell_new = irow_new.AddNewTableCell();
                foreach (XWPFParagraph icell_par_old in icell_new.Paragraphs)
                {
                    int iindex_cell_run = icell_par_old.Runs.Count;
                    for (int ii = 0; ii < iindex_cell_run; ii++)
                    {
                        icell_par_old.RemoveRun(ii);
                    }
                }
                XWPFParagraph icell_par;
                if (icell_new.Paragraphs.Count > 0)
                {
                    icell_par = icell_new.Paragraphs[0];
                }
                else
                {
                    icell_par = icell_new.AddParagraph();
                }
                /*icell_par.BorderBetween = Borders.Single;
                icell_par.BorderBottom = Borders.Single;
                icell_par.BorderLeft = Borders.Single;
                icell_par.BorderRight = Borders.Single;
                icell_par.BorderTop = Borders.Single;*/
                XWPFRun icell_run = icell_par.CreateRun();
                switch (j)
                {
                    case 0:
                        icell_run.SetText(i.ToString());
                        break;
                    case 1:
                        icell_run.SetText(string.Format("测试材料名称{0}", i));
                        break;
                    case 2:
                        icell_run.SetText(string.Format("规格{0}", i));
                        break;
                    case 3:
                        icell_run.SetText(string.Format("单位{0}", i));
                        break;
                    case 4:
                        icell_run.SetText(string.Format("数量{0}", i));
                        break;
                    case 5:
                        icell_run.SetText(string.Format("单价{0}", i));
                        break;
                    case 6:
                        icell_run.SetText(string.Format("金额{0}", i));
                        break;
                    case 7:
                        icell_run.SetText(string.Format("备注{0}", i));
                        break;
                }
                icell_par.AddRun(icell_run);
            }

            //itable.AddRow(irow_new);
        }
    }
}

表格没法指定名称,所以我们约定表格的第一行第一列的内容作为表格的关键字,如果等于“材料明细表”,则是我们要处理的表格;接下来的表头行,则是我们要对应的数据列,上面的代码因为是测试所以只是用循环做数据;

但是出事了,达不到如期效果,看下图: