簡單快速導出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版本。