Excel2007以后,工作簿是以XML文本保存数据的,用压缩工具打开工作簿文件,结构大致如下:
文件夹下基本全部是xml文本文件。微软提供了Open XML SDK 2.5 for Office,其中定义了和这些xml文件相关的类,帮助我们使用xml的形式解析工作簿里的数据和内容。跳转到微软官方资料 跳转到Open XML SDK 2.5 for Microsoft Office下载连接
在安装了 Open XML SDK 2.5 之后,在项目或应用程序中,添加对以下组件的引用。
- DocumentFormat.OpenXml
- WindowsBase
package翻译:程序包;
1、创建SpreadsheetDocument对象
1 using DocumentFormat.OpenXml.Packaging; 2 3 //要解析的工作簿路径 4 string path = @"E:\studyvs\test.xlsx"; 5 //如果是准备新建工作簿,可使用Create方法 6 SpreadsheetDocument xlPackag = SpreadsheetDocument.Open(path, true);
2、从workbook.xml开始入手
用记事本打开workbook.xml,看到一些有关工作簿的重要信息,比如:有几张工作表,表名称都是什么等。
workbook.xml中有一个sheets节点,我们尝试拿到它,并解析出三个工作表的名称。
ps:注意name属性是工作表名称的标识。
继续补充前面的代码:
1 using DocumentFormat.OpenXml.Packaging; 2 using DocumentFormat.OpenXml.Spreadsheet; 3 4 //要解析的工作簿路径 5 string path = @"E:\studyvs\test.xlsx"; 6 SpreadsheetDocument xlPackag = SpreadsheetDocument.Open(path, true); 7 //创建对象用于准备解析workbook.xml 8 WorkbookPart wkpart = xlPackag.WorkbookPart; 9 //Sheets类型定义在DocumentFormat.OpenXml.Spreadsheet命名空间下 10 Sheets sheets = wkpart.Workbook.Sheets; 11 foreach (Sheet sheet in sheets) 12 { 13 string s = string.Format("工作表名称为{0}的id是{1}", sheet.Name, sheet.SheetId); 14 System.Windows.Forms.MessageBox.Show(s); 15 } 16 //SpreadsheetDocument使用完毕后,要关闭。 17 xlPackag.Close();
要获得workbook.xml下的sheets元素还可可使用另一种表达方式:
IEnumerable<Sheet> sheets = wkpart.Workbook.Descendants<Sheet>();
需要注意的是:这里仅仅是拿到了三个工作表的名称,但并不能直接得到工作表下的数据,因为workbook.xml的sheets节点提供的仅仅就像我们看到的那样,只有有限的信息。但不要紧,workbookpart对象提供了GetPartById()方法,可以通过Id找到我们需要的。
3、工作表数据初接触
工作表mySheet中保存了一些数据,我们尝试把这些数据读取出来。
这个工作表的已用区域是A1:C4,数据类型是数字,文本和日期。
打开sheet1.xml,看一下文件内容:
我们看到sheetData下保存了工作表的单元格数据(有些数据似乎“不对”,这个等等再说)。
row元素表示了“行”,r表示行的索引。
c元素--cell,表示了“单元格”,r--CellReference表示了单元格的地址,t--DataType,表示了单元格的值类型。s--style
1 //要解析的工作簿路径 2 string path = @"E:\studyvs\test.xlsx"; 3 SpreadsheetDocument xlPackag = SpreadsheetDocument.Open(path, true); 4 //创建对象用于解析workbook.xml 5 WorkbookPart wkpart = xlPackag.WorkbookPart; 6 IEnumerable<WorksheetPart> sheetparts = wkpart.WorksheetParts; 7 //找出工作表纬度不是"A1"的worksheetpart。(在本例中指内容非空的工作表) 8 WorksheetPart sheetpart = sheetparts.Single(p => p.Worksheet.SheetDimension.Reference != "A1"); 9 Worksheet sheet = sheetpart.Worksheet; 10 //拿到工作表的已使用范围 11 string refaddress = sheet.SheetDimension.Reference; 12 //拿到工作表使用的行数 13 int rowcount = sheet.Descendants<Row>().Count(); 14 string s = string.Format("该工作表的引用区域是{0},行数是{1}行", refaddress, rowcount.ToString()); 15 System.Windows.Forms.MessageBox.Show(s); 16 //-----该工作表的引用区域是A1:C4,行数是4行 17 //SpreadsheetDocument使用完毕后,要关闭。 18 xlPackag.Close();
worksheetpar得到的worksheet并没有name属性或元素之类的东东,所以使用这种方法无法使用工作表名称筛选工作表,前文已提到,要使用工作表名称筛选想要的工作表,可通过workbookpart的GetPartbyId方法。
下面的演示和上面的目的相同,都是拿到mySheet工作表的已用单元格区域地范围,和已使用的行数。
1 string path = @"E:\studyvs\test.xlsx"; 2 SpreadsheetDocument xlPackag = SpreadsheetDocument.Open(path, true); 3 WorkbookPart wkpart = xlPackag.WorkbookPart; 4 IEnumerable<WorksheetPart> sheetparts = wkpart.WorksheetParts; 5 //关键:拿到工作表名为mySheet的id 6 string relationId = wkpart.Workbook.Descendants<Sheet>().Single(sht => sht.Name == "mySheet").Id; 7 //GetPartById方法得到的是OpenXMLPart对象,需要显式转换为WorksheetPart对象 8 WorksheetPart sheetpart =(WorksheetPart) wkpart.GetPartById(relationId); 9 Worksheet sheet = sheetpart.Worksheet; 10 string refaddress = sheet.SheetDimension.Reference; 11 int rowcount = sheet.Descendants<Row>().Count(); 12 string s = string.Format("该工作表的引用区域是{0},行数是{1}行", refaddress, rowcount.ToString()); 13 System.Windows.Forms.MessageBox.Show(s); 14 xlPackag.Close();
差不多可以开始真正取单元格里的数据了,那些才是我们更关心的。取单元格的数据可能是像下面这样的模式:
1 WorksheetPart sheetpart =(WorksheetPart) wkpart.GetPartById(sheetId); 2 Worksheet sheet = sheetpart.Worksheet; 3 foreach (Row row in sheet.Descendants<Row>()) 4 { 5 foreach (Cell cell in row) 6 { 7 //value元素 8 CellValue cellvalue = cell.CellValue; 9 //value元素的值 10 string value = cellvalue.InnerText; 11 //... 12 } 13 }
然而真正的麻烦来了。当我们在xml文件中去搜寻需要的数据时,发现如果单元格的值是数字,那么c元素中v元素的innertext是正确的(日期也表现为数字)。如果单元络的值是“文本”,那么其c元素中的v元素的innertext中一些看上去好像没什么规律的数字。
此时我们就要来第一次认识一下sharedStrings.xml这个文件了
4、认识sharedStrings.xml文件和共享字符串表SharedStringTable<sst>
打开该文件。
si:SharedStringItem t:text
对比一下mySheet中的元素,在mySheet中A1的值是0,B1的值3。可以发现,当mySheet中c元素的t--datatype属性的值是“s--string”时,v的innertext的值是sharedStrings.xml中"si"元素的索引,其真实值是si中的t元素的innertext。
事实上,这样做的好处之一使一个工作簿中重复出现的文本字符串可以生复使用,是可以节省文件所占用的空间。当c-cell元素中有t属性时,意味着该单元格的值是一个文本,v-cellvalue的innertext使用SharedStringTable中的索引来引用真实值。
5、取单元格数据的相对完整的代码
技巧:当c元素有属性t时,其值就是文本型。通过这一特点,判断innertext是单元格的真实值,还是sharedstringtable中的索引。从而以正确的方式得到真实值。
1 using DocumentFormat.OpenXml.Packaging; 2 using DocumentFormat.OpenXml.Spreadsheet; 3 4 string path = @"E:\studyvs\test.xlsx"; 5 SpreadsheetDocument xlPackag = SpreadsheetDocument.Open(path, true); 6 WorkbookPart wkpart = xlPackag.WorkbookPart; 7 IEnumerable<WorksheetPart> sheetparts = wkpart.WorksheetParts; 8 string sheetId = wkpart.Workbook.Descendants<Sheet>().Single(sht => sht.Name == "mySheet").Id; 9 //System.Windows.Forms.MessageBox.Show(sheetId); 10 WorksheetPart sheetpart =(WorksheetPart) wkpart.GetPartById(sheetId); 11 Worksheet sheet = sheetpart.Worksheet; 12 //row元素 cell元素 cellvalue元素 13 foreach (Row row in sheet.Descendants<Row>()) 14 { 15 foreach (Cell cell in row) 16 { 17 CellValue cellvalue = cell.CellValue; 18 string value = cellvalue.InnerText; 19 if (cell.GetAttributes().Any(atr=>atr.LocalName=="t")==true) 20 { 21 //使用workbookpart对象得到sharedstringtable 22 value = wkpart.SharedStringTablePart.SharedStringTable.ChildElements[int.Parse(value)].InnerText; 23 } 24 string s = string.Format("单元格{0}的值是{1}", cell.CellReference, value); 25 System.Windows.Forms.MessageBox.Show(s); 26 } 27 } 28 xlPackag.Close();
上面的方法,把所有值,不论什么类型,都处理做了文本型。仍需要进一步改进,可能才真正适合需求。还有如果,在拿数值的时候,还要获得单元格应用的样式,也需要进一步的分析使用什么方法更合适和方便。