1. 程式人生 > 實用技巧 >簡單快速匯出word文件

簡單快速匯出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);
                }
            }
        }
// 程式碼來源 https://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生成後:

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

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

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

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