非常に大きなテキスト ファイル (この特定のファイルは 10 GB を超えます) の最後の 10 行を表示する最も効率的な方法は何ですか。シンプルな C# アプリを作成することだけを考えていましたが、これを効果的に行う方法がわかりません。
21 に答える
ファイルの最後まで読み、10行が見つかるまで逆方向にシークし、さまざまなエンコーディングを考慮して最後まで読みます。ファイルの行数が10行未満の場合は必ず処理してください。以下は、トークンセパレータがで表されるencodedにnumberOfTokens
あるファイルの最後を見つけるために一般化された実装(これにタグを付けたC#で)です。結果はとして返されます(これは、トークンを列挙するを返すことで改善できます)。path
encoding
tokenSeparator
string
IEnumerable<string>
public static string ReadEndTokens(string path, Int64 numberOfTokens, Encoding encoding, string tokenSeparator) {
int sizeOfChar = encoding.GetByteCount("\n");
byte[] buffer = encoding.GetBytes(tokenSeparator);
using (FileStream fs = new FileStream(path, FileMode.Open)) {
Int64 tokenCount = 0;
Int64 endPosition = fs.Length / sizeOfChar;
for (Int64 position = sizeOfChar; position < endPosition; position += sizeOfChar) {
fs.Seek(-position, SeekOrigin.End);
fs.Read(buffer, 0, buffer.Length);
if (encoding.GetString(buffer) == tokenSeparator) {
tokenCount++;
if (tokenCount == numberOfTokens) {
byte[] returnBuffer = new byte[fs.Length - fs.Position];
fs.Read(returnBuffer, 0, returnBuffer.Length);
return encoding.GetString(returnBuffer);
}
}
}
// handle case where number of tokens in file is less than numberOfTokens
fs.Seek(0, SeekOrigin.Begin);
buffer = new byte[fs.Length];
fs.Read(buffer, 0, buffer.Length);
return encoding.GetString(buffer);
}
}
おそらくバイナリ ストリームとして開き、最後までシークしてから、改行を探してバックアップします。10 (最後の行によっては 11) をバックアップして 10 行を見つけ、最後まで読んで、読み取った内容に対して Encoding.GetString を使用して文字列形式にします。必要に応じて分割します。
しっぽ?Tail は、ファイルの最後の数行を表示する UNIX コマンドです。Windows 2003 Server リソース キットに Windows バージョンがあります。
他の人が示唆しているように、ファイルの最後に移動して、効果的に逆方向に読むことができます。ただし、少し注意が必要です。特に、可変長エンコーディング (UTF-8 など) を使用している場合は、「完全な」文字を確実に取得するように工夫する必要があるためです。
それがどれほど効率的かはわかりませんが、Windows PowerShellでは、ファイルの最後の10行を取得するのは簡単です。
Get-Content file.txt | Select-Object -last 10
FileStream.Seek()を使用してファイルの最後に移動し、十分な行数になるまで \n を探して逆方向に作業できるはずです。
それが unix の tail コマンドです。http://en.wikipedia.org/wiki/Tail_(Unix)を参照
インターネット上にはオープン ソースの実装がたくさんありますが、win32用の実装は次のとおりです。Tail for WIn32
次のコードは、エンコーディングを再評価する微妙な変更で問題を解決すると思います
StreamReader reader = new StreamReader(@"c:\test.txt"); //pick appropriate Encoding
reader.BaseStream.Seek(0, SeekOrigin.End);
int count = 0;
while ((count < 10) && (reader.BaseStream.Position > 0))
{
reader.BaseStream.Position--;
int c = reader.BaseStream.ReadByte();
if (reader.BaseStream.Position > 0)
reader.BaseStream.Position--;
if (c == Convert.ToInt32('\n'))
{
++count;
}
}
string str = reader.ReadToEnd();
string[] arr = str.Replace("\r", "").Split('\n');
reader.Close();
Windows バージョンのtailコマンドを使用して、その出力を > 記号でテキスト ファイルに出力するか、必要に応じて画面に表示することができます。
これが私のバージョンです。HTH
using (StreamReader sr = new StreamReader(path))
{
sr.BaseStream.Seek(0, SeekOrigin.End);
int c;
int count = 0;
long pos = -1;
while(count < 10)
{
sr.BaseStream.Seek(pos, SeekOrigin.End);
c = sr.Read();
sr.DiscardBufferedData();
if(c == Convert.ToInt32('\n'))
++count;
--pos;
}
sr.BaseStream.Seek(pos, SeekOrigin.End);
string str = sr.ReadToEnd();
string[] arr = str.Split('\n');
}
FileMode.Append でファイルを開くと、ファイルの最後までシークします。次に、必要なバイト数をシークして読み取ることができます。それはかなり大規模なファイルであるため、何をするかに関係なく、高速ではないかもしれません。
Sisutil の回答を出発点として使用すると、ファイルを 1 行ずつ読み取り、Queue<String>
. ファイルを最初から読み取りますが、ファイルを逆方向に読み取ろうとしないという利点があります。Jon Skeet が指摘したように、UTF-8 のような可変文字幅エンコーディングのファイルがある場合、これは非常に困難になる可能性があります。また、行の長さについての仮定も行いません。
これを 1.7GB のファイル (手元に 10GB のファイルはありませんでした) に対してテストしたところ、約 14 秒かかりました。もちろん、コンピューター間の読み込み時間と読み取り時間を比較する場合は、通常の警告が適用されます。
int numberOfLines = 10;
string fullFilePath = @"C:\Your\Large\File\BigFile.txt";
var queue = new Queue<string>(numberOfLines);
using (FileStream fs = File.Open(fullFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
using (BufferedStream bs = new BufferedStream(fs)) // May not make much difference.
using (StreamReader sr = new StreamReader(bs)) {
while (!sr.EndOfStream) {
if (queue.Count == numberOfLines) {
queue.Dequeue();
}
queue.Enqueue(sr.ReadLine());
}
}
// The queue now has our set of lines. So print to console, save to another file, etc.
do {
Console.WriteLine(queue.Dequeue());
} while (queue.Count > 0);
他のポスターはすべて、本当の近道がないことを示していると思います.
tail(またはpowershell)などのツールを使用するか、ファイルの終わりを探してからn個の改行を探すダムコードを書くことができます。
Web 上にはテールの実装がたくさんあります。ソース コードを見て、どのように実行されているかを確認してください。Tail は非常に効率的であり (非常に大きなファイルでも)、それを書いたときに正しく理解できたに違いありません!
便利な方法の 1 つにFileInfo.Length
. ファイルのサイズをバイト単位で示します。
あなたのファイルはどのような構造ですか?最後の 10 行がファイルの終わり近くにあると確信していますか? 12 行のテキストと 10 GB の 0 を含むファイルがある場合、最後を見るのはそれほど速くはありません。繰り返しますが、ファイル全体に目を通す必要がある場合があります。
ファイルにそれぞれ新しい行に多数の短い文字列が含まれていることが確実な場合は、最後までシークしてから、行末が 11 回数えられるまで戻って確認してください。その後、次の 10 行を読み進めることができます。
PowerShell を使用Get-Content big_file_name.txt -Tail 10
します。ここで、10 は取得する最終行の数です。
これにはパフォーマンス上の問題はありません。100 GB を超えるテキスト ファイルで実行したところ、すぐに結果が得られました。
ファイルを開き、行の読み取りを開始します。10 行を読み取った後、ファイルの先頭から開始して別のポインターを開くため、2 番目のポインターは最初のポインターより 10 行遅れます。最初のポインターがファイルの最後に到達するまで、2 つのポインターを同時に移動しながら読み取りを続けます。次に、2 番目のポインターを使用して結果を読み取ります。空のファイルやテールの長さより短いファイルなど、あらゆるサイズのファイルで機能します。また、テールの長さに合わせて簡単に調整できます。もちろん、欠点は、ファイル全体を読み取ることになることであり、それはまさに回避しようとしている可能性があります。
私はちょうど同じ問題を抱えていました.RESTインターフェースを介してアクセスする必要がある巨大なログファイルです。もちろん、それを任意のメモリにロードし、http 経由で完全に送信することは解決策ではありませんでした。
Jon が指摘したように、このソリューションには非常に特殊なユースケースがあります。私の場合、エンコーディングが utf-8 (BOM 付き!) であることは確かです (そして確認します)。したがって、UTF のすべての恩恵を享受できます。それは確かに汎用ソリューションではありません。
これが私にとって非常にうまく機能したものです(ストリームを閉じるのを忘れていました-現在修正されています):
private string tail(StreamReader streamReader, long numberOfBytesFromEnd)
{
Stream stream = streamReader.BaseStream;
long length = streamReader.BaseStream.Length;
if (length < numberOfBytesFromEnd)
numberOfBytesFromEnd = length;
stream.Seek(numberOfBytesFromEnd * -1, SeekOrigin.End);
int LF = '\n';
int CR = '\r';
bool found = false;
while (!found) {
int c = stream.ReadByte();
if (c == LF)
found = true;
}
string readToEnd = streamReader.ReadToEnd();
streamReader.Close();
return readToEnd;
}
最初に BaseStream を使用して最後近くのどこかにシークし、適切なストリーム位置を取得したら、通常の StreamReader を使用して最後まで読み取ります。
これは、最後からの行の量を指定することを実際には許可しませんが、行が任意に長くなり、パフォーマンスが再び低下する可能性があるため、とにかく良い考えではありません。そのため、バイト数を指定して、最初の改行に到達するまで読み取り、快適に最後まで読み取ります。理論的には、CarriageReturn も検索できますが、私の場合は必要ありませんでした。
このコードを使用すると、ライター スレッドが妨害されることはありません。
FileStream fileStream = new FileStream(
filename,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite);
StreamReader streamReader = new StreamReader(fileStream);
行ごとに偶数形式のファイル (daq システムなど) がある場合は、streamreader を使用してファイルの長さを取得し、行の 1 つ ( readline()
) を取得します。
全長を紐の長さで割ります。これで、ファイル内の行数を表す一般的な長い数値が得られました。
重要なのはreadline()
、配列などのデータを取得する前に使用することです。これにより、新しい行の先頭から開始し、前の行の残りのデータを取得しないことが保証されます。
StreamReader leader = new StreamReader(GetReadFile);
leader.BaseStream.Position = 0;
StreamReader follower = new StreamReader(GetReadFile);
int count = 0;
string tmper = null;
while (count <= 12)
{
tmper = leader.ReadLine();
count++;
}
long total = follower.BaseStream.Length; // get total length of file
long step = tmper.Length; // get length of 1 line
long size = total / step; // divide to get number of lines
long go = step * (size - 12); // get the bit location
long cut = follower.BaseStream.Seek(go, SeekOrigin.Begin); // Go to that location
follower.BaseStream.Position = go;
string led = null;
string[] lead = null ;
List<string[]> samples = new List<string[]>();
follower.ReadLine();
while (!follower.EndOfStream)
{
led = follower.ReadLine();
lead = Tokenize(led);
samples.Add(lead);
}
string[] を返す file.readalllines を使用しないのはなぜですか?
次に、最後の 10 行 (または配列のメンバー) を取得できますが、これは簡単な作業です。
このアプローチではエンコーディングの問題が考慮されておらず、このアプローチの正確な効率 (メソッドを完了するのにかかる時間など) についてはわかりません。