■やりたいこと
別システムから出力されるCSVファイルを取込む機能を作っています。
CSVの取込み自体は問題なく出来ているのですが、例外処理として別フォーマットのCSVファイルや、別の種類のファイルを取込もうとしたとき(固定長データや、項目数は同じだが数値のみの位置に文字列のデータが入っている等)にエラーメッセージが出るようにしたいです。
処理の流れとしては、
①「参照ボタン」をクリック
②取込むファイルを選択する画面が表示される(OpenFileDialog使用)
③「インポートボタン」をクリックする
④②で選択したCSVファイルの中身をListに格納し、DataGridViewに表示。
という風になっています。
この流れの③④の間でファイルのチェックを入れたいのですが、どの方法でチェックするか悩んでいます。
■調べたこと
CSVと同じ形式(データ型や項目数が合うように)のクラスを用意して、CsvHelperを使用してMappingすれば
ある程度はデータ型や項目数が合わなかった際にエラーにできるようなのですが、
今回他のシステムから出力されるCSVファイルには135項目あり、そのうち私の作っているシステムで使用するのは5項目のみです。
取込んでも130項目は無駄になってしまう為、出来れば必要部分のみListへ入れる方向で考えていました。
他に取り込みファイルのフォーマットをチェックする方法はありますか?
または、使わないデータでも全て取込んでチェックした方がよいのでしょうか。
よろしくお願いいたします。
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2019/06/26 08:22
2019/06/26 09:35
回答4件
0
ベストアンサー
何をしたいのかよくわかっていませんが・・・
135 列ある CSV ファイルの中から特定の 5 列のデータのみ取得したい。その際、各列の型を判定してそれを C# のオブジェクトに反映したいということですか?
であれば、JET または ACE プロバイダ + ADO.NET を使って、型は schema.ini というファイルを作って指定するという方向で考えるというのはいかがですか?
【追記】
検証してみましたが、JET または ACE プロバイダ + ADO.NET で特定の列を取得するのは SELECT クエリで取得したい列を指定すれば可能でしたが、schema.ini ファイルに指定される型と CSV データが違ってもエラーは出ませんでした。
なので、質問者さんの目的、
エラーメッセージが出るようにしたいです。
には上記の案ではダメでした。すみません。
以下、お役には立たないと思いますが、ご参考までにどのように検証したかを書いておきます。
まず、CSV ファイルですが、Microsoft のサンプルデータベース Northwind の Products テーブルから SSMS を使ってエクスポートしたものを使います。
Schema.ini は以下の通り。
[Products.csv] ColNameHeader=True Format=CSVDelimited CharacterSet=65001 Col1=ProductID Long Col2=ProductName Text Col3=SupplierID Long Col4=CategoryID Long Col5=QuantityPerUnit Text Col6=UnitPrice Currency Col7=UnitsInStock Short Col8=UnitsOnOrder Short Col9=ReorderLevel Short Col10=Discontinued Bit
Windows Forms アプリのコードは以下の通り。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.Data.OleDb; namespace WindowsFormsApplication1 { public partial class Form12 : Form { private DataGridView dataGridView1; private BindingSource bindingSource1; private string path = @"C:\Users...\WindowsFormsApplication1\"; private string filename = "Products.csv"; public Form12() { InitializeComponent(); this.dataGridView1 = new DataGridView(); this.dataGridView1.Dock = System.Windows.Forms.DockStyle.Fill; this.bindingSource1 = new BindingSource(); this.dataGridView1.DataSource = this.bindingSource1; this.Controls.Add(this.dataGridView1); } private void Form12_Load(object sender, EventArgs e) { // 接続文字列。HDR=Yes で一行目をヘッダーとして扱う string conString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + this.path + ";Extended Properties=\"text;HDR=Yes;FMT=Delimited\""; OleDbConnection con = new OleDbConnection(conString); string commText = "SELECT [ProductID],[ProductName],[UnitPrice],[Discontinued] FROM [" + this.filename + "]"; OleDbDataAdapter da = new OleDbDataAdapter(commText, con); DataTable dt = new DataTable(); da.Fill(dt); this.bindingSource1.DataSource = dt; } } }
結果は以下のようになります。
Schema.ini の型指定を例えば Text を Long にするなど変換できない様に変えれば何らかのエラーが出ると思っていたのですが、出ませんでした。(ただし、DataGridView への表示結果は期待したものにはなりませんが)
【注】ただし C# のオブジェクトとして取得した結果の型は schema.ini で指定した通りになります。上のコード例では DataTable に CSV データを取得していますが、DataTable の型は DataGridView の左から順に int, string, decimal, bool になります。そこはこのコードのメリットと言えるのではないでしょうか。
【追記2】
下の 2019/06/27 12:03 の私のコメントで
hihijiji さんの案 CsvHelper を NuGet でインストールして使うのが正解のようです。CSV ファイルの特定の列だけ取得できますし、指定した型に変換できないと例外をスローしてくれます。後で例を回答欄に追記しておきます。
と書きましたが、上の【追記】と同じ CSV ファイルの同じ列を CsvHelper で取得し、同じ Form の同じ DataGridView に表示した例を以下に書いておきます。
ただし、質問者さんの扱う CSV ファイルにはヘッダがないそうですが、下の例ではヘッダありにしています。ヘッダなしでも対応できるかどうかまでは調べてません。
コードは以下の通りです。上のコードに Product, ProductMapper クラスを追加し、Form12_Load ハンドラでは先の ADO.NET のコードをコメントアウトして CsvHelper のコードを追記しただけです。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.Data.OleDb; using CsvHelper; using CsvHelper.Configuration; using System.IO; namespace WindowsFormsApplication1 { public partial class Form12 : Form { private DataGridView dataGridView1; private BindingSource bindingSource1; private string path = @"C:\Users\surfe...\WindowsFormsApplication1\"; private string filename = "Products.csv"; public Form12() { InitializeComponent(); this.dataGridView1 = new DataGridView(); this.dataGridView1.Dock = System.Windows.Forms.DockStyle.Fill; this.bindingSource1 = new BindingSource(); this.dataGridView1.DataSource = this.bindingSource1; this.Controls.Add(this.dataGridView1); } private void Form12_Load(object sender, EventArgs e) { // 接続文字列。HDR=Yes で一行目をヘッダーとして扱う //string conString = // "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + // this.path + // ";Extended Properties=\"text;HDR=Yes;FMT=Delimited\""; //OleDbConnection con = new OleDbConnection(conString); //string commText = "SELECT [ProductID],[ProductName],[UnitPrice],[Discontinued] FROM [" + this.filename + "]"; //OleDbDataAdapter da = new OleDbDataAdapter(commText, con); //DataTable dt = new DataTable(); //da.Fill(dt); //this.bindingSource1.DataSource = dt; using (var csv = new CsvReader(new StreamReader(this.path + this.filename))) { var config = csv.Configuration; config.HasHeaderRecord = true; // ヘッダーが存在する場合 true config.RegisterClassMap<ProductMapper>(); var list = csv.GetRecords<Product>(); this.bindingSource1.DataSource = list; } } } public class Product { public int ProductID { get; set; } public string ProductName { set; get; } public decimal UnitPrice { set; get; } public bool Discontinued { set; get; } } public class ProductMapper : ClassMap<Product> { private ProductMapper() { Map(c => c.ProductID).Index(0); Map(c => c.ProductName).Index(1); Map(c => c.UnitPrice).Index(5); Map(c => c.Discontinued).Index(9); } } }
結果は上の画像と同じです。
クラス Product で指定した型に変換できないと例外がスローされます。例えば int 型でなければならない ProductID を CSV ファイルで "xxxxx" にすると以下の例外がスローされます。
投稿2019/06/26 02:39
編集2019/06/27 03:22退会済みユーザー
総合スコア0
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2019/06/26 08:25
退会済みユーザー
2019/06/27 03:03
退会済みユーザー
2019/06/27 09:39 編集
2019/06/28 06:45
0
質問の本質がよくわかりませんが、マッピングクラスで Ignore を使うのはどうでしょうか?
追記
質問を読み誤っていたようです。
次のコードの DecodeCsv を使えば CSV を IEnumerable<IEnumerable<string>> に変換できます。これでLINQ を使用して特定のフィールドを読み飛ばし、使用するフィールドを変換するという手もあります。
C#
1using System; 2using System.Collections.Generic; 3using System.Linq; 4using System.Text.RegularExpressions; 5 6namespace Zuishin 7{ 8 /// <summary> 9 /// CSV を解析または CSV に変換する 10 /// </summary> 11 public static class Csv 12 { 13 /// <summary> 14 /// CSV を解析する 15 /// </summary> 16 /// <param name="source">CSV の各行を保持する文字列の集合</param> 17 /// <returns>解析結果</returns> 18 public static IEnumerable<IEnumerable<string>> DecodeCsv(this IEnumerable<string> source) 19 { 20 const string contentName = "content"; 21 Regex quoted = new Regex($"^\"(?<{contentName}>([^\"]|\"\")*)\"(,|$)"); 22 Regex normal = new Regex($"^(?<{contentName}>[^,]*)(,|$)"); 23 var enumerator = source.GetEnumerator(); 24 while (enumerator.MoveNext()) 25 { 26 var record = new List<string>(); 27 string line = enumerator.Current; 28 while (!string.IsNullOrEmpty(line)) 29 { 30 Match match; 31 if (line[0] == '"') 32 { 33 do 34 { 35 match = quoted.Match(line); 36 if (match.Success) 37 { 38 var content = match.Groups[contentName]; 39 record.Add(content.Value.Replace("\"\"", "\"")); 40 line = line.Substring(match.Length); 41 break; 42 } 43 if (!enumerator.MoveNext()) 44 { 45 record.Add(line); 46 line = ""; 47 break; 48 } 49 line += "\n" + enumerator.Current; 50 } while (true); 51 } 52 else 53 { 54 match = normal.Match(line); 55 record.Add(match.Groups[contentName].Value); 56 line = line.Substring(match.Length); 57 } 58 } 59 yield return record; 60 } 61 } 62 63 /// <summary> 64 /// CSV を作る 65 /// </summary> 66 /// <param name="source">元となる集合</param> 67 /// <param name="alwaysQuote">true ならば各フィールドをダブルクォーテーションで囲む</param> 68 /// <returns>CSV</returns> 69 public static IEnumerable<string> EncodeCsv(this IEnumerable<IEnumerable<string>> source, bool alwaysQuote = false) 70 { 71 var regex = new Regex("^\"|[,\n]"); 72 string quote(string s) 73 { 74 return $"\"{s.Replace("\"", "\"\"")}\""; 75 } 76 string quoteIfNeeded(string s) 77 { 78 if (regex.IsMatch(s)) return quote(s); 79 return s; 80 } 81 var translate = alwaysQuote ? (Func<string, string>)(a => quote(a)) : a => quoteIfNeeded(a); 82 foreach (var record in source) 83 { 84 yield return string.Join(",", record.Select(translate)); 85 } 86 } 87 } 88}
投稿2019/06/26 02:32
編集2019/06/26 02:44総合スコア28669
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
他に取り込みファイルのフォーマットをチェックする方法はありますか?
どんな方法を取るにせよ、スキーマの定義をやってやらないとチェックする術がないと思うので、135項目の定義は作る必要はあろうかと思います。
あとはどの程度厳密にチェックするのか、という話なので、例えば文字列項目なら桁数までチェックするのは厳密でいいんですが、そこまでの厳密さが必要が無く、intやDateTimeとして扱える型なのかがわかればいい、という程度なら、ツールでも作ってTryParseして成功したらその型だ、と決め打ってしまい、定義を生成するとか…なんかイマイチな気はしますが、そういう方法はありそうですが。
または、使わないデータでも全て取込んでチェックした方がよいのでしょうか。
それは他人には決められません。
投稿2019/06/26 02:25
編集2019/06/26 02:26総合スコア8947
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。