获取超大文本文件 > 10GB 的最后10行

显示一个非常大的文本文件(这个文件超过10GB)的最后10行的最有效方法是什么。我只是想写一个简单的 C # 应用程序,但我不知道如何有效地做到这一点。

56955 次浏览

我可能只是打开它作为一个二进制流,寻找到结束,然后备份寻找换行符。备份10行(或者11行,取决于最后一行) ,找到您的10行,然后只读到最后并使用编码。获取字符串,将其转换为字符串格式。想分就分。

您应该能够使用 FileStream. Seek ()移动到文件的末尾,然后向后查找 n,直到有足够的行。

尾巴?Tail 是一个 unix 命令,它将显示文件的最后几行。在 Windows2003服务器资源工具包中有一个 Windows 版本。

这就是 unix tail 命令的作用,请参阅 http://en.wikipedia.org/wiki/Tail_(Unix)

互联网上有很多开源的实现,这里就有一个 Win32: WIn32的尾巴的实现

您可以使用 Windows 版本的 尾巴命令,只需将它的输出输入到一个带有 > 符号的文本文件中,或者根据您的需要在屏幕上查看它。

正如其他人所建议的那样,您可以转到文件的末尾并有效地向后读取。然而,这有点棘手-特别是因为如果您有一个可变长度的编码(如 UTF-8) ,您需要巧妙地确保您得到“完整的”字符。

如果使用 FileMode 打开文件。将它追加到文件的末尾。然后您可以查找所需的字节数并读取它们。不管你做什么,它可能不会很快,因为这是一个相当大的文件。

一个有用的方法是 FileInfo.Length,它以字节表示文件的大小。

你的文件结构是什么?您确定最后10行将接近文件的末尾吗?如果您有一个文件,其中有12行文本和10GB 的0,那么查看结尾不会真的那么快。不过话说回来,你可能得看完整个文件。

如果您确定该文件包含许多短字符串,每个短字符串在一个新的行上,寻找到结尾,然后检查回来,直到您数到11行的结尾。然后你可以向前读接下来的10行。

读到文件的末尾,然后向后查找,直到找到10行新行,然后考虑到各种编码,向前读到末尾。确保处理文件中的行数小于10的情况。下面是一个实现(在 C # 中标记了这一点) ,泛化为在 encoding编码的 path文件中找到最后一个 numberOfTokens,其中令牌分隔符由 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);
}
}

我不确定它的效率如何,但是在 Windows PowerShell 中获取文件的最后十行就像

Get-Content file.txt | Select-Object -last 10

我认为其他的海报都表明,没有真正的捷径。

您可以使用 tail (或 powershell)之类的工具,也可以编写一些简单的代码,查找文件的末尾,然后返回查找 n 个换行符。

在 web 上有很多 tail 的实现——看看源代码,看看 他们是如何做到的。Tail 是相当有效的(即使是在非常非常大的文件上) ,所以当他们写它的时候,他们必须得到它的权利!

打开文件并开始读取行。读取10行之后,打开另一个指针,从文件前面开始,因此第二个指针比第一个指针慢10行。继续读取,同时移动两个指针,直到第一个指针到达文件的末尾。然后使用第二个指针读取结果。它可以处理任何大小的文件,包括空的和短于尾长度的文件。而且它很容易调整任何长度的尾巴。 当然,缺点是您最终会读取整个文件,而这可能正是您试图避免的。

为什么不使用返回字符串[]的 file.readalllines 呢?

然后您可以得到最后10行(或数组的成员) ,这将是一个琐碎的任务。

这种方法没有考虑到任何编码问题,我不确定这种方法的确切效率(完成方法所需的时间等)。

我认为下面的代码将通过细微的改变重新编码来解决这个问题

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();

如果您有一个每行格式均匀的文件(比如一个 Nasdaq 系统) ,您只需使用流式阅读器获取文件的长度,然后取其中一行(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);
}

这是我的版本

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');
}

使用 Sisutil 的答案作为起点,您可以逐行读取文件并将它们加载到 Queue<String>中。它确实从一开始就读取文件,但它的优点是不尝试反向读取文件。如果您的文件使用可变字符宽度编码,比如 Jon Skeet 指出的 UTF-8,那么这将非常困难。它也不对行长做任何假设。

我在一个1.7 GB 的文件上测试了一下(手边没有10 GB 的文件) ,花了大约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);

我刚刚遇到了同样的问题,一个应该通过 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 读到结尾。

这实际上并不允许指定结尾的行数,这无论如何都不是一个好主意,因为这些行可能任意长,从而再次扼杀性能。所以我指定了字节的数量,直到我们到达第一个 Newline,然后轻松地读到最后。 理论上来说,你也可以查找 CarriageReturn,但是对我来说,这是没有必要的。

如果我们使用这段代码,它不会干扰编写线程:

        FileStream fileStream = new FileStream(
filename,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite);


StreamReader streamReader = new StreamReader(fileStream);

如果您需要从一个文本文件中反向读取任意数量的行,这里有一个与 LINQ 兼容的类,您可以使用它。它主要关注性能和对大文件的支持。你可以读几行,然后调用 倒车来得到最后几行的前进顺序:

用法 :

var reader = new ReverseTextReader(@"C:\Temp\ReverseTest.txt");
while (!reader.EndOfStream)
Console.WriteLine(reader.ReadLine());

反向文本阅读器类 :

/// <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
}

使用 PowerShell,Get-Content big_file_name.txt -Tail 10,其中10是要检索的底线数。

这不存在性能问题。我在一个超过100GB 的文本文件上运行它,得到了一个即时的结果。

我用这个代码为一个小实用工具前一段时间,我希望它可以帮助你!

private string ReadRows(int offset)     /*offset: how many lines it reads from the end (10 in your case)*/
{
/*no lines to read*/
if (offset == 0)
return result;


using (FileStream fs = new FileStream(FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 2048, true))
{
List<char> charBuilder = new List<char>(); /*StringBuilder doesn't work with Encoding: example char 𐍈 */
StringBuilder sb = new StringBuilder();


int count = 0;


/*tested with utf8 file encoded by notepad-pp; other encoding may not work*/


var decoder = ReaderEncoding.GetDecoder();
byte[] buffer;
int bufferLength;


fs.Seek(0, SeekOrigin.End);


while (true)
{
bufferLength = 1;
buffer = new byte[1];


/*for encoding with variable byte size, every time I read a byte that is part of the character and not an entire character the decoder returns '�' (invalid character) */


char[] chars = { '�' }; //� 65533
int iteration = 0;


while (chars.Contains('�'))
{
/*at every iteration that does not produce character, buffer get bigger, up to 4 byte*/
if (iteration > 0)
{
bufferLength = buffer.Length + 1;


byte[] newBuffer = new byte[bufferLength];


Array.Copy(buffer, newBuffer, bufferLength - 1);


buffer = newBuffer;
}


/*there are no characters with more than 4 bytes in utf-8*/
if (iteration > 4)
throw new Exception();




/*if all is ok, the last seek return IOError with chars = empty*/
try
{
fs.Seek(-(bufferLength), SeekOrigin.Current);
}
catch
{
chars = new char[] { '\0' };
break;
}


fs.Read(buffer, 0, bufferLength);


var charCount = decoder.GetCharCount(buffer, 0, bufferLength);
chars = new char[charCount];


decoder.GetChars(buffer, 0, bufferLength, chars, 0);


++iteration;
}


/*when i get a char*/
charBuilder.InsertRange(0, chars);


if (chars.Length > 0 && chars[0] == '\n')
++count;


/*exit when i get the correctly number of line (*last row is in interval)*/
if (count == offset + 1)
break;


/*the first search goes back, the reading goes on then we come back again, except the last */
try
{
fs.Seek(-(bufferLength), SeekOrigin.Current);
}
catch (Exception)
{
break;
}


}
}


/*everithing must be reversed, but not \0*/
charBuilder.RemoveAt(0);


/*yuppi!*/
return new string(charBuilder.ToArray());
}

我装了一个屏幕来显示速度

enter image description here