原文地址:http://dsqiu.iteye.com/blog/1887702
由于工作需要,要把Excel数据(格式如下图)读取出来并动态创建类,并利用数据去实例化,然后在进行序列化存储。
要读取Excels数据就必须了解Excel的存储结构和存储方法,才能进行读取操作,从参考⑨+1中可知,.xlsx是一组.xml文件的集合,可以把.xlsx后缀名改成.zip,然后在打开就可以看到,既然是这样我们就可以从解析xml角度去读取xlsx的数据,可以参考⑨+1的解决办法。
Excel的数据有两个作用:
1)动态创建类
上图要创建类为:
- public class DynamicClass
- {
- public string id;
- public string name;
- public string chapter_x;
- public unit x;
- public unit y;
- }
2)实例化对象
然后Value的每一行都是用来实例化类DynamicClass的对象。
因此,首要任务是对Excel文件进行读写,我google了下,发现读取Excel数据大概有三种方法:
1)采用OleDB读取文件
2)引用com组件:Microsoft.Office.INterop.Excel.dll,读取文件(本文就是采取这种方法的)
3)其他格式读取,如转成CSV或Zip进行读取
当然还有其他利用第三方包的方法。
方法1)读取效率高,在C#使用OleDb读取Excel,生成SQL语句使用的就是这个方法,但是方法1)只能读取单元格的“正文”数据,像批注等不能读取到(没有看到相关api)。
我这里采用的是2)的方法且参考②中代码进行改装的,把读取Excel的数据保存在StringBuilder classSource,objectData中,classSource用来动态创建类的“源代码”,objectData则是把所有对象数据存储起来。
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.IO;
- using System.Reflection;
- using Excel = Microsoft.Office.Interop.Excel;
- namespace ReadXlsxData
- {
- static class ParseXlsx
- {
- public static Excel.Application m_ExcelFile = new Excel.Application();
- public static StringBuilder classSource;
- public static StringBuilder objectData;
- public static void CloseExcelApplication()
- {
- m_ExcelFile.Quit();
- m_ExcelFile = null;
- }
- public static void ReadExcelFile(string excelFilePath)
- {
- classSource = new StringBuilder(); ;
- objectData = new StringBuilder();
- Excel._Workbook m_Workbook;
- Excel._Worksheet m_Worksheet;
- object missing = System.Reflection.Missing.Value;
- Console.WriteLine("excelFilePath:" + excelFilePath);
- m_ExcelFile.Workbooks.Open(excelFilePath, missing, missing, missing, missing, missing, missing, missing, missing, missing
- , missing, missing, missing, missing, missing);
- m_ExcelFile.Visible = false;
- m_Workbook = m_ExcelFile.Workbooks[1];
- m_Worksheet = (Excel.Worksheet)m_Workbook.ActiveSheet;
- int clomn_Count = m_Worksheet.UsedRange.Columns.Count;
- int row_Count = m_Worksheet.UsedRange.Rows.Count;
- classSource.Append("using System;\n");
- classSource.Append("[Serializable]\n");
- classSource.Append("public class DynamicClass \n");
- classSource.Append("{\n");
- string propertyName,propertyType;
- for (int j = 2; j < clomn_Count + 1; j++)
- {
- propertyName = ((Excel.Range)m_Worksheet.Cells[3, j]).Text.ToString();
- propertyType=((Excel.Range)m_Worksheet.Cells[4, j]).Text.ToString() ;
- classSource.Append(" private "+propertyType+" _" + propertyName + " ;\n");
- classSource.Append(" public "+propertyType+" " + "" + propertyName + "\n");
- classSource.Append(" {\n");
- classSource.Append(" get{ return _" + propertyName + ";} \n");
- classSource.Append(" set{ _" + propertyName + " = value; }\n");
- classSource.Append(" }\n");
- //classSource.Append("\tpublic " + ((Excel.Range)m_Worksheet.Cells[4, j]).Text.ToString() + " " + ((Excel.Range)m_Worksheet.Cells[3, j]).Text.ToString() + ";\n");
- }
- classSource.Append("}\n");
- //Console.WriteLine(classSource);
- for (int i = 7; i < row_Count + 1; i++)//
- {
- for (int j = 2; j < clomn_Count + 1; j++)
- {
- objectData.Append(((Excel.Range)m_Worksheet.Cells[i, j]).Text.ToString() + ";");
- }
- objectData.Append("\n");
- try
- {
- }
- catch (Exception ex)
- {
- Console.WriteLine(ex.Message);
- }
- }
- //关闭Excel相关对象
- m_Worksheet = null;
- m_Workbook = null;
- }
- }
- }
但是,执行到Excel.Worksheet xlsWs = (Excel.Worksheet)xlsWb.Worksheets[1]时,提示“找不到编译动态表达式所需的一种或多种类型。是否缺少对 Microsoft.CSharp.dll 和 System.Core.dll 的引用? ”错误。
庆幸在③中找到了解决方法,就是在引用“Microsoft.Office.Interop.Excel”的属性中的“嵌入互操作类型”改为“false”就可以了,但是想③中说的,都不知道为什么,我也有不干,虽然问题解决,但是没有彻底弄明白,总是有种不痛快的感觉。
那就只有继续google,找到参考④⑤⑥的三篇文章,大概有了自己的理解:嵌入互操作类型的目的是为了减轻要将com组件和软件一起部署的负担,只有设为false时编译时会引入com组件程序集才能获得组件中的类型信息。
读取Excel数据总算没有问题了,接着就要实现动态创建类,发现⑦中写的比较不错简洁明了,大致明白了原理,改装了一下就有下面代码:
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.CodeDom;
- using System.CodeDom.Compiler;
- using Microsoft.CSharp;
- using System.Reflection;
- using System.Text.RegularExpressions;
- namespace ReadXlsxData
- {
- class DynamicClass
- {
- public static Assembly assembly;
- public static string className="DynamicClass";
- //创建编译器实例。
- public static CSharpCodeProvider provider = new CSharpCodeProvider();
- //设置编译参数。
- public static CompilerParameters paras = new CompilerParameters();
- //动态创建类
- public static void NewAssembly(string classSource,string className)
- {
- DynamicClass.className = className;
- //Console.Write(classSource);
- paras.GenerateExecutable = false;
- //paras.ReferencedAssemblies.Add("System.dll");
- paras.GenerateInMemory = false;
- paras.OutputAssembly = "MyClass.dll";
- System.Diagnostics.Debug.WriteLine(classSource);
- //编译代码。
- CompilerResults result = provider.CompileAssemblyFromSource(paras, classSource);
- //获取编译后的程序集。
- assembly = result.CompiledAssembly;
- }
- //利用数据进行实例化对象,并返回Dictionary进行存储
- public static Dictionary<int,object> NewInstances(string objectData,int keyIndex)
- {
- string[] str=objectData.Split('\n');
- string[] strs;
- Dictionary<int, object> genObject = new Dictionary<int, object>();
- string strTemp;
- for(int j=0;j<str.Length-1;j++)
- {
- strTemp = str[j];
- object generatedClass = assembly.CreateInstance(className);
- PropertyInfo[] infos = generatedClass.GetType().GetProperties();
- strs = strTemp.Split(';');
- if (strs.Length-1 == infos.Length)
- {
- for (int i = 0; i < strs.Length-1; i++)
- {
- if (infos[i].PropertyType.Name== "String")
- infos[i].SetValue(generatedClass, strs[i], null);
- else
- {
- System.Type t = infos[i].PropertyType;
- System.Reflection.MethodInfo method = t.GetMethod("Parse",new Type[]{typeof (string)});
- //调用方法的一些标志位,这里的含义是Public并且是实例方法,这也是默认的值
- Object obj = Activator.CreateInstance(t);
- System.Reflection.BindingFlags flag = System.Reflection.BindingFlags.Public | BindingFlags.Static | System.Reflection.BindingFlags.Instance;
- //GetValue方法的参数
- //infos[i].SetValue(generatedClass, null, null);
- if(strs[i]==""||strs[i]==null)
- {
- infos[i].SetValue(generatedClass, null, null);
- }
- else
- {
- object[] parameters = new object[] { strs[i] };
- //取得方法返回的值
- //object returnValue = method.Invoke(obj, flag, Type.DefaultBinder, parameters, null);
- Console.WriteLine(method.Invoke(obj, flag, Type.DefaultBinder, parameters, null));
- infos[i].SetValue(generatedClass, method.Invoke(obj, flag, Type.DefaultBinder, parameters, null), null);
- }
- }
- }
- }
- genObject.Add(int.Parse(strs[keyIndex]), generatedClass);
- }
- return genObject;
- }
- }
- }
这里还是出现三个问题:
1)在classSource没有添加“[Serializable]\n”后面不能进行序列化,这点没有什么可说的,很简单的。
2)要把成员变量封装成对应的属性,才能用GetType().GetProperties()返回属性。
3)在1)添加了“[Serializable]\n”,然后还是在执行“System.Reflection.Assembly assembly = cr.CompiledAssembly;”出现:未能加载文件或程序集“file:///C:Users\Administrator\AppData\Local\Temps\23y9bgt.dll”或它的某一个依赖项。系统找不到指定的文件的错误。
4)在解决问题3)后,能够进行序列化操作,但是在反序列化出现:未处理SerializationException,无法找到程序集"xxxx,Version=0.0.0,Culture=neutral,PulbicKeyToken=null"。的错误。
对于问题3)和4)有点措手不及,对比了⑦中的差别是他的代码不要求序列化,然后我加了“[Serializable]\n"就有问题了,显然是没有生成对应的dll文件,因为在⑦中CompilerParameters.GenerateInMemory = true;也就是说⑦中生成的dll库没有在本地生成,直接保存在内存中,但是反序列化又要用到dll文件(这是就不能找到)就出现了4)的错误,而问题3)我想是没有“源代码”classSource没有引入基本类库System,所以加上"using System;",就解决了。问题4)只要把编译生成的程序集保存在当前目录下,这样需要的时候(序列化)会自动引入。可以参考⑨有详细一点的说明。
后面我为代码添加了注释但是注释行尾没有添加“\n”,也出现了3)的问题,所以3)问题的根本原因是自己动态创建的源代码不正确。
序列化一般有三种方法:BinaryFormatter,SoapFormatter和XML序列化,各自有不同的差异,下面测试中使用的是BinaryFormatter进行序列化和反序列化的。
还发现一个细节的差别:Visual Studio 2010 C++调试的当前目录和C#是不一样的,C++是在代码所在的根目录,而C#则是在Debug的目录下。
最后给出测试Main函数:
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.Runtime.Serialization.Formatters.Binary;
- using System.IO;
- namespace ReadXlsxData
- {
- class MainTest
- {
- public static void Main(string[] args)
- {
- string file = "E:\\work\\ReadXlsxData\\default.xlsx";
- ParseXlsx.ReadExcelFile(file);
- ParseXlsx.CloseExcelApplication();
- DynamicClass.NewAssembly( ParseXlsx.classSource.ToString(),"DynamicClass");
- Dictionary<int,object> dic=DynamicClass.NewInstances(ParseXlsx.objectData.ToString(),0);
- string strFile = "default.data";
- FileStream fs = new FileStream(strFile, FileMode.Create);
- BinaryFormatter formatter = new BinaryFormatter();
- formatter.Serialize(fs, dic);
- Dictionary<int, object> dicT;
- fs.Close();
- fs = new FileStream(strFile, FileMode.Open);
- dicT = (Dictionary<int, object>)formatter.Deserialize(fs);
- Console.Read();
- }
- }
- }
后面把这个工具代码用到Unity的项目中,反序列化时出现两个问题:1)无法找到程序集,2)XXX Field not found in XXX class。其中,问题1)参考11已经给了解决方法,问题2)就很诡异了,因为反序列化用的类是有AS输出的,在中文注释后面换行符使用的是“\n",然后在Unity中总是出现某个类的某个域找不到,但是在代码编辑器中,都不会显示语法错误,然后在VS中对类的代码进行粘贴复制竟然就可以(MonoDevelop还是不可以)了,只是有些注释的换行符被VS转成“\r\n",可能是编译器对中文注释和换行没有解析好导致的,只有输出的时候改成“\r\n”来换行了,好奇葩的Bug呀。
增补于 2013.11.13
花了一天时间,终于解决了,虽然效率低了点,因为之前没有学过C#,直接就开始用,很多概念还不是很清楚,今天的这几个问题在google上几乎可以检索到(很少)但是没有给出得到解决,然后只有从问题的本质上去理解,加上其他一些相关文章的引导,还是顺利的把问题解决了,而且尝试的技术点也挺多的:读取Excel数据,动态创建类和反射,序列化和反序列化,当然最大的收获是希望能够提升自己对问题的把握度。
参考:
①selen: http://blog.sina.com.cn/s/blog_6325aebe0100nhmq.html
②zhaozhi_1983: http://blog.csdn.net/zhaozhi_1983/article/details/2866099
③furenjian: http://www.cnblogs.com/furenjian/articles/3054491.html
④pnljs: http://www.cnblogs.com/pnljs/archive/2012/02/20/2359313.html
⑤Daniel Cazzulino's Blog: http://blogs.clariusconsulting.net/kzu/check-your-embed-interop-types-flag-when-doing-visual-studio-extensibility-work/
⑥Sam Ng's Blog: http://blogs.msdn.com/b/samng/archive/2010/01/24/the-pain-of-deploying-primary-interop-assemblies.aspx
⑦永春阁: http://www.cnblogs.com/firstyi/archive/2008/03/07/1094652.html
⑧ACG: http://social.msdn.microsoft.com/forums/en-US/netfxbcl/thread/3b64ce7e-1ad5-4164-b8d0-e3c94970e823
⑨Taotesea: C#动态程序集的加载、创建实例、序列化与反序列化http://blog.sina.com.cn/s/blog_4ba5666e0100vrhb.html
⑨+1 M I developer: http://www.codeproject.com/Articles/208075/How-to-Read-and-Write-xlsx-Excel-2007-file-Part-I
⑨+2djian: http://www.cnblogs.com/djian/archive/2011/01/25/1944586.html