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