【问题标题】:c# continuously read filec#连续读取文件
【发布时间】:2011-04-17 00:10:33
【问题描述】:

我想像 GNU tail 一样使用“-f”参数连续读取文件。我需要它来实时读取日志文件。 正确的做法是什么?

【问题讨论】:

    标签: c# file-io


    【解决方案1】:

    您可以使用FileSystemWatcher 类,它可以针对文件系统上发生的不同事件(例如文件更改)发送通知。

    【讨论】:

    • 好的。假设我收到文件更改的事件。我怎样才能从上次读完的位置读到最后?任何例子都值得赞赏:)
    【解决方案2】:

    如果您只是在寻找执行此操作的工具,请查看免费版本的Bare tail

    【讨论】:

    • 不,我不是在寻找工具。我需要一种在 C# 中为我的程序实现它的方法。
    【解决方案3】:

    您想以二进制模式打开FileStream。定期查找文件末尾减去 1024 个字节(或其他字节),然后读取到末尾并输出。这就是tail -f 的工作原理。

    回答您的问题:

    二进制,因为如果您将其作为文本读取,则很难随机访问该文件。您必须自己进行二进制到文本的转换,但这并不困难。 (见下文)

    1024 字节,因为它是一个非常方便的数字,应该可以处理 10 或 15 行文本。通常。

    以下是打开文件、读取最后 1024 字节并将其转换为文本的示例:

    static void ReadTail(string filename)
    {
        using (FileStream fs = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
        {
            // Seek 1024 bytes from the end of the file
            fs.Seek(-1024, SeekOrigin.End);
            // read 1024 bytes
            byte[] bytes = new byte[1024];
            fs.Read(bytes, 0, 1024);
            // Convert bytes to string
            string s = Encoding.Default.GetString(bytes);
            // or string s = Encoding.UTF8.GetString(bytes);
            // and output to console
            Console.WriteLine(s);
        }
    }
    

    请注意,您必须使用FileShare.ReadWrite 打开,因为您正在尝试读取当前打开以供其他进程写入的文件。

    另外请注意,我使用了Encoding.Default,它在美国/英语和大多数西欧语言中将是 8 位字符编码。如果文件是以其他编码(如 UTF-8 或其他 Unicode 编码)编写的,则字节可能无法正确转换为字符。如果您认为这将是一个问题,则必须通过确定编码来处理它。在堆栈溢出中搜索有关确定文件文本编码的信息。

    如果您想定期执行此操作(例如每 15 秒),您可以设置一个计时器,该计时器根据需要随时调用 ReadTail 方法。您可以通过在程序开始时仅打开一次文件来稍微优化一下。这取决于你。

    【讨论】:

    • 为什么是 1024?为什么是二进制?例子?
    • 如果自上次检查以来写入的字节数多于或少于 1024 个字节,这不会丢失一些数据或读取一些数据两次吗?
    • 值得注意的是,如果文件小于 1024 字节,代码将抛出 IOException,因为尝试将文件指针移动到文件开始之前的某个点。为避免问题: fs.Seek(Math.Max(-1024,-fs.Length), SeekOrigin.End);
    • Tail 不会盲目地获取最后 1024 个字节 - 它会跟踪最后一个大小和新大小,并且只获取最近添加的字节。 git.savannah.gnu.org/cgit/coreutils.git/tree/src/tail.c
    • @kah608:感谢您提供的信息。修改上面的代码应该很容易做到这一点。
    【解决方案4】:
    private void button1_Click(object sender, EventArgs e)
    {
        if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
        {
            path = folderBrowserDialog.SelectedPath;
            fileSystemWatcher.Path = path;
    
            string[] str = Directory.GetFiles(path);
            string line;
            fs = new FileStream(str[0], FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
            tr = new StreamReader(fs); 
    
            while ((line = tr.ReadLine()) != null)
            {
    
                listBox.Items.Add(line);
            }
    
    
        }
    }
    
    private void fileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
    {
        string line;
        line = tr.ReadLine();
        listBox.Items.Add(line);  
    }
    

    【讨论】:

      【解决方案5】:

      更自然的使用FileSystemWatcher的方法:

          var wh = new AutoResetEvent(false);
          var fsw = new FileSystemWatcher(".");
          fsw.Filter = "file-to-read";
          fsw.EnableRaisingEvents = true;
          fsw.Changed += (s,e) => wh.Set();
      
          var fs = new FileStream("file-to-read", FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
          using (var sr = new StreamReader(fs))
          {
              var s = "";
              while (true)
              {
                  s = sr.ReadLine();
                  if (s != null)
                      Console.WriteLine(s);
                  else
                      wh.WaitOne(1000);
              }
          }
      
          wh.Close();
      

      这里主读取周期停止等待传入数据,FileSystemWatcher 仅用于唤醒主读取周期。

      【讨论】:

      • 你必须在fsw.Changed事件被触发之前做:fsw.EnableRaisingEvents = true;
      • 是的,你是对的,我的错。由于WaitOne 超时,上面的代码才有效。
      • @MadsY 按照您的建议修正了答案。
      • 文件系统观察者的一个警告 - 在引发事件之前似乎需要刷新文件。如果您想实时访问文件更改,则需要定期打开文件,然后将文件位置设置为文件末尾,这将强制刷新任何未决的内存缓冲区,否则您会心血来潮操作系统来决定何时刷新。通常不是问题,除非您想立即进行任何更改。
      • @jixtra 希望我在 3 小时前开始尝试使用 FileSystemWatcher 时阅读了您的评论。我在其他任何地方都没有看到这一点。谢谢
      【解决方案6】:

      要持续监控文件的尾部,只需要记住之前的文件长度即可。

      public static void MonitorTailOfFile(string filePath)
      {
          var initialFileSize = new FileInfo(filePath).Length;
          var lastReadLength = initialFileSize - 1024;
          if (lastReadLength < 0) lastReadLength = 0;
      
          while (true)
          {
              try
              {
                  var fileSize = new FileInfo(filePath).Length;
                  if (fileSize > lastReadLength)
                  {
                      using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                      {
                          fs.Seek(lastReadLength, SeekOrigin.Begin);
                          var buffer = new byte[1024];
      
                          while (true)
                          {
                              var bytesRead = fs.Read(buffer, 0, buffer.Length);
                              lastReadLength += bytesRead;
      
                              if (bytesRead == 0)
                                  break;
      
                              var text = ASCIIEncoding.ASCII.GetString(buffer, 0, bytesRead);
      
                              Console.Write(text);
                          }
                      }
                  }
              }
              catch { }
      
              Thread.Sleep(1000);
          }
      }
      

      我不得不使用 ASCIIEncoding,因为此代码不够智能,无法满足缓冲区边界上 UTF8 的可变字符长度。

      注意:您可以将 Thread.Sleep 部分更改为不同的时间,也可以将其与文件观察器和阻塞模式 - Monitor.Enter/Wait/Pulse 链接。对我来说,计时器就足够了,如果文件没有改变,它最多只能每秒检查一次文件长度。

      【讨论】:

      • 大多数日志文件都会以新行结束,你不需要关心 utf-8 编码,所以记住最后一个新行的位置才是你真正想要的
      【解决方案7】:

      这是我的解决方案

          static IEnumerable<string> TailFrom(string file)
          {
              using (var reader = File.OpenText(file))
              {
                  while (true) 
                  {
                      string line = reader.ReadLine();
                      if (reader.BaseStream.Length < reader.BaseStream.Position) 
                          reader.BaseStream.Seek(0, SeekOrigin.Begin);
      
                      if (line != null) yield return line;
                      else Thread.Sleep(500);
                  }
              }
          }
      

      所以,在你的代码中你可以这样做

          foreach (string line in TailFrom(file)) 
          {
              Console.WriteLine($"line read= {line}");            
          }
      

      【讨论】:

        猜你喜欢
        • 2015-07-21
        • 2020-05-25
        • 1970-01-01
        • 2014-01-29
        • 1970-01-01
        • 1970-01-01
        • 2016-11-16
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多