内存映射文件读取文件结尾?

时间:2010-12-09 20:18:21

标签: .net file-io stream

我有一个6GB的文件,最后20行是坏的。我想使用带有.NET 4的内存映射文件来读取最后几行并在console.writelines中显示它们,然后转到最后20行并用String.Empty替换它们。使用带有C#示例的内存映射文件/流,这是一种很酷的方法吗?

感谢。

5 个答案:

答案 0 :(得分:3)

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

无论如何,这是一个不使用内存映射文件的C#实现,而是一个常规的FileStream。它基于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();
    }
}

答案 1 :(得分:1)

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

正常打开文件,然后将文件指针移动到文件末尾。一旦你到最后,反向读取文件(每次读取后减少文件指针),直到你得到所需的字符数。

很酷的方式......将字符反向加载到一个数组中,然后一旦完成阅读就不必反转它们。

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

答案 2 :(得分:0)

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

对于第二部分,您希望通过最后20行截断文件(通过将它们设置为string.Empty)。我不确定你是否可以用内存映射来做到这一点。 您可能必须在某处复制文件并用源数据覆盖原始数据,但最后的xxx字节除外(代表最后20行)

下面的代码将提取最后20行并显示它。

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

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

我不确定你最后20行的意思是不好的。如果磁盘物理损坏且数据无法读取,我添加了一个 badPositions 列表,其中包含memorymap在读取数据时遇到问题的位置。

我没有要测试的+ 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();
        }
    }
}

答案 3 :(得分:0)

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

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

但是,关于“反向读取线条”,细节仍然存在。有一些复杂的因素可能会让你陷入困境:

  1. 您无法在StreamReader上搜索,只能在流上搜索。
  2. 文件的最后一行可能会或可能不会以CRLF对终止。
  3. .Net框架的I / O类并没有真正区分CR,LF或CRLF作为行终止符。他们只是对这个惯例进行了抨击。
  4. 根据用于存储文件的编码,向后阅读是非常有问题的。您不知道特定的八位位组/字节代表什么:它可能是多字节编码序列的一部分。性格!=这个现代时代的字节。安全的唯一方法是,如果您知道该文件使用单字节编码,或者如果它是UTF-8,则它不包含代码点大于0x7F的字符。
  5. 我不确定是否有一个好的,简单的解决方案在显而易见的范围之外:按顺序读取文件,不要写最后20行。

答案 4 :(得分:0)

首先,我将用F#编写代码,但是由于我的C#编码生锈,应该可以将其转换为C#代码。

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

要使用memorymappedfile,您需要先将其全部读取到临时的映射文件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计算应该刷新多少缓冲区,源流读取器和目标流写入器。

您需要对文件执行的所有操作都可以对图像进行处理,而无需无意间对文件进行危险的操作。如果确定转换正确无误,则可以将图像内容写回。