【问题标题】:How to read last "n" lines of log file [duplicate]如何读取日志文件的最后“n”行[重复]
【发布时间】:2011-01-06 20:53:03
【问题描述】:

需要一段可以读出日志文件最后“n 行”的代码。我从网上想出了以下代码。我对 C 语言有点陌生。由于日志文件可能是 很大,我想避免读取整个文件的开销。有人可以建议任何性能增强。我真的不想阅读每个字符并改变位置。

   var reader = new StreamReader(filePath, Encoding.ASCII);
            reader.BaseStream.Seek(0, SeekOrigin.End);
            var count = 0;
            while (count <= tailCount)
            {
                if (reader.BaseStream.Position <= 0) break;
                reader.BaseStream.Position--;
                int c = reader.Read();
                if (reader.BaseStream.Position <= 0) break;
                reader.BaseStream.Position--;
                if (c == '\n')
                {
                    ++count;
                }
            }

            var str = reader.ReadToEnd();

【问题讨论】:

  • 你不能像那样使用 StreamReader。
  • 看看stackoverflow.com/questions/1271225/…。然后,您可以在 IEnumerable 上使用 LINQ 扩展 .Last() 来获取最后 N 行
  • @Russ:不,你不能。 LINQ 无法有效地为您提供最后 n 行。
  • @Slaks - 哎呀!我以为最后 N 件物品超载了……这是漫长的一天!现在想想,最后需要回溯一次才能得到 N 个项目。

标签: c# .net performance file-io


【解决方案1】:

您的代码将执行得很差,因为您不允许发生任何缓存。
此外,对于 Unicode,它根本不起作用。

我写了以下实现:

///<summary>Returns the end of a text reader.</summary>
///<param name="reader">The reader to read from.</param>
///<param name="lineCount">The number of lines to return.</param>
///<returns>The last lneCount lines from the reader.</returns>
public static string[] Tail(this TextReader reader, int lineCount) {
    var buffer = new List<string>(lineCount);
    string line;
    for (int i = 0; i < lineCount; i++) {
        line = reader.ReadLine();
        if (line == null) return buffer.ToArray();
        buffer.Add(line);
    }

    int lastLine = lineCount - 1;           //The index of the last line read from the buffer.  Everything > this index was read earlier than everything <= this indes

    while (null != (line = reader.ReadLine())) {
        lastLine++;
        if (lastLine == lineCount) lastLine = 0;
        buffer[lastLine] = line;
    }

    if (lastLine == lineCount - 1) return buffer.ToArray();
    var retVal = new string[lineCount];
    buffer.CopyTo(lastLine + 1, retVal, 0, lineCount - lastLine - 1);
    buffer.CopyTo(0, retVal, lineCount - lastLine - 1, lastLine + 1);
    return retVal;
}

【讨论】:

  • 真的很喜欢移位缓冲区的想法。但这不会有效地读取整个日志文件。有没有一种有效的方法来“寻找”到第 n 行的开头。然后从那里做一个 readLine()。这可能是我的一个愚蠢的疑问!
  • @frictionlesspulley:试试stackoverflow.com/questions/398378/…
【解决方案2】:

您的代码有问题。这是我的版本。由于它是一个日志文件,因此可能正在写入某些内容,因此最好确保您没有锁定它。

你走到最后。开始向后阅读,直到达到 n 行。然后从那里阅读所有内容。

        int n = 5; //or any arbitrary number
        int count = 0;
        string content;
        byte[] buffer = new byte[1];

        using (FileStream fs = new FileStream("text.txt", FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
        {
            // read to the end.
            fs.Seek(0, SeekOrigin.End);

            // read backwards 'n' lines
            while (count < n)
            {
                fs.Seek(-1, SeekOrigin.Current);
                fs.Read(buffer, 0, 1);
                if (buffer[0] == '\n')
                {
                    count++;
                }

                fs.Seek(-1, SeekOrigin.Current); // fs.Read(...) advances the position, so we need to go back again
            }
            fs.Seek(1, SeekOrigin.Current); // go past the last '\n'

            // read the last n lines
            using (StreamReader sr = new StreamReader(fs))
            {
                content = sr.ReadToEnd();
            }
        }

【讨论】:

  • 我喜欢这个解决方案以避免读取整个文件,但想添加检查 fs.Position > 0 应该包括在内以避免寻找超过开始位置
【解决方案3】:

我的一个朋友使用this methodBackwardReader可以找到here):

public static IList<string> GetLogTail(string logname, string numrows)
{
    int lineCnt = 1;
    List<string> lines = new List<string>();
    int maxLines;

    if (!int.TryParse(numrows, out maxLines))
    {
        maxLines = 100;
    }

    string logFile = HttpContext.Current.Server.MapPath("~/" + logname);

    BackwardReader br = new BackwardReader(logFile);
    while (!br.SOF)
    {
        string line = br.Readline();
        lines.Add(line + System.Environment.NewLine);
        if (lineCnt == maxLines) break;
        lineCnt++;
    }
    lines.Reverse();
    return lines;
}

【讨论】:

  • 为什么numrows一个字符串?
  • 与 SLaks 相同的问题,但为 BackwardReader +1。我不知道。
  • 老实说,SLaks,我在好友的博客文章中找不到任何解释原因的内容。我可以看到它本质上是一个从 JavaScript 调用的 WCF 方法,但我不确定这是否足以解释它。
  • BackwardReader 实现很慢(因为它不缓冲)并且不支持 Unicode。
  • BackwardReader 的链接不再可用。
【解决方案4】:

您的日志是否有类似长度的行?如果是,那么您可以计算线的平均长度,然后执行以下操作:

  1. 寻求 end_of_file -lines_needed*avg_line_length (previous_point)
  2. 读完所有内容
  3. 如果你抓住了足够多的行,那很好。如果没有,请寻找previous_point -lines_needed*avg_line_length
  4. 阅读所有内容直到previous_point
  5. 转到 3

内存映射文件也是一个好方法——映射文件尾部、计算行数、映射前一个块、计算行数等,直到得到所需的行数

【讨论】:

  • 对于只需要返回大概行数的情况,这是一个很好的答案。大大减少了循环次数和所用时间。添加了我的实现作为答案。
【解决方案5】:

这是我的答案:-

    private string StatisticsFile = @"c:\yourfilename.txt";

    // Read last lines of a file....
    public IList<string> ReadLastLines(int nFromLine, int nNoLines, out bool bMore)
    {
        // Initialise more
        bMore = false;
        try
        {
            char[] buffer = null;
            //lock (strMessages)  Lock something if you need to....
            {
                if (File.Exists(StatisticsFile))
                {
                    // Open file
                    using (StreamReader sr = new StreamReader(StatisticsFile))
                    {
                        long FileLength = sr.BaseStream.Length;

                        int c, linescount = 0;
                        long pos = FileLength - 1;
                        long PreviousReturn = FileLength;
                        // Process file
                        while (pos >= 0 && linescount < nFromLine + nNoLines) // Until found correct place
                        {
                            // Read a character from the end
                            c = BufferedGetCharBackwards(sr, pos);
                            if (c == Convert.ToInt32('\n'))
                            {
                                // Found return character
                                if (++linescount == nFromLine)
                                    // Found last place
                                    PreviousReturn = pos + 1; // Read to here
                            }
                            // Previous char
                            pos--;
                        }
                        pos++;
                        // Create buffer
                        buffer = new char[PreviousReturn - pos];
                        sr.DiscardBufferedData();
                        // Read all our chars
                        sr.BaseStream.Seek(pos, SeekOrigin.Begin);
                        sr.Read(buffer, (int)0, (int)(PreviousReturn - pos));
                        sr.Close();
                        // Store if more lines available
                        if (pos > 0)
                            // Is there more?
                            bMore = true;
                    }
                    if (buffer != null)
                    {
                        // Get data
                        string strResult = new string(buffer);
                        strResult = strResult.Replace("\r", "");

                        // Store in List
                        List<string> strSort = new List<string>(strResult.Split('\n'));
                        // Reverse order
                        strSort.Reverse();

                        return strSort;
                    }
                }
            }
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine("ReadLastLines Exception:" + ex.ToString());
        }
        // Lets return a list with no entries
        return new List<string>();
    }

    const int CACHE_BUFFER_SIZE = 1024;
    private long ncachestartbuffer = -1;
    private char[] cachebuffer = null;
    // Cache the file....
    private int BufferedGetCharBackwards(StreamReader sr, long iPosFromBegin)
    {
        // Check for error
        if (iPosFromBegin < 0 || iPosFromBegin >= sr.BaseStream.Length)
            return -1;
        // See if we have the character already
        if (ncachestartbuffer >= 0 && ncachestartbuffer <= iPosFromBegin && ncachestartbuffer + cachebuffer.Length > iPosFromBegin)
        {
            return cachebuffer[iPosFromBegin - ncachestartbuffer];
        }
        // Load into cache
        ncachestartbuffer = (int)Math.Max(0, iPosFromBegin - CACHE_BUFFER_SIZE + 1);
        int nLength = (int)Math.Min(CACHE_BUFFER_SIZE, sr.BaseStream.Length - ncachestartbuffer);
        cachebuffer = new char[nLength];
        sr.DiscardBufferedData();
        sr.BaseStream.Seek(ncachestartbuffer, SeekOrigin.Begin);
        sr.Read(cachebuffer, (int)0, (int)nLength);

        return BufferedGetCharBackwards(sr, iPosFromBegin);
    }

注意:-

  1. 调用 ReadLastLines,nLineFrom 从 0 开始表示最后一行,nNoLines 作为要读取的行数。
  2. 它反转列表,因此第一个是文件中的最后一行。
  3. bMore 如果有更多行要读取,则返回 true。
  4. 它将数据缓存在 1024 个字符块中 - 因此速度很快,对于非常大的文件,您可能希望增加此大小。

享受吧!

【讨论】:

    【解决方案6】:

    这绝不是最佳选择,但为了对小日志文件进行快速而肮脏的检查,我一直在使用这样的东西:

    List<string> mostRecentLines = File.ReadLines(filePath)
        // .Where(....)
        // .Distinct()
        .Reverse()
        .Take(10)
        .ToList()
    

    【讨论】:

      【解决方案7】:

      您现在可以在 C# 4.0 中非常轻松地(在早期版本中只需一点点努力)就可以将内存映射文件用于此类操作。它非常适合大文件,因为您可以只映射文件的一部分,然后将其作为虚拟内存进行访问。

      有一个good example here

      【讨论】:

      • 这是个好主意,但据我了解,它不允许按问题要求逐行(文本)读取文件。
      【解决方案8】:

      正如@EugeneMayevski 上面所说,如果您只需要返回大致数量的行,每行的行长大致相同,并且您更关心性能,尤其是对于大文件,这是一个更好的实现:

          internal static StringBuilder ReadApproxLastNLines(string filePath, int approxLinesToRead, int approxLengthPerLine)
          {
              //If each line is more or less of the same length and you don't really care if you get back exactly the last n
              using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
              {
                  var totalCharsToRead = approxLengthPerLine * approxLinesToRead;
                  var buffer = new byte[1];
                   //read approx chars to read backwards from end
                  fs.Seek(totalCharsToRead > fs.Length ? -fs.Length : -totalCharsToRead, SeekOrigin.End);
                  while (buffer[0] != '\n' && fs.Position > 0)                   //find new line char
                  {
                      fs.Read(buffer, 0, 1);
                  }
                  var returnStringBuilder = new StringBuilder();
                  using (StreamReader sr = new StreamReader(fs))
                  {
                      returnStringBuilder.Append(sr.ReadToEnd());
                  }
                  return returnStringBuilder;
              }
          }
      

      【讨论】:

        【解决方案9】:

        大多数日志文件都有日期时间戳。虽然可以改进,但如果您想要最近 N 天的日志消息,下面的代码效果很好。

            /// <summary>
            /// Returns list of entries from the last N days.
            /// </summary>
            /// <param name="N"></param>
            /// <param name="cSEP">field separator, default is TAB</param>
            /// <param name="indexOfDateColumn">default is 0; change if it is not the first item in each line</param>
            /// <param name="bFileHasHeaderRow"> if true, it will not include the header row</param>
            /// <returns></returns>
            public List<string> ReadMessagesFromLastNDays(int N, char cSEP ='\t', int indexOfDateColumn = 0, bool bFileHasHeaderRow = true)
            {
                List<string> listRet = new List<string>();
        
                //--- replace msFileName with the name (incl. path if appropriate)
                string[] lines = File.ReadAllLines(msFileName);
        
                if (lines.Length > 0)
                {
                    DateTime dtm = DateTime.Now.AddDays(-N);
        
                    string sCheckDate = GetTimeStamp(dtm);
                    //--- process lines in reverse
                    int iMin = bFileHasHeaderRow ? 1 : 0;
                    for (int i = lines.Length - 1; i >= iMin; i--)  //skip the header in line 0, if any
                    {
                        if (lines[i].Length > 0)  //skip empty lines
                        {
                            string[] s = lines[i].Split(cSEP);
                            //--- s[indexOfDateColumn] contains the DateTime stamp in the log file
                            if (string.Compare(s[indexOfDateColumn], sCheckDate) >= 0)
                            {
                                //--- insert at top of list or they'd be in reverse chronological order
                                listRet.Insert(0, s[1]);    
                            }
                            else
                            {
                                break; //out of loop
                            }
                        }
                    }
                }
        
                return listRet;
            }
        
            /// <summary>
            /// Returns DateTime Stamp as formatted in the log file
            /// </summary>
            /// <param name="dtm">DateTime value</param>
            /// <returns></returns>
            private string GetTimeStamp(DateTime dtm)
            {
                // adjust format string to match what you use
                return dtm.ToString("u");
            }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-07-26
          • 2017-04-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-06-20
          相关资源
          最近更新 更多