123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343 |
- using System;
- using System.Collections.Generic;
- using System.ComponentModel;
- using System.IO;
- using System.Linq;
- using System.Reflection;
- using System.Text;
- using System.Text.RegularExpressions;
- using UnityEngine;
- namespace Plugins.CxShine.Csv
- {
- // This class uses Reflection and Linq so it's not the fastest thing in the
- // world; however I only use it in development builds where we want to allow
- // game data to be easily tweaked so this isn't an issue; I would recommend
- // you do the same.
- public static class CsvUtil
- {
- // Quote semicolons too since some apps e.g. Numbers don't like them
- private static readonly char[] quotedChars = { ',', ';', '\n' };
- // Load a CSV into a list of struct/classes from a file where each line = 1 object
- // First line of the CSV must be a header containing property names
- // Can optionally include any other columns headed with #foo, which are ignored
- // E.g. you can include a #Description column to provide notes which are ignored
- // This method throws file exceptions if file is not found
- // Field names are matched case-insensitive for convenience
- // @param filename File to load
- // @param strict If true, log errors if a line doesn't have enough
- // fields as per the header. If false, ignores and just fills what it can
- public static List<T> LoadObjects<T>(string filename, bool strict = true) where T : new()
- {
- using (var stream = File.OpenRead(filename))
- {
- using (var rdr = new StreamReader(stream))
- {
- return LoadObjects<T>(rdr, strict);
- }
- }
- }
- // Load a CSV into a list of struct/classes from a stream where each line = 1 object
- // First line of the CSV must be a header containing property names
- // Can optionally include any other columns headed with #foo, which are ignored
- // E.g. you can include a #Description column to provide notes which are ignored
- // Field names are matched case-insensitive for convenience
- // @param rdr Input reader
- // @param strict If true, log errors if a line doesn't have enough
- // fields as per the header. If false, ignores and just fills what it can
- public static List<T> LoadObjects<T>(TextReader rdr, bool strict = true) where T : new()
- {
- var ret = new List<T>();
- var header = rdr.ReadLine();
- var fieldDefs = ParseHeader(header);
- var fi = typeof(T).GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
- var pi =
- typeof(T).GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
- var isValueType = typeof(T).IsValueType;
- string line;
- while ((line = rdr.ReadLine()) != null)
- {
- var obj = new T();
- // box manually to avoid issues with structs
- object boxed = obj;
- if (ParseLineToObject(line, fieldDefs, fi, pi, boxed, strict))
- {
- // unbox value types
- if (isValueType)
- obj = (T)boxed;
- ret.Add(obj);
- }
- }
- return ret;
- }
- // Load a CSV file containing fields for a single object from a file
- // No header is required, but it can be present with '#' prefix
- // First column is property name, second is value
- // You can optionally include other columns for descriptions etc, these are ignored
- // If you want to include a header, make sure the first line starts with '#'
- // then it will be ignored (as will any lines that start that way)
- // This method throws file exceptions if file is not found
- // Field names are matched case-insensitive for convenience
- public static void LoadObject<T>(string filename, ref T destObject)
- {
- using (var stream = File.Open(filename, FileMode.Open))
- {
- using (var rdr = new StreamReader(stream))
- {
- LoadObject(rdr, ref destObject);
- }
- }
- }
- // Load a CSV file containing fields for a single object from a stream
- // No header is required, but it can be present with '#' prefix
- // First column is property name, second is value
- // You can optionally include other columns for descriptions etc, these are ignored
- // Field names are matched case-insensitive for convenience
- public static void LoadObject<T>(TextReader rdr, ref T destObject)
- {
- var fi = typeof(T).GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
- var pi =
- typeof(T).GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
- // prevent auto-boxing causing problems with structs
- object nonValueObject = destObject;
- string line;
- while ((line = rdr.ReadLine()) != null)
- {
- // Ignore optional header lines
- if (line.StartsWith("#"))
- continue;
- var vals = EnumerateCsvLine(line).ToArray();
- if (vals.Length >= 2)
- SetField(RemoveSpaces(vals[0].Trim()), vals[1], fi, pi, nonValueObject);
- else
- Debug.LogWarning(string.Format("CsvUtil: ignoring line '{0}': not enough fields", line));
- }
- if (typeof(T).IsValueType)
- // unbox
- destObject = (T)nonValueObject;
- }
- // Save a single object to a CSV file
- // Will write 1 line per field, first column is name, second is value
- // This method throws exceptions if unable to write
- public static void SaveObject<T>(T obj, string filename)
- {
- using (var stream = File.Open(filename, FileMode.Create))
- {
- using (var wtr = new StreamWriter(stream, Encoding.UTF8))
- {
- SaveObject(obj, wtr);
- }
- }
- }
- // Save a single object to a CSV stream
- // Will write 1 line per field, first column is name, second is value
- // This method throws exceptions if unable to write
- public static void SaveObject<T>(T obj, TextWriter w)
- {
- var fi = typeof(T).GetFields();
- var firstLine = true;
- foreach (var f in fi)
- {
- // Good CSV files don't have a trailing newline so only add here
- if (firstLine)
- firstLine = false;
- else
- w.Write(Environment.NewLine);
- w.Write(f.Name);
- w.Write(",");
- var val = f.GetValue(obj).ToString();
- // Quote if necessary
- if (val.IndexOfAny(quotedChars) != -1) val = string.Format("\"{0}\"", val);
- w.Write(val);
- }
- }
- // Save multiple objects to a CSV file
- // Writes a header line with field names, followed by one line per
- // object with each field value in each column
- // This method throws exceptions if unable to write
- public static void SaveObjects<T>(IEnumerable<T> objs, string filename)
- {
- using (var stream = File.Open(filename, FileMode.Create))
- {
- using (var wtr = new StreamWriter(stream, Encoding.UTF8))
- {
- SaveObjects(objs, wtr);
- }
- }
- }
- // Save multiple objects to a CSV stream
- // Writes a header line with field names, followed by one line per
- // object with each field value in each column
- // This method throws exceptions if unable to write
- public static void SaveObjects<T>(IEnumerable<T> objs, TextWriter w)
- {
- var fi = typeof(T).GetFields();
- WriteHeader<T>(fi, w);
- var firstLine = true;
- foreach (var obj in objs)
- {
- // Good CSV files don't have a trailing newline so only add here
- if (firstLine)
- firstLine = false;
- else
- w.Write(Environment.NewLine);
- WriteObjectToLine(obj, fi, w);
- }
- }
- private static void WriteHeader<T>(FieldInfo[] fi, TextWriter w)
- {
- var firstCol = true;
- foreach (var f in fi)
- {
- // Good CSV files don't have a trailing comma so only add here
- if (firstCol)
- firstCol = false;
- else
- w.Write(",");
- w.Write(f.Name);
- }
- w.Write(Environment.NewLine);
- }
- private static void WriteObjectToLine<T>(T obj, FieldInfo[] fi, TextWriter w)
- {
- var firstCol = true;
- foreach (var f in fi)
- {
- // Good CSV files don't have a trailing comma so only add here
- if (firstCol)
- firstCol = false;
- else
- w.Write(",");
- var val = f.GetValue(obj).ToString();
- // Quote if necessary
- if (val.IndexOfAny(quotedChars) != -1) val = string.Format("\"{0}\"", val);
- w.Write(val);
- }
- }
- // Parse the header line and return a mapping of field names to column
- // indexes. Columns which have a '#' prefix are ignored.
- private static Dictionary<string, int> ParseHeader(string header)
- {
- var headers = new Dictionary<string, int>();
- var n = 0;
- foreach (var field in EnumerateCsvLine(header))
- {
- var trimmed = field.Trim();
- if (!trimmed.StartsWith("#"))
- {
- trimmed = RemoveSpaces(trimmed);
- headers[trimmed] = n;
- }
- ++n;
- }
- return headers;
- }
- // Parse an object line based on the header, return true if any fields matched
- private static bool ParseLineToObject(string line, Dictionary<string, int> fieldDefs, FieldInfo[] fi,
- PropertyInfo[] pi, object destObject, bool strict)
- {
- var values = EnumerateCsvLine(line).ToArray();
- var setAny = false;
- foreach (var field in fieldDefs.Keys)
- {
- var index = fieldDefs[field];
- if (index < values.Length)
- {
- var val = values[index];
- setAny = SetField(field, val, fi, pi, destObject) || setAny;
- }
- else if (strict)
- {
- //Debug.LogWarning(string.Format("CsvUtil: error parsing line '{0}': not enough fields", line));
- }
- }
- return setAny;
- }
- private static bool SetField(string fieldName, string val, FieldInfo[] fi, PropertyInfo[] pi, object destObject)
- {
- var result = false;
- foreach (var p in pi)
- // Case insensitive comparison
- if (string.Compare(fieldName, p.Name, true) == 0)
- {
- // Might need to parse the string into the property type
- var typedVal = p.PropertyType == typeof(string) ? val : ParseString(val, p.PropertyType);
- p.SetValue(destObject, typedVal, null);
- result = true;
- break;
- }
- foreach (var f in fi)
- // Case insensitive comparison
- if (string.Compare(fieldName, f.Name, true) == 0)
- {
- // Might need to parse the string into the field type
- var typedVal = f.FieldType == typeof(string) ? val : ParseString(val, f.FieldType);
- f.SetValue(destObject, typedVal);
- result = true;
- break;
- }
- return result;
- }
- private static object ParseString(string strValue, Type t)
- {
- try
- {
- if (strValue.Length == 0) strValue = "0";
- var cv = TypeDescriptor.GetConverter(t);
- return cv.ConvertFromInvariantString(strValue);
- }
- catch (Exception e)
- {
- Console.WriteLine(e);
- Debug.Log("Csv model 字段类型错误" + t + ":" + strValue);
- throw;
- }
- }
- private static IEnumerable<string> EnumerateCsvLine(string line)
- {
- // Regex taken from http://wiki.unity3d.com/index.php?title=CSVReader
- foreach (Match m in Regex.Matches(line,
- @"(((?<x>(?=[,\r\n]+))|""(?<x>([^""]|"""")+)""|(?<x>[^,\r\n]+)),?)",
- RegexOptions.ExplicitCapture))
- yield return m.Groups[1].Value;
- }
- private static string RemoveSpaces(string strValue)
- {
- return Regex.Replace(strValue, @"\s", string.Empty);
- }
- }
- }
|