簡單快速導出word文檔

最近,我寫公司項目word導出功能,應該只有2小時的工作量,卻被硬生生的拉長2天,項目上線到業務正常運行也被拉長到2個星期。

 

為什麼如此浪費時間呢?

    1)公司的項目比較老,採用硬編碼模式,意味著word改一個字就要發布一次程式碼。發布檢驗就浪時間了。

    2)由於硬編碼,採用的是<html>這種格式,手寫程式碼比較廢時,而且編寫表格時會遇到單元格字數變多被撐大,表格變形的情況。表格長度需要人工計算。這類意想不到的問題。

    3)公司測試庫數據不全,測試庫數據無法全面覆蓋線上環境。這又拉長了檢驗時間。

    4)項目分支被正在開發的分支合併了,一下子被拉長了4天。

 

這簡單功能浪費太多時間了,我在網上搜了一下word導出的方案:

    第一種:硬編碼,就是公司的方案,問題太多了不用考慮。

    第二種:通過Sql查詢數據,存入字典,再通過第三方組件替換word的文字。這種方案,簡單容操作,sql查詢可以換成存儲過程,也存在缺點,1)存儲過程要寫提很細,邏輯演算法都寫在存儲過程,存儲過程可能變得很複雜。2)不支援表格內插入多條數據。

    第三種:通過Sql查詢數據,使用Razor模板引擎生成word。這種方案解決了存儲過程複雜問題,但Razor模板內使用<html>這種格式,所以寫模板時很麻煩。

    第四種:通過Sql查詢數據,存入字典,再通過第三方組件替換word的域。這種方案與第二種方案類似,對我個人來說,我不喜歡修改域。

 

但是,我想要一個簡單、容易控制、表格內能插入多條數據、可商用的方案。

    簡單:類似第二種方案,數據存入字典,循環替換word的文字,存儲過程可以寫得簡單。

    容易控制:模板不能使用<html>這種格式,最好能用office直接控制表格文字大小、顏色。

    表格內能插入多條數據:我寫的組件內必須有索引。

    可商用:拒絕商用組件。

 

經過幾天琢磨,我找到可行的方案:存儲過程+模板+演算法可控

依賴組件:

    DocumentFormat.OpenXml,微軟官方開源組件,支援docx文件,MIT協議。

    ToolGood.Algorithm,本人的Excel計算引擎組件,MIT協議,可簡化存儲過程。

 

核心程式碼:

    ReplaceTemplate 替換Word文字

    ReplaceTable 替換Word表格並支援插入

 

ReplaceTemplate 替換Word文字

    public class WordTemplate : AlgorithmEngine
    {
        private readonly static Regex _tempEngine = new Regex("^###([^::]*)[::](.*)$");// 定義臨時變數
        private readonly static Regex _tempMatch = new Regex("(#[^#]+#)");// 
        private readonly static Regex _simplifyMatch = new Regex(@"(\{[^\{\}]*\})");//簡化文本 只讀取欄位


        private void ReplaceTemplate(Body body)
        {
            
            var tempMatches = new List<string>();
            List<Paragraph> deleteParagraph = new List<Paragraph>();
            foreach (var paragraph in body.Descendants<Paragraph>()) {
                var text = paragraph.InnerText.Trim();
                var m = _tempEngine.Match(text);
                if (m.Success) {
                    var name = m.Groups[1].Value.Trim();
                    var engine = m.Groups[2].Value.Trim();
                    var value = this.TryEvaluate(engine, "");
                    this.AddParameter(name, value);
                    deleteParagraph.Add(paragraph);
                    continue;
                }
                var m2 = _tempMatch.Match(text);
                if (m2.Success) {
                    tempMatches.Add(m2.Groups[1].Value);
                    continue;
                }
                var m3 = _simplifyMatch.Match(text);
                if (m3.Success) {
                    tempMatches.Add(m3.Groups[1].Value);
                    continue;
                }
            }
            foreach (var paragraph in deleteParagraph) {
                paragraph.Remove();
            }

            Regex nameReg = new Regex(string.Join("|", listNames));
            foreach (var m in tempMatches) {
                string value;
                if (m.StartsWith("#")) {
                    var eval = m.Trim('#');
                    ……
                    value = this.TryEvaluate(eval, "");
                } else {
                    value = this.TryEvaluate(m.Replace("{", "[").Replace("}", "]"), "");
                }
                foreach (var paragraph in body.Descendants<Paragraph>()) {
                    ReplaceText(paragraph, m, value);
                }
            }
        }
// 程式碼來源 //stackoverflow.com/questions/19094388/openxml-replace-text-in-all-document
        private void ReplaceText(Paragraph paragraph, string find, string replaceWith){
    ….
}
}

View Code

 

ReplaceTable 替換Word表格並支援插入

       private readonly static Regex _rowMatch = new Regex(@"({{(.*?)}})");//

        private int _idx;
        private List<string> listNames = new List<string>();

        private void ReplaceTable(Body body)
        {
            foreach (Table table in body.Descendants<Table>()) {

                foreach (TableRow row in table.Descendants<TableRow>()) {
                    bool isRowData = false;
                    foreach (var paragraph in row.Descendants<Paragraph>()) {
                        var text = paragraph.InnerText.Trim();
                        if (_rowMatch.IsMatch(text)) {
                            isRowData = true;
                            break;
                        }
                    }
                    if (isRowData) {
                        // 防止 list[i].Id 寫成  [list][[i]].Id 這種繁雜的方式
                        Regex nameReg = new Regex(string.Join("|", listNames));
                        Dictionary<string, string> tempMatches = new Dictionary<string, string>();
                        foreach (Paragraph ph in row.Descendants<Paragraph>()) {
                            var m2 = _rowMatch.Match(ph.InnerText.Trim());
                            if (m2.Success) {
                                var txt = m2.Groups[1].Value;
                                var eval = txt.Substring(2, txt.Length - 4).Trim();
                                eval = nameReg.Replace(eval, new MatchEvaluator((k) => {
                                    return "[" + k.Value + "]";
                                }));
                                tempMatches[txt] = eval;
                            }
                        }

                        TableRow tpl = row.CloneNode(true) as TableRow;
                        TableRow lastRow = row;
                        TableRow opRow = row;
                        var startIndex = UseExcelIndex ? 1 : 0;
                        _idx = startIndex;

                        while (true) {
                            if (_idx > startIndex) { opRow = tpl.CloneNode(true) as TableRow; }

                            bool isMatch = true;
                            foreach (var m in tempMatches) {
                                string value = this.TryEvaluate(m.Value, null);
                                if (value == null) {
                                    isMatch = false;
                                    break;
                                }
                                foreach (var ph in opRow.Descendants<Paragraph>()) {
                                    ReplaceText(ph, m.Key, value);
                                }
                            }
                            if (isMatch==false) {
                                //當數據為空時,清空數據
                                if (_idx == startIndex) {
                                    foreach (var ph in opRow.Descendants<Paragraph>()) {
                                        ph.RemoveAllChildren();
                                    }
                                }
                                break;
                            }

                            if (_idx > startIndex) { table.InsertAfter(opRow, lastRow); }
                            lastRow = opRow;
                            _idx++;
                        }

                    }
                }
            }
        }

View Code

 

案例上手:

後台程式碼:

            // 獲取數據
            var helper = SqlHelperFactory.OpenSqliteFile("test.db");
        .......
            var dt = helper.ExecuteDataTable("select * from Introduction");
            var tableTests = helper.Select<TableTest>("select * from TableTest");

            ToolGood.OutputWord.WordTemplate openXmlTemplate = new ToolGood.OutputWord.WordTemplate();
            // 載入數據
            openXmlTemplate.SetData(dt);
            openXmlTemplate.SetListData("list", JsonConvert.SerializeObject(tableTests));

            // 生成模板 一
            openXmlTemplate.BuildTemplate("test.docx", "openxml_2.docx");

            // 生成模板 二
            var bs = openXmlTemplate.BuildTemplate("test.docx");
            File.WriteAllBytes("openxml_1.docx", bs);

Word模板:

 

Word生成後:

 

 

 

完整程式碼://github.com/toolgood/ToolGood.OutputWord

該組件已上傳到Nuget:Install-Package ToolGood.OutputWord

Excel公式參考://github.com/toolgood/ToolGood.Algorithm

 

JAVA版本:暫時沒有,ToolGood.Algorithm已支援JAVA版本。