【问题标题】:Memory Mapped File to Read End of File?内存映射文件读取文件结尾?
【发布时间】:2010-12-09 20:18:21
【问题描述】:

我有一个 6gb 的文件,最后 20 行是错误的。我想使用 .NET 4 的内存映射文件来读取最后几行并将它们显示在 console.writelines 中,然后转到最后 20 行并用 String.Empty 替换它们。使用带有 C# 示例的内存映射文件/流来做到这一点的好方法是什么?

谢谢。

【问题讨论】:

  • 你知道一种普通的方式,现在正在寻找一种很酷的方式?
  • 我现在不知道有什么办法。我希望默认为“酷”。现在我实际上使用 File 对象和 readline 上的老式流读取文件,直到最后并只显示结尾,我什至不在删除部分。

标签: .net file-io stream


【解决方案1】:

如果您最终映射整个文件,内存映射文件可能会成为大文件(通常是大小等于或大于 RAM 的文件)的问题。如果你只映射结尾,那应该不是一个真正的问题。

无论如何,这是一个不使用内存映射文件,而是使用常规 FileStream 的 C# 实现。它基于ReverseStreamReader 实现(也包括代码)。我很想看看它与其他 MMF 解决方案在性能和内存消耗方面的对比。

public static void OverwriteEndLines(string filePath, int linesToStrip)
{
    if (filePath == null)
        throw new ArgumentNullException("filePath");

    if (linesToStrip <= 0)
        return;

    using (FileStream file = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite))
    {
        using (ReverseStreamReader reader = new ReverseStreamReader(file))
        {
            int count = 0;
            do
            {
                string line = reader.ReadLine();
                if (line == null) // end of file
                    break;

                count++;
                if (count == linesToStrip)
                {
                    // write CR LF
                    for (int i = 0; i < linesToStrip; i++)
                    {
                        file.WriteByte((byte)'\r');
                        file.WriteByte((byte)'\n');
                    }

                    // truncate file to current stream position
                    file.SetLength(file.Position);
                    break;
                }
            }
            while (true);
        }
    }
}

// NOTE: we have not implemented all ReadXXX methods
public class ReverseStreamReader : StreamReader
{
    private bool _returnEmptyLine;

    public ReverseStreamReader(Stream stream)
        : base(stream)
    {
        BaseStream.Seek(0, SeekOrigin.End);
    }

    public override int Read()
    {
        if (BaseStream.Position == 0)
            return -1;

        BaseStream.Seek(-1, SeekOrigin.Current);
        int i = BaseStream.ReadByte();
        BaseStream.Seek(-1, SeekOrigin.Current);
        return i;
    }

    public override string ReadLine()
    {
        if (BaseStream.Position == 0)
        {
            if (_returnEmptyLine)
            {
                _returnEmptyLine = false;
                return string.Empty;
            }
            return null;
        }

        int read;
        StringBuilder sb = new StringBuilder();
        while((read = Read()) >= 0)
        {
            if (read == '\n')
            {
                read = Read();
                // supports windows & unix format
                if ((read > 0) && (read != '\r'))
                {
                    BaseStream.Position++;
                }
                else if (BaseStream.Position == 0)
                {
                   // handle the special empty first line case
                    _returnEmptyLine = true;
                }
                break;
            }
            sb.Append((char)read);
        }

        // reverse string. Note this is optional if we don't really need string content
        if (sb.Length > 1)
        {
            char[] array = new char[sb.Length];
            sb.CopyTo(0, array, 0, array.Length);
            Array.Reverse(array);
            return new string(array);
        }
        return sb.ToString();
    }
}

【讨论】:

    【解决方案2】:

    从问题看来,您需要一个内存映射文件。但是,有一种方法可以在不使用内存映射文件的情况下执行此操作。

    正常打开文件,然后将文件指针移动到文件末尾。结束后,反向读取文件(每次读取后递减文件指针),直到获得所需的字符数。

    很酷的方法...将字符反向加载到数组中,然后在阅读完毕后就不必反转它们。

    对数组进行修复,然后将它们写回。关闭,冲洗,完成!

    【讨论】:

    • 为什么反过来呢?当你这样做时会发生什么?
    • 向后读取文件与向前读取文件没有什么不同。或者,他可以在移动到末尾后返回指针并向前阅读,但这并不那么酷:)
    • 见我下面的帖子:由于编码问题,向后阅读文本是有问题的。
    【解决方案3】:

    解决方案有两个部分。对于第一部分,您需要向后读取内存映射以抓取行,直到您读取所需的行数(在本例中为 20)。

    对于第二部分,您希望将文件截断最后二十行(通过将它们设置为 string.Empty)。我不确定您是否可以使用内存映射来做到这一点。 您可能需要在某处制作文件副本并用源数据覆盖原始数据,除了最后 xxx 个字节(表示最后二十行)

    下面的代码将提取最后二十行并显示出来。

    您还将获得位置(lastBytePos 变量) 最后二十行开始的地方。您可以使用该信息来了解在何处截断文件。

    更新:截断文件调用FileStream.SetLength(lastBytePos)

    我不确定你所说的最后 20 行不好是什么意思。如果磁盘物理损坏并且无法读取数据,我添加了一个 badPositions 列表,其中包含内存映射在读取数据时出现问题的位置。

    我没有要测试的 +2GB 文件,但它应该可以工作(手指交叉)。

    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.IO.MemoryMappedFiles;
    using System.IO;
    
    namespace ConsoleApplication
    {
        class Program
        {
            static void Main(string[] args)
            {
                string filename = "textfile1.txt";
                long fileLen = new FileInfo(filename).Length;
                List<long> badPositions = new List<long>();
                List<byte> currentLine = new List<byte>();
                List<string> lines = new List<string>();
                bool lastReadByteWasLF = false;
                int linesToRead = 20;
                int linesRead = 0;
                long lastBytePos = fileLen;
    
                MemoryMappedFile mapFile = MemoryMappedFile.CreateFromFile(filename, FileMode.Open);
    
                using (mapFile)
                {
                    var view = mapFile.CreateViewAccessor();
    
                    for (long i = fileLen - 1; i >= 0; i--) //iterate backwards
                    {
    
                        try
                        {
                            byte b = view.ReadByte(i);
                            lastBytePos = i;
    
                            switch (b)
                            {
                                case 13: //CR
                                    if (lastReadByteWasLF)
                                    {
                                        {
                                            //A line has been read
                                            var bArray = currentLine.ToArray();
                                            if (bArray.LongLength > 1)
                                            {
                                                //Add line string to lines collection
                                                lines.Insert(0, Encoding.UTF8.GetString(bArray, 1, bArray.Length - 1));
    
                                                //Clear current line list
                                                currentLine.Clear();
    
                                                //Add CRLF to currentLine -- comment this out if you don't want CRLFs in lines
                                                currentLine.Add(13);
                                                currentLine.Add(10);
    
                                                linesRead++;
                                            }
                                        }
                                    }
                                    lastReadByteWasLF = false;
    
                                    break;
                                case 10: //LF
                                    lastReadByteWasLF = true;
                                    currentLine.Insert(0, b);
                                    break;
                                default:
                                    lastReadByteWasLF = false;
                                    currentLine.Insert(0, b);
                                    break;
                            }
    
                            if (linesToRead == linesRead)
                            {
                                break;
                            }
    
    
                        }
                        catch
                        {
                            lastReadByteWasLF = false;
                            currentLine.Insert(0, (byte) '?');
                            badPositions.Insert(0, i);
                        }
                    }
    
                }
    
                if (linesToRead > linesRead)
                {
                    //Read last line
                    {
                        var bArray = currentLine.ToArray();
                        if (bArray.LongLength > 1)
                        {
                            //Add line string to lines collection
                            lines.Insert(0, Encoding.UTF8.GetString(bArray));
                            linesRead++;
                        }
                    }
                }
    
                //Print results
                lines.ForEach( o => Console.WriteLine(o));
                Console.ReadKey();
            }
        }
    }
    

    【讨论】:

      【解决方案4】:

      我对 ReverseStreamReaders 一无所知。解决方案[基本上]很简单:

      • 寻找文件结尾
      • 反向阅读行数。边走边算字符。
      • 当您累积 20 行时,您就完成了:通过减少 20 行中包含的字符数来设置流上的文件长度并关闭文件。

      但是,关于“反向阅读行”的细节是魔鬼。有一些复杂的因素可能会给您带来麻烦:

      1. 您不能在 StreamReader 上搜索,只能在流上搜索。
      2. 文件的最后一行可能会也可能不会以 CRLF 对结束。
      3. .Net 框架的 I/O 类并没有真正区分 CR、LF 或 CRLF 作为行终止符。他们只是在那个约定上下注。
      4. 根据用于存储文件的编码,向后读取非常有问题。您不知道特定的八位字节/字节代表什么:它很可能是多字节编码序列的一部分。字符!= 在这个现代时代的字节。唯一安全的方法是,如果您知道该文件使用单字节编码,或者如果它是 UTF-8,则它不包含代码点大于 0x7F 的字符。

      我不确定除了显而易见的之外还有一个好的简单解决方案:按顺序读取文件,不要写最后二十行。

      【讨论】:

        【解决方案5】:

        首先我会用 F# 编写代码,但是应该可以翻译成 C# 代码,因为我的 C# 编码已经生锈了。

        其次,据我了解,您需要采取一种有效的方式来访问某些文件的内容并对其进行更改,然后将其写回。

        要使用内存映射文件,您需要先将其全部读入临时映射文件 tmp。这只会稍微过热,因为您将在一次阅读中完成所有操作。然后您使用 tmp 更改内容,并在完成后首先将新文件内容写回。这将比使用普通文件流更快,并且您不应该非常担心堆栈/堆溢出。

        open System.IO
        open Sytem.IO.MemoryMappedFiles
        
        // Create a memorymapped image of the file content i.e. copy content
        // return the memorymappedfile
        // use is the same as using in C# 
        let createMappedImage path =
            let mmf = MemorymappedFile.create("tmp", (fileInfo(path)).Length)
            use writer = new StreamWriter(mmf.CreaViewStream())
            writer.write(File.ReadAllText(path))
            mmf // return memorymappedfile to be used
        
        // Some manipulation function to apply to the image
        
        
        // type : char[] -> StreamReader -> unit 
        let fillBuffer (buffer : byte[]) (reader : StreamReader) =
            let mutable entry = 0
            let mutable ret = reader.Read() // return -1 as EOF
            while ret >= 0 && entry < buffer.Length do
               buffer.[entry] <-  ret
               entry <- entry + 1
            entry // return count of byte read
        
         // type : int -> byte[] -> StreamWriter -> unit
         let flushBuffer count (buffer : byte[]) (writer : StreamWriter) =
             let stop = count + 1
             let mutable entry = 0
             while entry < stop do
                writer.Write(buffer.[entry])
                entry <- entry + 1
             // return unit e.i. void
        
         // read then write the buffer one time
         // writeThrough call fillBuffer which return the count of byte read,
         // and input it to the flushBuffer that then write it to the destination.
         let writeThrough buffer source dest =
             flushBuffer (fillBuffer buffer source) buffer dest
             // return unit
        
        
        // write back the altered content of the image without overflow
        let writeBackMappedImage bufsize dest image =
            // buffer for read/write
            let buf = Array.Create bsize (byte 0)// normal page is 4096 byte         
            // delete old content on write
            use writer = new StreamWriter(File.Open(dest,FileMode.Truncate,FileAccess.Write))
            use reader = new StreamReader(image.CreateViewStream())
            while not reader.EndOfStream do
                writeThrough buf reader writer
        
        let image = createMappedImage "some path"
        let alteredImage = alteration image // some undefined function to correct the content.
        writeBackMappedImage image
        image.dispose()
        image.close()
        

        这还没有运行,所以可能会有一些错误,但我认为这个想法很清楚。如上面所说的createMappedImage创建文件的内存映射图像文件。

        fillbuffer 接受一个字节数组和一个流读取器,然后填充它并返回 flushBuffer 计算应该刷新多少缓冲区,一个源流读取器和一个目标流写入器。

        您需要对文件执行的任何操作都可以对图像执行,而不会无意中对文件造成危险。当您确定转换正确时,您可以将图像内容写回。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2012-03-25
          • 1970-01-01
          • 1970-01-01
          • 2020-11-06
          • 2012-02-08
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多