本記事は、.NET と C# を使ったテキストファイルの読み書きと、ランダムアクセスによるバイナリファイルの読み書きについて解説しています。
ひとくちにファイルの読み書きと言っても、.NET には似たような派生クラスが数多く存在し、また使えるメソッドも多岐に渡ります。
そこで、今回はその中で最もメジャーな Fileクラス、StreamReaderクラス、StreamWriterクラス、FileStreamクラスの4種類を取り上げ、その中でも最もよく使われるメソッドに限定して、簡単なサンプルプログラムと図を交ぜて出来るだけ分かり易く解説ししました。
また、記事の最後には実践を意識したサンプルをいくつかご用意しました。コピペして使えるように関数化していますので、ファイルを扱う必要が生じたときは、是非この記事をご利用下さい。
フォルダやファイルを操作(コピー、削除、移動)するクラスについては、「【実践】C#ファイル/フォルダ操作術。すぐに使えるサンプルコード付き」で詳しく紹介していますので、併せてご活用下さい。
.NETのファイル関連クラスで理解すべきポイント
.NETで用意されているファイル関連のクラス
.NET においては、ファイルを読み書きする代表的なクラスとして、Fileクラス、StreamReaderクラス、StreamWriterクラス、FileStreamクラスが用意されています。
クラス名 | 概要 |
---|---|
File | ファイルやディレクトリの作成、コピー、削除など、ファイルシステムに対する操作を提供するクラスであるが、指定した文字列や配列データをまとめてファイルに書き込んだり、読み込むメソッドが用意されている。 |
StreamReader | テキストファイルからテキストを読み込むためのクラス。 |
StreamWriter | テキストデータをファイルに書き込むためのクラス。 |
FileStream | バイナリデータに対してランダムアクセスを行うためのクラス。 |
これらのクラスは System.IO 名前空間に含まれていますので、プログラムの冒頭で名前空間の参照設定が必要になります。また、テキストファイルを扱う場合は文字コードの指定も必要になってくるので、併せて System.Text 名前空間も参照設定しておきます。
具体的には次の2行をプログラムの冒頭に記述します。
1 2 |
using System.IO; using System.Text; |
File,StreamReader,StreamWriter,RIleStream クラスの継承関係
上図はクラスの継承関係図です。Object は.NET の最上位クラスであり、その直下にFileクラスが存在します。StreamReaderクラス、StreamWriterクラス、FileStreamクラスは、このFileクラスから派生しているのかと思われそうですが、実は別のクラスから派生しています。
ファイルを開く(Open)、閉じる(Close)
StreamReaderクラス、StreamWriterクラス、FileStreamクラスを使う場合は、読み書きする直前にファイルを開き(Open)、処理が終わったら閉じる(Close)必要があります。
それぞれのクラスを new した時点でファイルが開かれますが、閉じる処理は Closeメソッドを呼ぶ必要があります。
1 2 3 4 5 6 7 8 9 |
StreamReader sr = new StreamReader(@"p:\hoge.txt")) // ループ内で1行づつ読み込む while ((line = sr.ReadLine()) != null) //ファイルを開く { //~~~処理~~~ } sr.Close(); // ファイルを閉じる |
ファイルを読み書きしている際に例外が発生したり、プログラムの不具合により Closeされないケースが発生すると、プログラムが終わってもファイルが開かれたままになり、ファイルにアクセスできなくなります。
この様な事態を避けるため、using を使って次の様に記述することが推奨されています。こう書いておけば自動的に必ずCloseメソッドが呼ばれるようになります。
1 2 3 4 5 6 7 8 |
using (StreamReader sr = new StreamReader(@"p:\hoge.txt")) { // ループ内で1行筒読み込む while ((line = sr.ReadLine()) != null) { //~~~処理~~~ } } |
ここで登場する using は名前空間の指定とは全く別の意味で使われるもので、ファイルのクローズだけではなく、データベースやネットワーク通信においても使われる記述方法です。
文字コードの指定方法
読み書きするファイルがテキストファイルの場合、文字コードの指定が必要となります。
多くの場合、ファイル読み込み系メソッドでは第2引数、書き込み系メソッドでは第3引数に文字コードが指定できますが、省略した場合は UTF-8 が選択されたことになります。
任意の文字コードを指定する場合、Encoding クラスを使用しますが、これは System.Text 名前空間に静的メソッドとして用意されていますので、using System.Text の記述が必要です。
1 |
using System.Text; |
Encoding クラスを使う場合、プロパティで指定する方法と GetEncoding メソッドを使う方法の2通りがあり、両者は微妙にサポートする文字コードが異なります。
文字コードの指定方法 | 文字コードの指定方法 |
---|---|
Encodingのプロパティを使う方法 | Encoding.UTF-8 Encoding.UTF-7 Encoding.UTF-32 Encoding.ASCII など |
EncodingのGetEncodingメソッドを使う方法 | Encoding.GetEncodings("utf-8") Encoding.GetEncodings("utf-16") Encoding.GetEncodings("shift-jis") Encoding.GetEncodings("uft-32") など |
1 2 3 4 5 |
// Encoding のプロパティを使った例 ReadAllText(@"d:\hoge",Encoding.UTF-8); // Encoding の GetEncoding メソッドを使った例 ReadAllText(@"d:\hoge",Encoding.GetEncoding("utf-8"); |
尚、GetEncoding メソッドで指定可能な文字コード名については、次のコードで取得することが可能です。
1 2 3 4 5 6 7 |
using System.Text; var encodings = Encoding.GetEncodings(); foreach(var encoding in encodings) { Console.WriteLine($"{encoding.CodePage},{encoding.DisplayName},{encoding.Name}"); } |
1200,Unicode,utf-16
1201,Unicode (Big-Endian),utf-16BE
12000,Unicode (UTF-32),utf-32
12001,Unicode (UTF-32 Big-Endian),utf-32BE
20127,US-ASCII,us-ascii
28591,Western European (ISO),iso-8859-1
65001,Unicode (UTF-8),utf-8
.NET CORE では サポート外のSHIFT-JISを指定できるようにする
.NET Framework では問題なかったのですが、.NET CORE からシフトJISがサポート外となり、 GetEncodingsメソッドで "shift-jis" が指定できなくなりました。
しかし、下記の1行を記述しておくことで、Shift-jisをはじめ数多くの文字コードがサポートされるようになります。
1 |
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); |
GetEncodingsメソッドを呼ぶ前であれば記述する場所はどこでもよく、GetEncodingsの直前でも良いですし、プログラムの先頭やコンストラクタの中に記述しても構いません。
ファイルの読み込み
ファイルを読み込むには、Fileクラス を使う方法と StreamReaderクラスを使う方法の2通りがあります。
Fileクラスは、ファイルを一括で読み込んでメモリ内で処理したい場合に適しており、StreamReaderはファイルから1行づつ読み込みながら処理したい場合に使います。
とはいえ、両者ともよく似た動作のメソッドが用意されているので、ほぼ同じことが出来るようになっています。
Fileクラスを使った読み込む
Fileクラスを使うと、指定したファイルの内容を一括で読み込むことができます。ReadAllTextとReadAllLinesの違いは、読み込んだ結果を1つの文字列として返すか、改行で分割して配列として返すかの違いです。
この2つは全てメモリに読み込もうとするため、メモリが少ないとメモリオーバーフローの例外が発生します。
一方、ReadLines は 一括で読み込むのではなく、IEnumerable<string> 列挙型として返してくれます。従って、foreach文の中で1行づつ取り出して、条件に一致するものだけメモリ内に取り込むといった場合に使用します。
メソッド | 戻り値の型 | 説明 |
---|---|---|
ReadAllText( string path, Encoding encoding ) | string | 指定されたファイルのテキストをすべて読み取り、1つの文字列として返します。 |
ReadAllLines( string path, Encoding encoding ) | string[] | 指定されたファイルを読み込み、改行で分割して配列で返します。 |
ReadLines( string path, Encoding encoding ) | IEnumerable<string> | 指定されたファイルの各行を列挙可能な文字列として返します。foreach ループで1行づつ読み込みたい場合に使います。 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 指定したファイルの内容を全て読み込んで1つの文字列として string str = File.ReadAllText(@"p:\hoge.txt"); // 指定したファイルの内容をUTF-8として全て読み込み、改行で分割して配列で返す string[] lines = File.ReadAllLines(@"p:\hoge.txt",Encoding.UTF8); // 指定したファイルを1行づつ読み込んで返す List<string> result = new List<string>(); foreach (var line in File.ReadLines(@"p:\hoge.txt")) { if(line.Contains("TEL NO")) { result.Add(line); } } |
StreamReaderクラスを使った読み込み
StreamReaderクラスは、ファイルから1行づつ読み込んで処理したい場合に用います。
File クラスの ReadLineメソッド動作は同じですが、StreamReaderクラスを実行した後は、必ず閉じる(Close)処理が必要になります。
メソッド | 戻り値の型 | 説明 |
---|---|---|
ReadLine( string path, Encoding encoding ) | string | 指定されたファイルから1行読み取ります。全ての行を読み終えると null を返します。File.ReadLines() と同じ動作です。 |
ReadToEnd( string path, Encoding encoding ) | string | 指定されたファイルを最後まで読み込み、1つの文字列として返します。File.ReadAllText() と同じ動作です。 |
Close() | なし | ファイルを閉じます。 |
プロパティ | 説明 |
---|---|
EndOfStream | ファイルの終端に達すると true を返します。 |
ReadLineは呼ぶ毎にテキストファイルから1行づつ取り出すメソッドなので、全ての行を取り出す為にループが必要になります。
全ての行を取り出し終わると ReadLineは null を返しますが、同時に EndOfStream プロパティも true となるため、そのどちらかを while ループの終了条件として利用します。
ReadLine()の戻り値を使ったループのサンプル
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
List<string> lines = new List<string>(); string? line = null; using (StreamReader sr = new StreamReader(@"p:\hoge.txt")) { // ループ内で1行づつ読み込む while ((line = sr.ReadLine()) != null) { // 1行づつ読み込んでリストに追加 lines.Add(line); } } |
EndOfStreamを使ったループのサンプル
1 2 3 4 5 6 7 8 9 10 11 12 13 |
List<string> lines = new List<string>(); using (StreamReader sr = new StreamReader(@"p:\hoge.txt")) { // ループ内で1行づつ読み込む while (! sr.EndOfStream) { string line = sr.ReadLine(); // 1行づつ読み込んでリストに追加 lines.Add(line); } } |
一方、ReadToEnd は1度呼ぶだけでファイルの中身を全て読み込んで文字列として返してくれますが、File.ReadAlLTextメソッドの方が1行で書けるので、ReadToEndを使うメリットはほぼ無いと思われます。
1 2 3 4 |
using (StreamReader sr = new StreamReader(@"p:\hoge.txt")) { var data = sr.ReadToEnd(); } |
ファイルの書き込み
ファイルに書き込むには、Fileクラス を使う方法と、 StreamWriterクラスを使う方法の2通りがあります。
Fileクラスは文字列変数や文字列配列に保持した内容を、簡単にファイル書き込みする場合に使用します。一方、StreamWriterクラスは、何らかの処理をした結果を順次ファイルに書き込みたい場合に使用します。
Fileクラスによる書き込み
Fileクラスを使うと、文字列や配列の中身を1行でファイルに書き込むことが可能です。
文字列の中身をファイルに書き込みたい場合は WriteAllText を、リストや配列など列挙型(IEnumerable)のデータを書き込みたい場合は WriteAllLines を使用します。
メソッド | 戻り値の型 | 説明 |
---|---|---|
WriteAllText( string path, string content, Encoding encoding ) | なし | 指定されたファイルに、指定された文字列のコレクションの各要素を各行として書き込みます。ファイルが存在しない場合は新規に作成し、すでに存在する場合は上書きします。 |
WriteAllLines( string path, IEnumerable<string> content, Encoding encoding ) | なし | 指定されたファイルに指定された文字列を書き込みます。ファイルが存在しない場合は新規に作成し、すでに存在する場合は上書きします。 |
1 2 3 4 5 6 7 8 |
// 文字列をファイルに書き込む例 string content = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; File.WriteAllText(@"p:\data.txt",content); // リストの内容をファイルに書き込む例 List<string> contents = new List<string>(){"AAA","BBB","CCC"}; File.WriteAllLines(@"p:\data.txt",contents); |
StreamWriterクラスによる書き込み
StreamWriter クラスは、1行づつファイルに書き込む場合に用います。
WriteメソッドとWriteLineメソッドはともに引数で指定された文字列をファイルに書き込むメソッドですが、Writeメソッドはそのまま書き込むのに対し、WriteLineメソッドは自動的に改行コードを末尾に付加して書き込みます。
メソッド | 戻り値の型 | 説明 |
---|---|---|
Wite( object content ) | string | 引数で指定された値をそのままファイルに書き込みます。 引数は object 以外にも、int ,long ,double,string など様々な型が指定できます。 |
WriteLine( object content ) | string | 引数で指定された値の末尾に改行コードを付加し、ファイルに書き込みます。 引数は object 以外にも、int ,long ,double,string など様々な型が指定できます。 |
Flush() | なし | オペレーティングシステムの都合により書き込みが遅延させられているデータを、強制的にファイルに書き込みます。 |
Close() | なし | ファイルを閉じます。 |
例えば、0~5までの数値を witeメソッドで書き込む場合、次の様になります。
1 2 3 4 5 6 7 |
using (StreamWriter sw = new StreamWriter(@"p:\hoge.txt")) { for (int i = 0; i < 5; i++) { sw.Write(i); } } |
01234
0~5までの数値を witeLineメソッドで書き込むと、次の様になります。
1 2 3 4 5 6 7 |
using (StreamWriter sw = new StreamWriter(@"p:\hoge.txt")) { for (int i = 0; i < 5; i++) { sw.WriteLine(i); } } |
0
1
2
3
4
Writeメソッド、WriteLineメソッドで便利な点は、引数の型を選ばないというところです。
FileクラスのWriteメソッドやWriteLinesメソッドは文字列型しか指定できませんでしたが、StreamWriterにおいて様々な型を直接指定できます。
1 2 3 4 5 6 7 8 9 |
using (StreamWriter sw = new StreamWriter(@"p:\hoge.txt")) { sw.WriteLine(100); sw.WriteLine(3.1235676); sw.WriteLine("ABCDE"); sw.WriteLine(new TimeSpan(60)); sw.WriteLine(DateTime.Now); } |
100
3.1235676
ABCDE
00:00:00.0000060
2024/02/17 19:03:34
ちなみに、File.WriteLines メソッドは文字列配列が指定可能でしたが、StreamWrite.WriteLine はあくまでも1つの値しか指定できません。リストや配列を渡すと期待した通りの結果にはならないのでご注意ください。
1 2 3 4 5 6 7 |
using System.IO; using (StreamWriter sw = new StreamWriter(@"p:\hoge.txt")) { sw.WriteLine(new string[] {"AAA","BBB","CCC"}); sw.WriteLine(new List<int>() { 1, 2, 3, 4, 5 }); } |
System.String[]
System.Collections.Generic.List`1[System.Int32]
ランダムアクセスによるファイルの読み書き
これまでに登場したファイルの読み書きは、ファイルの先頭から末尾にかけて順番に取り出す(=シーケンシャルアクセス)方法でした。ランダムアクセスはファイルに対して位置とサイズを指定して読み書きする方法です。
ランダムアクセスにおけるアクセス位置の指定
読み書きする位置(=アクセス位置)は Seek メソッドを使って指定します。一方、サイズはReadメソッド、書き込むサイズはWriteメソッドの引数で指定します。
Seekメソッドでアクセス位置を指定するには、第一引数(offset)に基準位置からの相対距離、第二引数に基準位置(SeekOrigin.Begin , SeekOrigin.Curent , SeekOrigin.End)を指定します。例えばファイル末尾から数えて100バイトの位置を指定する場合、Seek(100,SeekOrigin.End)と記述します。
基準位置は省略可能で、省略した場合は SeekOrigin.Begin が指定されたと見なされます。
SeekOrigin.Begin | ファイルの先頭を基準にする |
SeekOrigin.Curent | 現在のカレント位置を基準にする |
SeekOrigin.End | ファイルの末尾を基準にする |
注意が必要なのは SeekOrigin.Curent を指定した場合です。ファイルに対してSeekメソッドを実行すると、その位置(基準位置+offset)がカレント位置になります。
しかし、そこでRead メソッドや Writeメソッドを実行すると、カレント位置に読み書きしたサイズが加算されてしまうのです。
上記の例では、一旦 Seekメソッド で①にカレント位置が移動しますが、Readメソッドを実行したことで②がカレント位置となり、Writeメソッドを実行した時点で③にカレント位置が設定されます。
例えば、特定の位置の値に対して加算した結果を元の位置に書き込みたい場合は、読み込み時のカレント位置と書き込み時のカレント位置を合わせる必要があります。
1 2 3 4 |
fs.Seek(5,SeekOrigin.Start); //読み込みたい位置を設定 fs.Read(buff,0,3); fs.Seek(5,SeekOrigin.Start); //読み込んだ時と同じ位置を設定 fs.Write(new byte[]{'#','#','#'}); |
ランダムアクセスのためのファイルの開き方
ランダムアクセスは読み書きを同時に行うことが前提であるため、FileStreamクラスを new するだけで読み書き可能になります。この時、第1引数にファイル名を、第2引数にファイルモードを指定します。
1 2 3 4 5 6 7 |
var buff = new Byte[10]; using (var fs = new FileStream(@"p:\hoge.txt", FileMode.OpenOrCreate)) { fs.Seek(5, SeekOrigin.Begin); fs.Read(buff,0,3); fs.Write(buff,0,3); } |
ファイルモードには次の値が指定できます。
ファイルモード | 説明 |
---|---|
FileMode.Create | ファイルが存在しない場合は作成し、存在する場合は上書きします。 |
FileMode.CreateNew | ファイルが存在しない場合は作成し、存在する場合は例外を発生します。 |
FileMode.Open | ファイルを開きます。ファイルが存在しない場合は例外を発生します。 |
FileMode.OpenOrCreate | ファイルが存在する場合は開き、存在しない場合は作成します。 |
FileMode.Truncate | ファイルを開き、ファイルサイズを 0 にします。 |
FileMode.Append | ファイルを末尾に追加モードで開きます。ファイルが存在しない場合は作成します。 |
ランダムアクセスで用いられるメソッドは次のものがあります。
メソッド | 戻り値の型 | 説明 |
---|---|---|
Seek( long offset, SeekOrigin origin ) | Int64 | origin と offset で指定した位置にアクセス位置を設定します。 戻り値として設定後のアクセス位置を返します。 |
Read( byte[] buffer, int offset, int count ) | なし | 指定されたバッファに、指定されたオフセット位置から指定されたバイト数を読み込みます。 |
Write( byte[] buffer, int offset, int count ) | なし | 指定されたバッファから、指定されたオフセット位置から指定されたバイト数を書き込みます。 |
Flush() | なし | オペレーティングシステムの都合により書き込みが遅延させられているデータを、強制的にファイルに書き込みます。 |
Close() | なし | ファイルを閉じます。 |
ランダムアクセスを使ったファイルの読み込み
ランダムアクセスファイルの指定位置からデータを読み込むにはReadメソッドを使います。
Readメソッドには引数が3つあり、1つ目は読み込んだデータの保存先(下図の例では buff 変数)、2つ目はbuffへの書き込み位置、3つ目は ファイルから読み込むサイズ=buffに書き込むサイズを指定します。
下記はファイルの先頭から3バイトだけ読み込んで、buffの先頭から3バイト分を書き込むサンプルです。
1 2 3 4 5 |
var buff = new Byte[10]; using (var fs = new FileStream(@"p:\hoge.txt", FileMode.OpenOrCreate)) { fs.Read(buff,0,3); } |
下記はSeekと組み合わせたサンプルです。ファイルの先頭から数えて5バイト目の位置から、Read で3バイト読み込んで、 buff の先頭に書き込んでいます。
1 2 3 4 5 6 |
var buff = new Byte[10]; using (var fs = new FileStream(@"p:\hoge.txt", FileMode.OpenOrCreate)) { fs.Seek(5, SeekOrigin.Begin); fs.Read(buff,0,3); } |
ランダムアクセスを使ったファイルの書き込み
メモリ(buff変数)に格納されている内容をランダムアクセスファイルに書き込むには Writeメソッドを使用します。
Writeメソッドには引数が3つあり、1つ目は書き込みたいデータが格納されている変数、2つ目は変数からの読み出し位置、3つ目はファイルに書き込みたいサイズを指定します。
下記はbuff 変数の先頭(0バイト目)から3バイトを取り出し、ファイルの先頭に書き込むサンプルです。
1 2 3 4 5 6 7 8 9 |
var buff = new Byte[10]; //A~Jまでの文字をバイトとしてbuffに格納 Enumerable.Range(0, buff.Length).Select(i => buff[i] = (Byte)"ABCDEFGHIJ"[i]).ToArray(); using (var fs = new FileStream(@"p:\hoge.txt", FileMode.OpenOrCreate)) { fs.Write(buff,0,3); } |
下記はSeekと組み合わせたサンプルです。buff の先頭から3バイト分を取り出し、ファイルの先頭から数えて5バイト目の位置にWrite で書き込んでいます。
1 2 3 4 5 6 7 8 9 |
var buff = new Byte[10]; //A~Jまでの文字をバイトとしてbuffに格納 Enumerable.Range(0, buff.Length).Select(i => buff[i] = (Byte)"ABCDEFGHIJ"[i]).ToArray(); using (var fs = new FileStream(@"p:\hoge.txt", FileMode.OpenOrCreate)) { fs.Seek(5, SeekOrigin.Begin); fs.Write(buff,0,3); } |
巨大なファイルを扱うサンプル
巨大なファイルを取り扱う場合、全ての内容をメモリに読み込むことは出来ません。
「メモリが許す範囲のデータサイズだけ読み込んで処理し、結果をファイルに書き出す」という処理をループで繰り返す必要があります。
この章では、下記の4つのケースについてテンプレートとなるプログラムを紹介します。
- 1つのファイルを加工して別のファイルに出力する
- 1つのファイルを指定行数ごとに分割する
- 複数のファイルを1つのファイルに結合する
- 通常のテキストエディタでは開けない巨大なファイルの一部を表示する。
1つのファイルを加工して別のファイルとして出力する
input_fileで指定されたファイルの中身を func 関数で編集し、output_fileで指定したファイルに書き込むサンプルプログラムです。func を省略すると、単なる文字コード変換プログラムとして動作します。
func の戻り値がそのまま output_file に書き出されるので、 func 内で必要な編集を行なって下さい。
特定の条件に合致する行を省いてoutput_file に書き込みたい場合は、func の戻り値として nullを返せば実現可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
using System.IO; using System.Text; public class FileUtil { /// <summary> /// 指定された入力ファイルからテキストを読み込み、指定された関数を各行に適用してから出力ファイルに書き込む。 /// </summary> /// <param name="input_file">入力ファイルのパス</param> /// <param name="output_file">出力ファイルのパス</param> /// <param name="input_encoding">入力ファイルの文字エンコーディング</param> /// <param name="output_encoding">出力ファイルの文字エンコーディング</param> /// <param name="func">各行に適用される関数</param> public void TransFile(string input_file, string output_file, string input_encoding = "shift_jis", string output_encoding = "utf-8", Func<int, string, string> func = null) { // 文字コードでシフトJISをサポートする Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); // 読み込むファイルをオープン using (StreamReader sr = new StreamReader(input_file, Encoding.GetEncoding(input_encoding))) { // 書き込むファイルをオープン using (StreamWriter sw = new StreamWriter(output_file, false, Encoding.GetEncoding(output_encoding))) { // 1行づつ読み込む for (int i = 0; !sr.EndOfStream; i++) { string line = sr.ReadLine(); // 関数が null でなければ実行 if (func != null) { // 関数の戻り値を取得 line = func(i, line); } // 戻り値が null でなければファイルに書き込む if (line != null) { sw.WriteLine(line); } } } } } } |
このクラスを使ってファイルに含まれるアルファベットを大文字に変換する場合は、次の様に書くことができます。
1 2 3 4 5 6 |
using System.IO; using System.Text; // hoge.txt のアルファベットを小文字から大文字に変換し、 hoge2.txt に書き込む new FileUtil().TransFile(@"p:\hoge.txt",@"p:\hoge2.txt",func:(no,line)=>{ return line.ToUpper(); }); |
1つのファイルを指定行数ごとに分割する
指定した行数でファイルを分割するサンプルプログラムです。分割対象のファイルがCSV形式の場合、ヘッダを指定することで、分割されたファイルにもヘッダを含めることが出来ます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
using System.IO; using System.Text; public class FileUtil { /// <summary> /// 指定されたファイルを指定された行数ごとに分割します。 /// </summary> /// <param name="input_file">入力ファイルのパス</param> /// <param name="output_file">出力ファイルのベースパス</param> /// <param name="line_max">1ファイルあたりの最大行数</param> /// <param name="input_encoding">入力ファイルのエンコーディング</param> /// <param name="output_encoding">出力ファイルのエンコーディング</param> /// <param name="header_count">先頭のヘッダーの行数。デフォルトは0。</param> public void SplitFile(string input_file, string output_file, int line_max, string input_encoding, string output_encoding, int header_count = 0) { // 文字コードでシフトJISをサポートする Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); List<string> headers = new List<string>(); // ヘッダーを格納するリスト int file_no = 0; // 出力ファイルの連番 int line_no = 0; // 行番号 int body_count = 0; // ボディの行数カウンタ StreamWriter sw = new StreamWriter(output_file, false, Encoding.GetEncoding(output_encoding)); using (StreamReader sr = new StreamReader(input_file, Encoding.GetEncoding(input_encoding))) { string line; while ((line = sr.ReadLine()) != null) { // ヘッダをリストに保存する if (line_no < header_count) { headers.Add(line); } else { // ボディの行数が分割数に達すると新しいファイルを作成 if (body_count >= line_max) { sw?.Close(); // 新たな連番付きファイル名を生成する var ext = Path.GetExtension(output_file); var name = output_file.Substring(0, output_file.Length - ext.Length); sw = new StreamWriter($"{name}_{file_no + 1}{ext}", false, Encoding.GetEncoding(output_encoding)); // ヘッダを書き込む foreach (string header in headers) { sw.WriteLine(header); } file_no++; body_count = 0; } // ボディのカウントを1つ増やす body_count++; } // ファイルに書き込む sw?.WriteLine(line); // 行番号を1つ増やす line_no++; } } sw?.Close(); } } |
このクラスを使って、ヘッダを1行持つCSVファイルを30行毎に分割する場合、次の様に記述できます。
1 2 3 4 5 6 |
using System.IO; using System.Text; // ファイルを加工して別ファイルに出力 new FileUtil().SplitFile(@"p:\hoge.txt", @"p:\hoge2.txt", 30, "shift-jis", "utf-8",1); |
複数のファイルを1つのファイルに結合する
input_file で指定されたファイル名末尾が連番のファイルを output_file に結合するサンプルプログラムです。
CSVファイルの結合にも対応していて、2つ目以降のファイルについては、header_count で指定した行数分を読み飛ばすようにしています。
input_file に "hoge.txt" を指定すると、 "hoge*.txt" のワイルドカードに一致するファイルを結合しようとするため、"hogehage.txt" や "hogemage.txt" などファイル名の先頭が一致するものは全て結合されてしまうことにご注意ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
using System.IO; using System.Text; public class FileUtil { /// <summary> /// 指定されたファイル名の連番で、同じ拡張子を持つCSVファイルを抽出し、それらをソートしてマージします。 /// </summary> /// <param name="input_file">ベースとなる入力ファイル名。連番の部分は「_数字.csv」の形式である必要があります。</param> /// <param name="output_file">出力ファイル名</param> /// <param name="input_encoding">入力ファイルのエンコーディング</param> /// <param name="output_encoding">出力ファイルのエンコーディング</param> /// <param name="header_count">CSVファイルのヘッダーの行数。0の場合はヘッダーがないと見なします。</param> public void MergeFiles(string input_file, string output_file, string input_encoding, string output_encoding, int header_count = 0) { // 文字コードでシフトJISをサポートする Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); // ファイル名の拡張子部分を取得 string dir = Path.GetDirectoryName(input_file); string ext = Path.GetExtension(input_file); string name = Path.GetFileNameWithoutExtension(input_file); // 同じ拡張子を持つCSVファイルを抽出し、ソートする string[] mergeFiles = Directory.GetFiles(dir, $"{name}*{ext}").OrderByDescending(f => f).ToArray(); // マージするファイルが存在しない場合は処理を終了 if (mergeFiles.Length == 0) { Console.WriteLine("マージするファイルが見つかりませんでした。"); return; } int file_no = 0; // 出力ファイルを開く using (StreamWriter sw = new StreamWriter(output_file, false, Encoding.GetEncoding(output_encoding))) { // 各ファイルを順番に開いて内容をマージする foreach (string file_name in mergeFiles) { using (StreamReader sr = new StreamReader(file_name, Encoding.GetEncoding(input_encoding))) { int line_num = 0; while (! sr.EndOfStream) { string line = sr.ReadLine(); // 2番目以降のファイルのヘッダはスキップする if (file_no > 0 && line_num < header_count) { line_num++; continue; } // ファイルに書き込む sw.WriteLine(line); line_num++; } } file_no++; } } } } |
このクラスを使って "hoge2*.txt"のワイルドカードに一致するヘッダ付きCSVファイルを結合する場合、次の様に記述できます。
1 2 3 4 5 6 |
using System.IO; using System.Text; // ファイルを加工して別ファイルに出力 new FileUtil().MergeFiles(@"p:\hoge2.csv", @"p:\hoge.csv", "shift-jis", "utf-8",1); |
通常のテキストエディタでは開けない巨大なファイルの一部を表示する。
ランダムアクセスを使って、メモリに読み込めないような巨大なファイルから一部を取り出して表示するサンプルプログラムです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
using System.IO; using System.Text; public class FileUtil { /// <summary> /// 指定された位置から指定されたサイズ分のバイトを読み取り、16進数と文字列の形式で表示します。 /// </summary> /// <param name="file_path">ファイルのパス</param> /// <param name="start_position">読み取りを開始する位置(バイト単位)</param> /// <param name="size">読み取るバイト数</param> /// <param name="chunk_size">バイト列を表示する際のチャンクサイズ。デフォルトは16</param> public void DisplayPartialBinary(string file_path, long start_position, long size, int chunk_size = 16) { using (FileStream fs = new FileStream(file_path, FileMode.Open, FileAccess.Read)) { // ファイルの末尾まで移動して、サイズを取得 long file_size = fs.Length; // 指定された位置がファイルサイズを超えている場合は処理を終了 if (start_position >= file_size) { Console.WriteLine("指定された位置がファイルの範囲を超えています。"); return; } // 指定された位置から読み取るサイズを計算 long remaining_size = Math.Min(size, file_size - start_position); // ファイルの指定された位置に移動 fs.Seek(start_position, SeekOrigin.Begin); // 読み取りと表示 long bytes_read = 0; byte[] buffer = new byte[chunk_size]; while (bytes_read < remaining_size) { int bytesRead = fs.Read(buffer, 0, (int)Math.Min(chunk_size, remaining_size - bytes_read)); DisplayChunk(buffer, start_position + bytes_read, chunk_size); bytes_read += bytesRead; } } } /// <summary> /// バイト列を16進数と文字列の形式で表示します。 /// </summary> /// <param name="chunk">表示するバイト列。</param> /// <param name="start_position">バイト列の開始位置(ファイル内の絶対位置)。</param> /// <param name="chunk_size">16進数部分の表示桁数。</param> public void DisplayChunk(byte[] chunk, long start_position, int chunk_size) { StringBuilder hexBuilder = new StringBuilder(); StringBuilder textBuilder = new StringBuilder(); foreach (byte b in chunk) { hexBuilder.AppendFormat("{0:X2} ", b); textBuilder.Append(b >= 32 && b <= 126 ? (char)b : '.'); } string hexChunk = hexBuilder.ToString().PadRight(chunk_size * 3); string textChunk = textBuilder.ToString(); Console.WriteLine($"{start_position:X8} | {hexChunk} | {textChunk}"); } } |
このクラスを使って "hoge.dll" というバイナリファイルの先頭から256バイトを16進表記で表示する場合、次の様に記述できます。
1 2 3 4 5 6 |
using System.IO; using System.Text; // ファイルを加工して別ファイルに出力 new FileUtil().DisplayPartialBinary(@"p:\hoge.dll",0,256); |
00000000 | 4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00 | MZ…………..
00000010 | B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 | ……..@…….
00000020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | …………….
00000030 | 00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 00 | …………….
00000040 | 0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 54 68 | ……..!..L.!Th
00000050 | 69 73 20 70 72 6F 67 72 61 6D 20 63 61 6E 6E 6F | is program canno
00000060 | 74 20 62 65 20 72 75 6E 20 69 6E 20 44 4F 53 20 | t be run in DOS
00000070 | 6D 6F 64 65 2E 0D 0D 0A 24 00 00 00 00 00 00 00 | mode….$…….
00000080 | 50 45 00 00 4C 01 03 00 5D EE 5F 81 00 00 00 00 | PE..L…]._…..
00000090 | 00 00 00 00 E0 00 22 20 0B 01 30 00 00 F2 01 00 | ……" ..0…..
000000A0 | 00 06 00 00 00 00 00 00 66 10 02 00 00 20 00 00 | ……..f…. ..
000000B0 | 00 20 02 00 00 00 00 10 00 20 00 00 00 02 00 00 | . ……. ……
000000C0 | 04 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 | …………….
000000D0 | 00 60 02 00 00 02 00 00 00 00 00 00 03 00 60 85 | …………….
000000E0 | 00 00 10 00 00 10 00 00 00 00 10 00 00 10 00 00 | …………….
000000F0 | 00 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 | …………….
まとめ
今回は.NET と C#を使って、テキストファイルの読み書きとバイナリファイルのランダムアクセスについて解説しました。
今回紹介したクラスには次の特徴があります。
- FIleクラス
メモリ内の文字列データを一括してファイルに書き込んだり、読み出したりするときに使う。
1行記述するだけでファイルへの読み書きが実現出来る。 - StreamReaderクラス、StreamWriterクラス
ファイルから1行づつ読み込んで処理したり、逆に書き込みたいときに使う。
new した後は必ず Closeメソッドでファイルを閉じる必要がある。 - FileStreamクラス
バイナリファイルをランダムアクセスし、任意の場所を読み書きしたい場合に使う。
new した後は必ず Closeメソッドでファイルを閉じる必要がある。
最後の章では巨大なファイルが扱えるよう、極力メモリを使わない方法を用いてファイルの分割、ファイルの結合を行うサンプルと、通常のテキストファイルでは開けないようなファイルに対して、ランダムアクセスで指定した一部のみを取り出して画面に出力するサンプルプログラムを紹介しました。
サンプルプログラムはコピペして使うことを考慮して関数化してあり、また手を加えやすいようコメントも多めに書きました。皆さんのニーズに合った形で修正のうえ、ご利用いただければと思います。
コメント