非常に大きなテキストファイルの最後の10行を表示する最も効率的な方法は何ですか(この特定のファイルは10GBを超えています)。単純なC#アプリを作成することを考えていましたが、これを効果的に行う方法がわかりません。
ファイルの最後まで読み取り、10個の改行が見つかるまで逆方向にシークし、さまざまなエンコードを考慮して最後まで前方に読み取ります。ファイルの行数が10未満の場合は必ず処理してください。以下は、トークンセパレーターが表すnumberOfTokens
でエンコードされたpath
にあるファイルの最後のencoding
を見つけるために一般化された実装です(タグ付けしたC#で)。 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など)を使用している場合は、「全体」の文字を取得するように注意する必要があるためです。
FileStream.Seek() を使用してファイルの末尾に移動し、逆方向に作業して、十分な行ができるまで\ nを探します。
どれほど効率的かはわかりませんが、Windows PowerShellでは、ファイルの最後の10行を取得するのは簡単です
Get-Content file.txt | Select-Object -last 10
それが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でファイルを開くと、ファイルの最後を探します。次に、必要なバイト数をシークバックして読み取ることができます。しかし、それは非常に大きなファイルなので、何をするかに関係なく高速ではないかもしれません。
便利な方法の1つはFileInfo.Length
。ファイルのサイズをバイト単位で示します。
ファイルはどのような構造ですか?最後の10行はファイルの終わり近くにありますか? 12行のテキストと10GBの0を含むファイルがある場合、最後を見るのはそれほど速くありません。次に、ファイル全体を調べる必要があります。
ファイルの新しい行にそれぞれ短い文字列が多数含まれていることが確実な場合は、最後までシークし、11行の終わりを数えるまで再度確認します。その後、次の10行を読み進めることができます。
他のポスターはすべて、本当の近道がないことを示していると思います。
Tail(またはpowershell)などのツールを使用するか、ファイルの終わりを探してn個の改行を探すダムコードを書くことができます。
Webにはテールの実装がたくさんあります。ソースコードを見て、theyがどのように実行されるかを確認してください。 Tailは(非常に大きなファイルでも)非常に効率的であるため、作成時には正しく設定されているはずです。
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);
PowerShellを使用して、Get-Content big_file_name.txt -Tail 10
ここで、10は取得する最終行の数です。
これにはパフォーマンスの問題はありません。 100 GBを超えるテキストファイルで実行すると、すぐに結果が得られました。
1行に偶数形式のファイルがある場合(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);
}
テキストファイルから任意の数の行を逆方向に読み取る必要がある場合に使用できるLINQ互換クラスを次に示します。パフォーマンスと大きなファイルのサポートに焦点を当てています。複数の行を読み取ってReverse()を呼び出して、最後の数行を順方向に取得できます。
使用法:
var reader = new ReverseTextReader(@"C:\Temp\ReverseTest.txt");
while (!reader.EndOfStream)
Console.WriteLine(reader.ReadLine());
ReverseTextReaderクラス:
/// <summary>
/// Reads a text file backwards, line-by-line.
/// </summary>
/// <remarks>This class uses file seeking to read a text file of any size in reverse order. This
/// is useful for needs such as reading a log file newest-entries first.</remarks>
public sealed class ReverseTextReader : IEnumerable<string>
{
private const int BufferSize = 16384; // The number of bytes read from the uderlying stream.
private readonly Stream _stream; // Stores the stream feeding data into this reader
private readonly Encoding _encoding; // Stores the encoding used to process the file
private byte[] _leftoverBuffer; // Stores the leftover partial line after processing a buffer
private readonly Queue<string> _lines; // Stores the lines parsed from the buffer
#region Constructors
/// <summary>
/// Creates a reader for the specified file.
/// </summary>
/// <param name="filePath"></param>
public ReverseTextReader(string filePath)
: this(new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read), Encoding.Default)
{ }
/// <summary>
/// Creates a reader using the specified stream.
/// </summary>
/// <param name="stream"></param>
public ReverseTextReader(Stream stream)
: this(stream, Encoding.Default)
{ }
/// <summary>
/// Creates a reader using the specified path and encoding.
/// </summary>
/// <param name="filePath"></param>
/// <param name="encoding"></param>
public ReverseTextReader(string filePath, Encoding encoding)
: this(new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read), encoding)
{ }
/// <summary>
/// Creates a reader using the specified stream and encoding.
/// </summary>
/// <param name="stream"></param>
/// <param name="encoding"></param>
public ReverseTextReader(Stream stream, Encoding encoding)
{
_stream = stream;
_encoding = encoding;
_lines = new Queue<string>(128);
// The stream needs to support seeking for this to work
if(!_stream.CanSeek)
throw new InvalidOperationException("The specified stream needs to support seeking to be read backwards.");
if (!_stream.CanRead)
throw new InvalidOperationException("The specified stream needs to support reading to be read backwards.");
// Set the current position to the end of the file
_stream.Position = _stream.Length;
_leftoverBuffer = new byte[0];
}
#endregion
#region Overrides
/// <summary>
/// Reads the next previous line from the underlying stream.
/// </summary>
/// <returns></returns>
public string ReadLine()
{
// Are there lines left to read? If so, return the next one
if (_lines.Count != 0) return _lines.Dequeue();
// Are we at the beginning of the stream? If so, we're done
if (_stream.Position == 0) return null;
#region Read and Process the Next Chunk
// Remember the current position
var currentPosition = _stream.Position;
var newPosition = currentPosition - BufferSize;
// Are we before the beginning of the stream?
if (newPosition < 0) newPosition = 0;
// Calculate the buffer size to read
var count = (int)(currentPosition - newPosition);
// Set the new position
_stream.Position = newPosition;
// Make a new buffer but append the previous leftovers
var buffer = new byte[count + _leftoverBuffer.Length];
// Read the next buffer
_stream.Read(buffer, 0, count);
// Move the position of the stream back
_stream.Position = newPosition;
// And copy in the leftovers from the last buffer
if (_leftoverBuffer.Length != 0)
Array.Copy(_leftoverBuffer, 0, buffer, count, _leftoverBuffer.Length);
// Look for CrLf delimiters
var end = buffer.Length - 1;
var start = buffer.Length - 2;
// Search backwards for a line feed
while (start >= 0)
{
// Is it a line feed?
if (buffer[start] == 10)
{
// Yes. Extract a line and queue it (but exclude the \r\n)
_lines.Enqueue(_encoding.GetString(buffer, start + 1, end - start - 2));
// And reset the end
end = start;
}
// Move to the previous character
start--;
}
// What's left over is a portion of a line. Save it for later.
_leftoverBuffer = new byte[end + 1];
Array.Copy(buffer, 0, _leftoverBuffer, 0, end + 1);
// Are we at the beginning of the stream?
if (_stream.Position == 0)
// Yes. Add the last line.
_lines.Enqueue(_encoding.GetString(_leftoverBuffer, 0, end - 1));
#endregion
// If we have something in the queue, return it
return _lines.Count == 0 ? null : _lines.Dequeue();
}
#endregion
#region IEnumerator<string> Interface
public IEnumerator<string> GetEnumerator()
{
string line;
// So long as the next line isn't null...
while ((line = ReadLine()) != null)
// Read and return it.
yield return line;
}
IEnumerator IEnumerable.GetEnumerator()
{
throw new NotImplementedException();
}
#endregion
}
同じ問題がありました。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);
ファイルを開き、行の読み取りを開始します。 10行を読み取った後、ファイルの先頭から別のポインターを開くので、2番目のポインターは最初の行よりも10行遅れます。読み取りを続け、2つのポインターを同時に動かして、最初のポインターがファイルの終わりに達するまで続けます。次に、2番目のポインターを使用して結果を読み取ります。空のファイルやテールの長さより短いファイルなど、あらゆるサイズのファイルで機能します。また、テールの長さに合わせて簡単に調整できます。もちろん、欠点は、ファイル全体を読むことになり、それがまさに避けようとしていることです。