質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Windows Forms

Windows Forms(WinForms)はMicrosoft .NET フレームワークに含まれる視覚的なアプリケーションのプログラミングインターフェイス(API)です。WinFormsは管理されているコードの既存のWindowsのAPIをラップすることで元のMicrosoft Windowsのインターフェイスのエレメントにアクセスすることができます。

Q&A

解決済

4回答

6346閲覧

取込むCSVのフォーマットチェック

dev3310

総合スコア24

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Windows Forms

Windows Forms(WinForms)はMicrosoft .NET フレームワークに含まれる視覚的なアプリケーションのプログラミングインターフェイス(API)です。WinFormsは管理されているコードの既存のWindowsのAPIをラップすることで元のMicrosoft Windowsのインターフェイスのエレメントにアクセスすることができます。

0グッド

0クリップ

投稿2019/06/26 02:15

■やりたいこと

別システムから出力されるCSVファイルを取込む機能を作っています。

CSVの取込み自体は問題なく出来ているのですが、例外処理として別フォーマットのCSVファイルや、別の種類のファイルを取込もうとしたとき(固定長データや、項目数は同じだが数値のみの位置に文字列のデータが入っている等)にエラーメッセージが出るようにしたいです。

処理の流れとしては、
①「参照ボタン」をクリック
②取込むファイルを選択する画面が表示される(OpenFileDialog使用)
③「インポートボタン」をクリックする
④②で選択したCSVファイルの中身をListに格納し、DataGridViewに表示。
という風になっています。

この流れの③④の間でファイルのチェックを入れたいのですが、どの方法でチェックするか悩んでいます。

■調べたこと

CSVと同じ形式(データ型や項目数が合うように)のクラスを用意して、CsvHelperを使用してMappingすれば
ある程度はデータ型や項目数が合わなかった際にエラーにできるようなのですが、
今回他のシステムから出力されるCSVファイルには135項目あり、そのうち私の作っているシステムで使用するのは5項目のみです。

取込んでも130項目は無駄になってしまう為、出来れば必要部分のみListへ入れる方向で考えていました。

他に取り込みファイルのフォーマットをチェックする方法はありますか?
または、使わないデータでも全て取込んでチェックした方がよいのでしょうか。
よろしくお願いいたします。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

papinianus

2019/06/26 04:51

4の部分を開示できないのですか?4の最初でヘッダ行からカラムを特定するときに必然的にデータがとれるかとれないか判明するとも思ったので。
dev3310

2019/06/26 08:22

今回のCSVデータにヘッダ行はなく、何番目の劣化だけでデータを特定しています。
papinianus

2019/06/26 09:35

それではデータのどこに着目してフォーマットチェックする設計なのでしょうか?コード的な助言ならともかく、データのどこを見るかは質問者様以外には分かりようがないです。 サンプルデータでもあればまた別ですけど。
guest

回答4

0

CSVファイルには135項目あり、そのうち私の作っているシステムで使用するのは5項目のみです。

その5項目+指標になる項目だけMappingすればいいだけでは?
部分的なマッピングもCsvHelperならできたはずですよ。

投稿2019/06/26 04:06

hihijiji

総合スコア4150

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

dev3310

2019/06/26 08:25

検証までしていただき、ありがとうございます。 結果としてはエラーにすることは出来ないとのことですが、後ほどじっくり読んで、自分の環境でも再現し、勉強したいと思います。 まずは取り急ぎお礼を失礼します。
退会済みユーザー

退会済みユーザー

2019/06/27 03:03

hihijiji さんの案 CsvHelper を NuGet でインストールして使うのが正解のようです。CSV ファイルの特定の列だけ取得できますし、指定した型に変換できないと例外をスローしてくれます。後で例を回答欄に追記しておきます。
退会済みユーザー

退会済みユーザー

2019/06/27 09:39 編集

考え直しましたが、CsvHelper を使うより Microsoft.VisualBasic.FileIO 名前空間の TextFieldParser を使う方が良さそうだと思いました。 行の全列を文字列の配列として取得してきて、必要な列のみパースしてから List<T> か DataTable のオブジェクトに代入して行けば望むことができます。 その方が、サードパーティ製のライブラリを使わなくて済むというメリットがありますし、パースするコードを自分で書く分例外の検出などに自由度が高そうです。
dev3310

2019/06/28 06:45

参考用のコードまで載せていただきありがとうございます。 TextFieldParserを使用し、途中取込データをチェックするロジックを自分で書きたして、必要な列のみListに入れる方法で解決することが出来ました。
guest

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
Zuishin

総合スコア28660

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

他に取り込みファイルのフォーマットをチェックする方法はありますか?

どんな方法を取るにせよ、スキーマの定義をやってやらないとチェックする術がないと思うので、135項目の定義は作る必要はあろうかと思います。

あとはどの程度厳密にチェックするのか、という話なので、例えば文字列項目なら桁数までチェックするのは厳密でいいんですが、そこまでの厳密さが必要が無く、intやDateTimeとして扱える型なのかがわかればいい、という程度なら、ツールでも作ってTryParseして成功したらその型だ、と決め打ってしまい、定義を生成するとか…なんかイマイチな気はしますが、そういう方法はありそうですが。

または、使わないデータでも全て取込んでチェックした方がよいのでしょうか。

それは他人には決められません。

投稿2019/06/26 02:25

編集2019/06/26 02:26
gentaro

総合スコア8949

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問