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


68

显示超大文本文件(此特定文件超过10GB)的最后10行的最有效方法是什么。我当时只是想编写一个简单的C#应用​​程序,但不确定如何有效地执行此操作。


“有效”?你到底什么意思?快速执行?内存占用量小?
孟买

7
上述所有的?:D
DV。

快速执行是重中之重。谢谢!
克里斯·康威

Answers:


80

读到文件末尾,然后向后搜索,直到找到十个换行符,然后再考虑各种编码,再向前读末尾。确保处理文件中的行数少于十的情况。下面是一个实现(在C#中,您已对此进行了标记),该实现被通用化为找到numberOfTokens文件中的最后一个,该文件位于标记分隔符表示为的path编码encoding位置tokenSeparator。结果以a形式返回string(可以通过返回IEnumerable<string>枚举令牌的a来改善)。

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

17
假定字符大小始终相同的编码。使用其他编码可能会比较棘手。
乔恩·斯基特

3
而且,正如Skeet曾经告诉我的那样,不能保证Read方法可以读取请求的字节数。您必须检查返回值以确定您是否已完成阅读...

2
@Jon:可变长度字符编码。真是的
杰森

1
@Will:在几个地方应该在代码中添加错误检查。不过,感谢您让我想起有关Stream.Read的令人讨厌的事实之一。
杰森

3
我已经注意到,在大约4MB的文件上执行此过程非常及时。有任何建议的改进吗?还是其他有关尾文件的C#示例?
GoNeale

23

我可能只是将其作为二进制流打开,寻找到最后,然后备份以寻找换行符。备份10行(或11行,取决于最后一行)以找到10行,然后读到最后并在读取内容上使用Encoding.GetString即可将其转换为字符串格式。根据需要拆分。



17

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


嗯?\r并且\n是UTF-8中的单字节。可能存在问题,但仅适用于奇怪的旧式编码。
CodesInChaos 2013年

3
@CodesInChaos:我并没有说\r\n没有单字节...但其他,所以你需要采取的该帐户字符会占用更多的字节(在U + 0080的任何东西) -如果你试图在文件中的一些任意点,您可能是“中人物”,并且必须对此加以考虑。UTF-8使它变得可行(但并不容易),因为您总是可以知道什么时候是中字符...但是其他编码可能不行。我编写了代码以向后读取文件,这是一件痛苦的事。
乔恩·斯基特


6

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

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

使用PowerShell V5开始,获取内容命令支持-Tail该参数具备的性能问题,这种方法确实。这应该是Get-Content file.txt -Tail 10。另外,您可以指定-Wait参数以在进行更新时将更新输出到文件,类似于tail -f。因此Get-Content file -Tail 10 -Wait将输出文件的最后10行,然后等待并在以后添加新行添加到文件中。
培根咬碎了


4

我认为以下代码将通过细化更改重新编码来解决问题

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

1
进行一些简短的测试,将reader.Read()更改为reader.BaseStream.ReadByte(),同时应检查Position> 0,第二个Position--应检查Position> 0。最后,最后,每个换行符都是“ \ r \ n”,而不仅仅是'\ n',因此将Split('\ n')更改为Replace(“ \ r”,“”).Split('\ n' )。它需要进行一些微调,但是如果您有时间抱怨“不起作用”,请找出问题所在并进行批评。
彼得·拉塞伦扎

2

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


我认为那是埃里克·内斯所说的。但是有时候我真的很喜欢Linux命令-针对命令行中的文本操作进行了优化,不,对不起,终端...
Anthony Horne 2015年

2

这是我的版本。高温超导

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

如果文件少于10行,则代码将崩溃。使用此而用一句话代替while (count < 10 && -pos < sr.BaseStream.Length)
加斯帕

1

如果使用FileMode.Append打开文件,它将为您寻找文件的结尾。然后,您可以查找所需的字节数并读取它们。不管您做什么都可能不会很快,因为那是一个很大的文件。


1

一种有用的方法是 FileInfo.Length。它以字节为单位给出文件的大小。

您的文件是什么结构?您确定最后10行将在文件末尾附近吗?如果您的文件包含12行文本和10GB的0,那么查看末尾并不会真的那么快。再一次,您可能必须浏览整个文件。

如果确定文件中每个换行中包含许多短字符串,请查找到末尾,然后再进行检查,直到算出11行末尾。然后,您可以向前阅读接下来的10行。


1

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

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

Web上有很多实现方法-查看源代码以了解它们是如何实现的。尾巴非常有效(即使在非常大的文件上也是如此),因此他们在编写时必须正确!


1

使用Sisutil的答案作为起点,您可以逐行读取文件并将其加载到 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);    

0

打开文件并开始阅读行。阅读10行之后,打开另一个指针,从文件的开头开始,因此第二个指针比第一个指针落后10行。继续阅读,一致移动两个指针,直到第一个指针到达文件末尾。然后使用第二个指针读取结果。它适用于任何大小的文件,包括空的和短于尾巴的长度。而且很容易调整尾巴的任何长度。当然,缺点是您最终要读取整个文件,而这恰恰是您要避免的事情。


1
如果文件为10GB,我可以肯定地说,这就是他要避免的事情:-)
gbjbaanb

0

如果您的文件每行具有偶数格式(例如daq系统),则只需使用streamreader来获取文件的长度,然后采用其中的一行(readline())。

将总长度除以字符串的长度。现在,您有了一个通用的长数字来表示文件中的行数。

关键是您readline()在获取阵列数据或其他数据之前使用了pre。这将确保您从新行的开头开始,并且不会从上一行中获得任何剩余数据。

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

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读到末尾。

这实际上并不能指定从末尾开始的行数,这也不是一个好主意,因为行数可能会任意长,从而再次导致性能下降。因此,我指定了字节数,读取直到我们到达第一个换行符为止,然后舒适地读取到最后。从理论上讲,您也可以寻找CarriageReturn,但就我而言,这不是必需的。

如果使用此代码,则不会干扰编写器线程:

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

        StreamReader streamReader = new StreamReader(fileStream);

请注意,这是假定'\n'该字符将作为字符的一个字节出现,并且不能以任何其他方式出现。某些编码是可以的,但肯定不是全部。另外,从头开始加载“一定数量的行”(可能为0)可能对您来说很好,但这并不是问题中真正要问的。最后,您可能应该进行调用,streamReader.DiscardBufferedData()以便如果它缓冲了任何内容,则它不会在下一个读取调用中使用该信息,而是查询流。
乔恩·斯基特

感谢您的评论,我要说的是,我现在完全不高兴:我的第一则评论是Jon Skeet hinself :-)
Xan-Kun Clark-Davis

我编辑了答案,希望这样更好。就我而言,答案应通过http传输并显示在浏览器中。因此,我真的不想使用行号,因为很多长行可以快速改变整个情况。通过指定字节数,我始终可以保证答案很快。哦,男孩,这太快了。我将做一些测试(在实际工作之后:-)),因为我真的很好奇。它似乎胜过所有其他解决方案,但这有些牵强。我想知道操作系统实际上在做什么……感谢您度过美好的一天
Xan-Kun Clark-Davis

0

如果您需要从文本文件中反向读取任意数量的行,则可以使用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
}

0

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

这没有性能问题。我在超过100 GB的文本文件上运行它,并获得了即时结果。


0

我前段时间将这个代码用于一个小型实用程序,希望对您有所帮助!

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

我附上了速度屏幕

在此处输入图片说明


-11

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

然后,您将获得最后10行(或数组的成员),这将是一项微不足道的任务。

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


1
在给出答案之前,请先阅读问题!这种方法将花费FAR太多的时间。
rayanisran 2011年

伙计,您在这里留下了不错的足迹!希望您现在是更好的程序员!;-)
AaA
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.