CsvUtil.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Reflection;
  7. using System.Text;
  8. using System.Text.RegularExpressions;
  9. using UnityEngine;
  10. namespace Plugins.CxShine.Csv
  11. {
  12. // This class uses Reflection and Linq so it's not the fastest thing in the
  13. // world; however I only use it in development builds where we want to allow
  14. // game data to be easily tweaked so this isn't an issue; I would recommend
  15. // you do the same.
  16. public static class CsvUtil
  17. {
  18. // Quote semicolons too since some apps e.g. Numbers don't like them
  19. private static readonly char[] quotedChars = { ',', ';', '\n' };
  20. // Load a CSV into a list of struct/classes from a file where each line = 1 object
  21. // First line of the CSV must be a header containing property names
  22. // Can optionally include any other columns headed with #foo, which are ignored
  23. // E.g. you can include a #Description column to provide notes which are ignored
  24. // This method throws file exceptions if file is not found
  25. // Field names are matched case-insensitive for convenience
  26. // @param filename File to load
  27. // @param strict If true, log errors if a line doesn't have enough
  28. // fields as per the header. If false, ignores and just fills what it can
  29. public static List<T> LoadObjects<T>(string filename, bool strict = true) where T : new()
  30. {
  31. using (var stream = File.OpenRead(filename))
  32. {
  33. using (var rdr = new StreamReader(stream))
  34. {
  35. return LoadObjects<T>(rdr, strict);
  36. }
  37. }
  38. }
  39. // Load a CSV into a list of struct/classes from a stream where each line = 1 object
  40. // First line of the CSV must be a header containing property names
  41. // Can optionally include any other columns headed with #foo, which are ignored
  42. // E.g. you can include a #Description column to provide notes which are ignored
  43. // Field names are matched case-insensitive for convenience
  44. // @param rdr Input reader
  45. // @param strict If true, log errors if a line doesn't have enough
  46. // fields as per the header. If false, ignores and just fills what it can
  47. public static List<T> LoadObjects<T>(TextReader rdr, bool strict = true) where T : new()
  48. {
  49. var ret = new List<T>();
  50. var header = rdr.ReadLine();
  51. var fieldDefs = ParseHeader(header);
  52. var fi = typeof(T).GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
  53. var pi =
  54. typeof(T).GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
  55. var isValueType = typeof(T).IsValueType;
  56. string line;
  57. while ((line = rdr.ReadLine()) != null)
  58. {
  59. var obj = new T();
  60. // box manually to avoid issues with structs
  61. object boxed = obj;
  62. if (ParseLineToObject(line, fieldDefs, fi, pi, boxed, strict))
  63. {
  64. // unbox value types
  65. if (isValueType)
  66. obj = (T)boxed;
  67. ret.Add(obj);
  68. }
  69. }
  70. return ret;
  71. }
  72. // Load a CSV file containing fields for a single object from a file
  73. // No header is required, but it can be present with '#' prefix
  74. // First column is property name, second is value
  75. // You can optionally include other columns for descriptions etc, these are ignored
  76. // If you want to include a header, make sure the first line starts with '#'
  77. // then it will be ignored (as will any lines that start that way)
  78. // This method throws file exceptions if file is not found
  79. // Field names are matched case-insensitive for convenience
  80. public static void LoadObject<T>(string filename, ref T destObject)
  81. {
  82. using (var stream = File.Open(filename, FileMode.Open))
  83. {
  84. using (var rdr = new StreamReader(stream))
  85. {
  86. LoadObject(rdr, ref destObject);
  87. }
  88. }
  89. }
  90. // Load a CSV file containing fields for a single object from a stream
  91. // No header is required, but it can be present with '#' prefix
  92. // First column is property name, second is value
  93. // You can optionally include other columns for descriptions etc, these are ignored
  94. // Field names are matched case-insensitive for convenience
  95. public static void LoadObject<T>(TextReader rdr, ref T destObject)
  96. {
  97. var fi = typeof(T).GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
  98. var pi =
  99. typeof(T).GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
  100. // prevent auto-boxing causing problems with structs
  101. object nonValueObject = destObject;
  102. string line;
  103. while ((line = rdr.ReadLine()) != null)
  104. {
  105. // Ignore optional header lines
  106. if (line.StartsWith("#"))
  107. continue;
  108. var vals = EnumerateCsvLine(line).ToArray();
  109. if (vals.Length >= 2)
  110. SetField(RemoveSpaces(vals[0].Trim()), vals[1], fi, pi, nonValueObject);
  111. else
  112. Debug.LogWarning(string.Format("CsvUtil: ignoring line '{0}': not enough fields", line));
  113. }
  114. if (typeof(T).IsValueType)
  115. // unbox
  116. destObject = (T)nonValueObject;
  117. }
  118. // Save a single object to a CSV file
  119. // Will write 1 line per field, first column is name, second is value
  120. // This method throws exceptions if unable to write
  121. public static void SaveObject<T>(T obj, string filename)
  122. {
  123. using (var stream = File.Open(filename, FileMode.Create))
  124. {
  125. using (var wtr = new StreamWriter(stream, Encoding.UTF8))
  126. {
  127. SaveObject(obj, wtr);
  128. }
  129. }
  130. }
  131. // Save a single object to a CSV stream
  132. // Will write 1 line per field, first column is name, second is value
  133. // This method throws exceptions if unable to write
  134. public static void SaveObject<T>(T obj, TextWriter w)
  135. {
  136. var fi = typeof(T).GetFields();
  137. var firstLine = true;
  138. foreach (var f in fi)
  139. {
  140. // Good CSV files don't have a trailing newline so only add here
  141. if (firstLine)
  142. firstLine = false;
  143. else
  144. w.Write(Environment.NewLine);
  145. w.Write(f.Name);
  146. w.Write(",");
  147. var val = f.GetValue(obj).ToString();
  148. // Quote if necessary
  149. if (val.IndexOfAny(quotedChars) != -1) val = string.Format("\"{0}\"", val);
  150. w.Write(val);
  151. }
  152. }
  153. // Save multiple objects to a CSV file
  154. // Writes a header line with field names, followed by one line per
  155. // object with each field value in each column
  156. // This method throws exceptions if unable to write
  157. public static void SaveObjects<T>(IEnumerable<T> objs, string filename)
  158. {
  159. using (var stream = File.Open(filename, FileMode.Create))
  160. {
  161. using (var wtr = new StreamWriter(stream, Encoding.UTF8))
  162. {
  163. SaveObjects(objs, wtr);
  164. }
  165. }
  166. }
  167. // Save multiple objects to a CSV stream
  168. // Writes a header line with field names, followed by one line per
  169. // object with each field value in each column
  170. // This method throws exceptions if unable to write
  171. public static void SaveObjects<T>(IEnumerable<T> objs, TextWriter w)
  172. {
  173. var fi = typeof(T).GetFields();
  174. WriteHeader<T>(fi, w);
  175. var firstLine = true;
  176. foreach (var obj in objs)
  177. {
  178. // Good CSV files don't have a trailing newline so only add here
  179. if (firstLine)
  180. firstLine = false;
  181. else
  182. w.Write(Environment.NewLine);
  183. WriteObjectToLine(obj, fi, w);
  184. }
  185. }
  186. private static void WriteHeader<T>(FieldInfo[] fi, TextWriter w)
  187. {
  188. var firstCol = true;
  189. foreach (var f in fi)
  190. {
  191. // Good CSV files don't have a trailing comma so only add here
  192. if (firstCol)
  193. firstCol = false;
  194. else
  195. w.Write(",");
  196. w.Write(f.Name);
  197. }
  198. w.Write(Environment.NewLine);
  199. }
  200. private static void WriteObjectToLine<T>(T obj, FieldInfo[] fi, TextWriter w)
  201. {
  202. var firstCol = true;
  203. foreach (var f in fi)
  204. {
  205. // Good CSV files don't have a trailing comma so only add here
  206. if (firstCol)
  207. firstCol = false;
  208. else
  209. w.Write(",");
  210. var val = f.GetValue(obj).ToString();
  211. // Quote if necessary
  212. if (val.IndexOfAny(quotedChars) != -1) val = string.Format("\"{0}\"", val);
  213. w.Write(val);
  214. }
  215. }
  216. // Parse the header line and return a mapping of field names to column
  217. // indexes. Columns which have a '#' prefix are ignored.
  218. private static Dictionary<string, int> ParseHeader(string header)
  219. {
  220. var headers = new Dictionary<string, int>();
  221. var n = 0;
  222. foreach (var field in EnumerateCsvLine(header))
  223. {
  224. var trimmed = field.Trim();
  225. if (!trimmed.StartsWith("#"))
  226. {
  227. trimmed = RemoveSpaces(trimmed);
  228. headers[trimmed] = n;
  229. }
  230. ++n;
  231. }
  232. return headers;
  233. }
  234. // Parse an object line based on the header, return true if any fields matched
  235. private static bool ParseLineToObject(string line, Dictionary<string, int> fieldDefs, FieldInfo[] fi,
  236. PropertyInfo[] pi, object destObject, bool strict)
  237. {
  238. var values = EnumerateCsvLine(line).ToArray();
  239. var setAny = false;
  240. foreach (var field in fieldDefs.Keys)
  241. {
  242. var index = fieldDefs[field];
  243. if (index < values.Length)
  244. {
  245. var val = values[index];
  246. setAny = SetField(field, val, fi, pi, destObject) || setAny;
  247. }
  248. else if (strict)
  249. {
  250. //Debug.LogWarning(string.Format("CsvUtil: error parsing line '{0}': not enough fields", line));
  251. }
  252. }
  253. return setAny;
  254. }
  255. private static bool SetField(string fieldName, string val, FieldInfo[] fi, PropertyInfo[] pi, object destObject)
  256. {
  257. var result = false;
  258. foreach (var p in pi)
  259. // Case insensitive comparison
  260. if (string.Compare(fieldName, p.Name, true) == 0)
  261. {
  262. // Might need to parse the string into the property type
  263. var typedVal = p.PropertyType == typeof(string) ? val : ParseString(val, p.PropertyType);
  264. p.SetValue(destObject, typedVal, null);
  265. result = true;
  266. break;
  267. }
  268. foreach (var f in fi)
  269. // Case insensitive comparison
  270. if (string.Compare(fieldName, f.Name, true) == 0)
  271. {
  272. // Might need to parse the string into the field type
  273. var typedVal = f.FieldType == typeof(string) ? val : ParseString(val, f.FieldType);
  274. f.SetValue(destObject, typedVal);
  275. result = true;
  276. break;
  277. }
  278. return result;
  279. }
  280. private static object ParseString(string strValue, Type t)
  281. {
  282. try
  283. {
  284. if (strValue.Length == 0) strValue = "0";
  285. var cv = TypeDescriptor.GetConverter(t);
  286. return cv.ConvertFromInvariantString(strValue);
  287. }
  288. catch (Exception e)
  289. {
  290. Console.WriteLine(e);
  291. Debug.Log("Csv model 字段类型错误" + t + ":" + strValue);
  292. throw;
  293. }
  294. }
  295. private static IEnumerable<string> EnumerateCsvLine(string line)
  296. {
  297. // Regex taken from http://wiki.unity3d.com/index.php?title=CSVReader
  298. foreach (Match m in Regex.Matches(line,
  299. @"(((?<x>(?=[,\r\n]+))|""(?<x>([^""]|"""")+)""|(?<x>[^,\r\n]+)),?)",
  300. RegexOptions.ExplicitCapture))
  301. yield return m.Groups[1].Value;
  302. }
  303. private static string RemoveSpaces(string strValue)
  304. {
  305. return Regex.Replace(strValue, @"\s", string.Empty);
  306. }
  307. }
  308. }