Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9743

C# Attributeを利用した、お手軽!CSVの入出力クラスを作ろう!

$
0
0

お久しぶりです.
株式会社グレンジでエンジニアマネージャー兼クライアントエンジニアのmesshiです.
(CyberAgentグループ、ゲーム事業部の子会社です)

さて、今年もAdventCalendarの時期がやってきたので、ひょっこり投稿しておこうかなと思います.

まえおき

今回取り上げる記事は、次のようなニーズがある方に役立つかと思います.

・プロダクトへ入れるソースコードは最低限にしたい (ThirdParty製を入れたくない)
・入出力部分のメンテナンスはしたくない
・デバッグ用のコードなので、多少パフォーマンス悪くても良い

私の利用用途は少し特殊で、ゲーム事業部の各子会社のチューニングを協力させてもらう事が多いのですが
チューニングのイテレーションをガンガン回すために、次のようなワークフローを作ります.
「①実機計測」→「②計測結果をCsvに書き出し」→「③Csvを送信」→「④データとして取り込み可視化」

その際、Csvに関する部分に関して、上記のようなニーズがありました.

では、前置きが長くなりましたが、早速作っていきましょう!

Attributeを定義する

AttributeにCsvの入出力に必要なメタ情報を持たせます.
ヘッダー名、ヘッダーの順番さえあれば事足りるでしょう.
それを定義したクラスを生成します.

SimapleCsvAttribute.cs
[AttributeUsage(AttributeTargets.Property,AllowMultiple=false)]publicclassSimpleCsvAttribute:Attribute{#regionvariable/// 列順privateint_order;#endregion#regionProperty/// 列順publicintOrder=>_order;/// 名前publicstringName{get;set;}#endregion#regionmethodpublicSimpleCsvAttribute(intorder){_order=order;}#endregion}

AttributeUsageは、Attributeを付与できる対象のことです.
今回はProperty属性をしています

AllowMultipleは複数のAttributeを設定できるかどうかです
デフォルトfalseなので指定しなくてもOKです

データを定義する

プロパティでCsvに書き出すデータを定義します.

ProfileData.cs
/// <summary>/// ProfileData/// </summary>publicclassProfileData{[SimpleCsv(1)]publicstringName{get;set;}[SimpleCsv(2,Name="処理時間")]publicintTime{get;set;}}

[SimpleCsv]のAttributeを付けなければ書き出しが行われません.
Nameの指定をしなかった場合は、プロパティの変数名をそのまま使用します

書き込みクラスを定義する

コンストラクタでGenericに指定されたクラスから、Attributeの情報を抜き出し、メンバ変数のリストに格納します.
あとは、そのAttributeの情報からデータを書き込むだけです.

SimpleCsvWriter.cs
usingSystem;usingSystem.Collections.Generic;usingSystem.IO;usingSystem.Text.RegularExpressions;usingSystem.Linq;usingSystem.Reflection;usingSystem.Text;/// <summary>/// SimpleCsvWriter/// </summary>publicclassSimpleCsvWriter<T>:IDisposablewhereT:class,new(){#regiondefine/// 区切り文字privateconststringDELIMITER=",";/// 引用符の正規表現privateconststringQUOTE_REGEX="[\"\\r\\n,]";/// 属性プロパティ情報publicclassAttributePropertyInfo{publicPropertyInfoProperty;publicSimpleCsvAttributeCsvAttribute;}/// 値取得のRegexprivatestaticreadonlyRegexVALUE_REGEX=newRegex(QUOTE_REGEX);/// ファイルのエンコードタイプprivatestaticreadonlyEncodingFILE_ENCODING=Encoding.UTF8;#endregion#regionvariable/// StreamWriterprivateStreamWriter_writer;/// 属性の情報リストprivateList<AttributePropertyInfo>_attirbuteInfos;/// データリストprivateT[]_records;#endregion#regionmethod/// <summary>/// コンストラクタ/// </summary>publicSimpleCsvWriter(stringdirectory,stringfileName,T[]records){if(!Directory.Exists(directory)){Directory.CreateDirectory(directory);}_records=records;varfilePath=directory+Path.DirectorySeparatorChar+fileName+".csv";_writer=newStreamWriter(filePath,false,FILE_ENCODING);vartargetType=GetType().GetGenericArguments()[0];_attirbuteInfos=targetType.GetProperties().Select(x=>newAttributePropertyInfo(){Property=x,CsvAttribute=x.GetCustomAttributes(typeof(SimpleCsvAttribute),false).FirstOrDefault()asSimpleCsvAttribute}).Where(x=>x.CsvAttribute!=null).OrderBy(x=>x.CsvAttribute.Order).ToList();}/// <summary>/// 書き込む/// </summary>publicvoidWrite(){WriteHeader();for(inti=0;i<_records.Length;i++){WriteLine(_records[i]);}}/// <summary>/// ヘッダーを書き込む/// </summary>privatevoidWriteHeader(){varheaders=_attirbuteInfos.Select(x=>x.CsvAttribute.Name??x.Property.Name).Select(x=>Quote(x)).ToArray();_writer.WriteLine(string.Join(DELIMITER,headers));}/// <summary>/// 1行書き込む/// </summary>privatevoidWriteLine(Trecord){varvalues=_attirbuteInfos.Select(x=>x.Property.GetValue(record)).Select(x=>Quote(x)).ToArray();_writer.WriteLine(string.Join(DELIMITER,values));}/// <summary>/// 引用符を変換する (コンマやダブルクォーテーションなど)/// </summary>privatestringQuote(objectvalue){stringtarget=value!=null?value.ToString():"";if(VALUE_REGEX.Match(target).Success){return"\""+target.Replace("\"","\"\"")+"\"";}else{returntarget;}}/// <summary>/// 破棄処理/// </summary>publicvoidDispose(){_writer.Dispose();}#endregion}

読み込みクラスを定義する

書き込みとほぼ同じ要領で行います

SimpleCsvReader.cs
usingSystem;usingSystem.Collections.Generic;usingSystem.IO;usingSystem.Text.RegularExpressions;usingSystem.Linq;usingSystem.Reflection;usingSystem.Text;/// <summary>/// SimpleCsvReader/// </summary>publicclassSimpleCsvReader<T>:IDisposablewhereT:class,new(){#regiondefine/// 引用符privateconststringQUOTE="\"";/// 改行正規表現privateconststringNEW_LINE_PATTERN="(?:\x0D\x0A|[\x0D\x0A])?$";/// 区切り正規表現privateconststringDELIMITER_PATTERN="(\"[^\"]*(?:\"\"[^\"]*)*\"|[^,]*),";/// 属性プロパティ情報publicclassAttributePropertyInfo{publicPropertyInfoProperty;publicSimpleCsvAttributeCsvAttribute;}/// 引用符の正規表現privatestaticreadonlyRegexQUOTE_REGEX=newRegex(QUOTE);/// 改行コードの正規表現privatestaticreadonlyRegexNEW_LINE_REGEX=newRegex(NEW_LINE_PATTERN,RegexOptions.Singleline);/// 区切り文字の正規表現privatestaticreadonlyRegexDELIMITER_REGEX=newRegex(DELIMITER_PATTERN);/// ファイルのエンコードタイプprivatestaticreadonlyEncodingFILE_ENCODING=Encoding.UTF8;#endregion#regionvariable/// StreamReaderprivateStreamReader_reader;/// 属性の情報リストprivateList<AttributePropertyInfo>_attirbuteInfos;/// データリストprivateT[]_records;#endregion#regionmethod/// <summary>/// コンストラクタ/// </summary>publicSimpleCsvReader(stringdirectory,stringfileName){if(!Directory.Exists(directory)){Directory.CreateDirectory(directory);}varfilePath=directory+Path.DirectorySeparatorChar+fileName+".csv";try{_reader=newStreamReader(filePath,FILE_ENCODING);}catch(Exceptionex){throwex;}vartargetType=GetType().GetGenericArguments()[0];_attirbuteInfos=targetType.GetProperties().Select(x=>newAttributePropertyInfo(){Property=x,CsvAttribute=x.GetCustomAttributes(typeof(SimpleCsvAttribute),false).FirstOrDefault()asSimpleCsvAttribute}).Where(x=>x.CsvAttribute!=null).OrderBy(x=>x.CsvAttribute.Order).ToList();}/// <summary>/// 読み込み/// </summary>publicT[]Read(boolhasHeader=true){if(_reader==null){returnnull;}if(hasHeader){// ヘッダー分だけ進める_reader.ReadLine();}List<T>dataList=newList<T>();while(!_reader.EndOfStream){varline=_reader.ReadLine();if(line==null){break;}// 改行を考慮して行を読み込むwhile(!HasEnoughQuote(line)){line+="\n"+_reader.ReadLine();if(_reader.EndOfStream){break;}}// 改行コードを排除するline=NEW_LINE_REGEX.Replace(line,"");// 要素分解を行うline+=",";varmatches=DELIMITER_REGEX.Matches(line);varcolumns=matches.Cast<Match>().Select(x=>Dequote(x)).ToArray();// データを作成するvardata=newT();for(inti=0;i<columns.Length;i++){varattribute=_attirbuteInfos[i];attribute.Property.SetValue(data,Convert.ChangeType(columns[i],attribute.Property.PropertyType));}dataList.Add(data);}returndataList.ToArray();}/// <summary>/// バイト配列で読み込む/// </summary>publicbyte[]ReadBytes(){if(_reader==null){returnnull;}byte[]readBytes=null;using(MemoryStreammemoryStream=newMemoryStream()){_reader.BaseStream.CopyTo(memoryStream);readBytes=memoryStream.ToArray();}returnreadBytes;}/// <summary>/// 引用符が十分であるかどうか/// </summary>privateboolHasEnoughQuote(stringline){return(QUOTE_REGEX.Matches(line).Count%2)==0;}/// <summary>/// 引用符を変換する/// </summary>privatestringDequote(Matchmatch){vars=match.Groups[1].Value;varquoted=Regex.Match(s,"^\"(.*)\"$",RegexOptions.Singleline);if(quoted.Success){returnquoted.Groups[1].Value.Replace("\"\"","\"");}else{returns;}}/// <summary>/// 破棄処理/// </summary>publicvoidDispose(){if(_reader!=null){_reader.Dispose();}}#endregion}

ReadByteのメソッドは不要ですが、Csvをポストする際にByte配列にする必要があったので付け加えているだけです.
以上で、必要なクラスの定義は終了です.

利用例

では、実際に使ってみましょう.
Attributeを利用したデータクラスを作成し、SimpleCsvWriterにGenricで渡すだけです.

ProfileTest.cs
usingSystem.IO;usingSystem.Collections.Generic;usingUnityEngine;/// <summary>/// ProfileTest/// </summary>publicclassProfileTest:MonoBehaviour{#regiondefine/// <summary>/// ProfileData/// </summary>publicclassProfileData{[SimpleCsv(1)]publicstringName{get;set;}[SimpleCsv(2,Name="処理時間")]publicintTime{get;set;}}#endregion#regionmethod/// <summary>/// データを書き込む/// </summary>publicvoidWrite(){// 適当なテスト用のデータ作成varlist=newList<ProfileData>(10);for(inti=0;i<10;i++){list.Add(newProfileData(){Name=i.ToString(),Time=(i*i)});}// 書き込み部分vardirectory=Application.persistentDataPath+Path.DirectorySeparatorChar+"Exports";using(varwriter=newSimpleCsvWriter<ProfileData>(directory,"test",list.ToArray())){writer.Write();}}/// <summary>/// データを読み込む/// </summary>publicProfileData[]Read(){ProfileData[]profiles=null;vardirectory=Application.persistentDataPath+Path.DirectorySeparatorChar+"Exports";using(varreader=newSimpleCsvReader<ProfileData>(directory,"test")){profiles=reader.Read();}returnprofiles;}#regionunity_script/// <summary>/// 開始処理/// </summary>privatevoidStart(){// データ書き込みWrite();// データ読み込みvarprofiles=Read();foreach(varprofileinprofiles){Debug.Log(profile.Name+" = "+profile.Time);}}#endregion#endregion}

最後に

Csvの入出力クラスはCsvHelperなどのThird Party製のものや、その他沢山の紹介記事がありますが、
痒いところに手が届かずこのようなクラスを作成することになりました.
この記事が誰かの一助になれば幸いです.

少し早いですが、今年もお疲れ様でした.
来年も頑張っていきましょう!


Viewing all articles
Browse latest Browse all 9743

Trending Articles