Cách đọc ngược lại tệp văn bản bằng trình lặp trong C #


87

Tôi cần xử lý một tệp lớn, khoảng 400K dòng và 200 M. Nhưng đôi khi tôi phải xử lý từ dưới lên. Làm cách nào tôi có thể sử dụng trình lặp (trả về lợi nhuận) ở đây? Về cơ bản tôi không thích tải mọi thứ vào bộ nhớ. Tôi biết sử dụng trình lặp trong .NET sẽ hiệu quả hơn.



Một khả năng là đọc một lượng đủ lớn từ cuối và sau đó sử dụng String.LastIndexOf để quay ngược tìm kiếm "\ r \ n".
— user34660

Câu trả lời:


136

Đọc ngược các tệp văn bản thực sự khó khăn trừ khi bạn đang sử dụng mã hóa kích thước cố định (ví dụ: ASCII). Khi bạn có mã hóa kích thước thay đổi (chẳng hạn như UTF-8), bạn sẽ tiếp tục phải kiểm tra xem bạn có đang ở giữa một ký tự hay không khi bạn tìm nạp dữ liệu.

Không có gì được xây dựng trong khuôn khổ và tôi nghi ngờ bạn phải thực hiện mã hóa cứng riêng biệt cho từng mã hóa có độ rộng thay đổi.

CHỈNH SỬA: Điều này đã được thử nghiệm phần nào - nhưng điều đó không có nghĩa là nó vẫn còn một số lỗi nhỏ xung quanh. Nó sử dụng StreamUtil từ MiscUtil, nhưng tôi chỉ bao gồm phương thức cần thiết (mới) từ đó ở phía dưới. Ồ, và nó cần cấu trúc lại - có một phương pháp khá tốn kém, như bạn sẽ thấy:

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace MiscUtil.IO
{
    /// <summary>
    /// Takes an encoding (defaulting to UTF-8) and a function which produces a seekable stream
    /// (or a filename for convenience) and yields lines from the end of the stream backwards.
    /// Only single byte encodings, and UTF-8 and Unicode, are supported. The stream
    /// returned by the function must be seekable.
    /// </summary>
    public sealed class ReverseLineReader : IEnumerable<string>
    {
        /// <summary>
        /// Buffer size to use by default. Classes with internal access can specify
        /// a different buffer size - this is useful for testing.
        /// </summary>
        private const int DefaultBufferSize = 4096;

        /// <summary>
        /// Means of creating a Stream to read from.
        /// </summary>
        private readonly Func<Stream> streamSource;

        /// <summary>
        /// Encoding to use when converting bytes to text
        /// </summary>
        private readonly Encoding encoding;

        /// <summary>
        /// Size of buffer (in bytes) to read each time we read from the
        /// stream. This must be at least as big as the maximum number of
        /// bytes for a single character.
        /// </summary>
        private readonly int bufferSize;

        /// <summary>
        /// Function which, when given a position within a file and a byte, states whether
        /// or not the byte represents the start of a character.
        /// </summary>
        private Func<long,byte,bool> characterStartDetector;

        /// <summary>
        /// Creates a LineReader from a stream source. The delegate is only
        /// called when the enumerator is fetched. UTF-8 is used to decode
        /// the stream into text.
        /// </summary>
        /// <param name="streamSource">Data source</param>
        public ReverseLineReader(Func<Stream> streamSource)
            : this(streamSource, Encoding.UTF8)
        {
        }

        /// <summary>
        /// Creates a LineReader from a filename. The file is only opened
        /// (or even checked for existence) when the enumerator is fetched.
        /// UTF8 is used to decode the file into text.
        /// </summary>
        /// <param name="filename">File to read from</param>
        public ReverseLineReader(string filename)
            : this(filename, Encoding.UTF8)
        {
        }

        /// <summary>
        /// Creates a LineReader from a filename. The file is only opened
        /// (or even checked for existence) when the enumerator is fetched.
        /// </summary>
        /// <param name="filename">File to read from</param>
        /// <param name="encoding">Encoding to use to decode the file into text</param>
        public ReverseLineReader(string filename, Encoding encoding)
            : this(() => File.OpenRead(filename), encoding)
        {
        }

        /// <summary>
        /// Creates a LineReader from a stream source. The delegate is only
        /// called when the enumerator is fetched.
        /// </summary>
        /// <param name="streamSource">Data source</param>
        /// <param name="encoding">Encoding to use to decode the stream into text</param>
        public ReverseLineReader(Func<Stream> streamSource, Encoding encoding)
            : this(streamSource, encoding, DefaultBufferSize)
        {
        }

        internal ReverseLineReader(Func<Stream> streamSource, Encoding encoding, int bufferSize)
        {
            this.streamSource = streamSource;
            this.encoding = encoding;
            this.bufferSize = bufferSize;
            if (encoding.IsSingleByte)
            {
                // For a single byte encoding, every byte is the start (and end) of a character
                characterStartDetector = (pos, data) => true;
            }
            else if (encoding is UnicodeEncoding)
            {
                // For UTF-16, even-numbered positions are the start of a character.
                // TODO: This assumes no surrogate pairs. More work required
                // to handle that.
                characterStartDetector = (pos, data) => (pos & 1) == 0;
            }
            else if (encoding is UTF8Encoding)
            {
                // For UTF-8, bytes with the top bit clear or the second bit set are the start of a character
                // See http://www.cl.cam.ac.uk/~mgk25/unicode.html
                characterStartDetector = (pos, data) => (data & 0x80) == 0 || (data & 0x40) != 0;
            }
            else
            {
                throw new ArgumentException("Only single byte, UTF-8 and Unicode encodings are permitted");
            }
        }

        /// <summary>
        /// Returns the enumerator reading strings backwards. If this method discovers that
        /// the returned stream is either unreadable or unseekable, a NotSupportedException is thrown.
        /// </summary>
        public IEnumerator<string> GetEnumerator()
        {
            Stream stream = streamSource();
            if (!stream.CanSeek)
            {
                stream.Dispose();
                throw new NotSupportedException("Unable to seek within stream");
            }
            if (!stream.CanRead)
            {
                stream.Dispose();
                throw new NotSupportedException("Unable to read within stream");
            }
            return GetEnumeratorImpl(stream);
        }

        private IEnumerator<string> GetEnumeratorImpl(Stream stream)
        {
            try
            {
                long position = stream.Length;

                if (encoding is UnicodeEncoding && (position & 1) != 0)
                {
                    throw new InvalidDataException("UTF-16 encoding provided, but stream has odd length.");
                }

                // Allow up to two bytes for data from the start of the previous
                // read which didn't quite make it as full characters
                byte[] buffer = new byte[bufferSize + 2];
                char[] charBuffer = new char[encoding.GetMaxCharCount(buffer.Length)];
                int leftOverData = 0;
                String previousEnd = null;
                // TextReader doesn't return an empty string if there's line break at the end
                // of the data. Therefore we don't return an empty string if it's our *first*
                // return.
                bool firstYield = true;

                // A line-feed at the start of the previous buffer means we need to swallow
                // the carriage-return at the end of this buffer - hence this needs declaring
                // way up here!
                bool swallowCarriageReturn = false;

                while (position > 0)
                {
                    int bytesToRead = Math.Min(position > int.MaxValue ? bufferSize : (int)position, bufferSize);

                    position -= bytesToRead;
                    stream.Position = position;
                    StreamUtil.ReadExactly(stream, buffer, bytesToRead);
                    // If we haven't read a full buffer, but we had bytes left
                    // over from before, copy them to the end of the buffer
                    if (leftOverData > 0 && bytesToRead != bufferSize)
                    {
                        // Buffer.BlockCopy doesn't document its behaviour with respect
                        // to overlapping data: we *might* just have read 7 bytes instead of
                        // 8, and have two bytes to copy...
                        Array.Copy(buffer, bufferSize, buffer, bytesToRead, leftOverData);
                    }
                    // We've now *effectively* read this much data.
                    bytesToRead += leftOverData;

                    int firstCharPosition = 0;
                    while (!characterStartDetector(position + firstCharPosition, buffer[firstCharPosition]))
                    {
                        firstCharPosition++;
                        // Bad UTF-8 sequences could trigger this. For UTF-8 we should always
                        // see a valid character start in every 3 bytes, and if this is the start of the file
                        // so we've done a short read, we should have the character start
                        // somewhere in the usable buffer.
                        if (firstCharPosition == 3 || firstCharPosition == bytesToRead)
                        {
                            throw new InvalidDataException("Invalid UTF-8 data");
                        }
                    }
                    leftOverData = firstCharPosition;

                    int charsRead = encoding.GetChars(buffer, firstCharPosition, bytesToRead - firstCharPosition, charBuffer, 0);
                    int endExclusive = charsRead;

                    for (int i = charsRead - 1; i >= 0; i--)
                    {
                        char lookingAt = charBuffer[i];
                        if (swallowCarriageReturn)
                        {
                            swallowCarriageReturn = false;
                            if (lookingAt == '\r')
                            {
                                endExclusive--;
                                continue;
                            }
                        }
                        // Anything non-line-breaking, just keep looking backwards
                        if (lookingAt != '\n' && lookingAt != '\r')
                        {
                            continue;
                        }
                        // End of CRLF? Swallow the preceding CR
                        if (lookingAt == '\n')
                        {
                            swallowCarriageReturn = true;
                        }
                        int start = i + 1;
                        string bufferContents = new string(charBuffer, start, endExclusive - start);
                        endExclusive = i;
                        string stringToYield = previousEnd == null ? bufferContents : bufferContents + previousEnd;
                        if (!firstYield || stringToYield.Length != 0)
                        {
                            yield return stringToYield;
                        }
                        firstYield = false;
                        previousEnd = null;
                    }

                    previousEnd = endExclusive == 0 ? null : (new string(charBuffer, 0, endExclusive) + previousEnd);

                    // If we didn't decode the start of the array, put it at the end for next time
                    if (leftOverData != 0)
                    {
                        Buffer.BlockCopy(buffer, 0, buffer, bufferSize, leftOverData);
                    }
                }
                if (leftOverData != 0)
                {
                    // At the start of the final buffer, we had the end of another character.
                    throw new InvalidDataException("Invalid UTF-8 data at start of stream");
                }
                if (firstYield && string.IsNullOrEmpty(previousEnd))
                {
                    yield break;
                }
                yield return previousEnd ?? "";
            }
            finally
            {
                stream.Dispose();
            }
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
}


// StreamUtil.cs:
public static class StreamUtil
{
    public static void ReadExactly(Stream input, byte[] buffer, int bytesToRead)
    {
        int index = 0;
        while (index < bytesToRead)
        {
            int read = input.Read(buffer, index, bytesToRead - index);
            if (read == 0)
            {
                throw new EndOfStreamException
                    (String.Format("End of stream reached with {0} byte{1} left to read.",
                                   bytesToRead - index,
                                   bytesToRead - index == 1 ? "s" : ""));
            }
            index += read;
        }
    }
}

Phản hồi rất hoan nghênh. Điều này thật vui :)


Righto - Tôi định bắt đầu sau khoảng một giờ. Tôi sẽ có thể hỗ trợ mã hóa byte đơn, Encoding.Unicode và Encoding.UTF8. Các mã hóa byte dobule khác sẽ không được hỗ trợ. Tôi đang chờ đợi thử nghiệm là một nỗi đau :(
— Jon Skeet

4
ồ! Tôi biết nó đã hơn ba năm tuổi, nhưng đoạn mã này thật tuyệt! Cảm ơn!! (ps Tôi chỉ thay đổi File.OpenRead (filename) với File.Open (filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite) để cho các iterator đọc các tập tin đã mở
— Stefano

1
@GrimaceofDespair: Thêm "bởi vì sau đó tôi sẽ phải thiết kế để kế thừa, điều này làm tăng thêm chi phí rất đáng kể về cả thời gian thiết kế và tính linh hoạt trong tương lai". Thường thì thậm chí không rõ ràng về cách thức kế thừa có thể được sử dụng một cách hợp lý cho một loại - tốt hơn là nên cấm nó cho đến khi tìm thấy sự rõ ràng đó, IMO.
— Jon Skeet,

3
@rahularyansharma: Trong khi tôi thích chia các vấn đề thành các khía cạnh trực giao. Khi bạn đã tìm ra cách mở tệp trong trường hợp của mình, tôi hy vọng mã của mình sẽ phù hợp với bạn.
— Jon Skeet

1
Nếu ai đó muốn có thể chia sẻ tệp với một quá trình khác, chẳng hạn như khi bạn muốn đọc tệp nhật ký được mở để ghi bởi cha mẹ, chỉ cần thay thế: File.OpenRead (tên tệp) Bằng: new FileStream (tên tệp, Mã tệp. Mở, FileAccess.ReadWrite, FileShare.ReadWrite)
— gamut

9

Chú ý: cách tiếp cận này không hoạt động (giải thích trong EDIT)

Bạn có thể sử dụng File.ReadLines để lấy trình lặp dòng

foreach (var line in File.ReadLines(@"C:\temp\ReverseRead.txt").Reverse())
{
    if (noNeedToReadFurther)
        break;

    // process line here
    Console.WriteLine(line);
}

BIÊN TẬP:

Sau khi đọc bình luận của applejacks01 , tôi chạy một số thử nghiệm và nó trông giống như .Reverse() thực sự tải toàn bộ tệp.

Tôi đã từng File.ReadLines()in dòng đầu tiên của tệp 40MB - mức sử dụng bộ nhớ của ứng dụng console là 5MB . Sau đó, được sử dụng File.ReadLines().Reverse()để in dòng cuối cùng của cùng một tệp - bộ nhớ sử dụng là 95MB .

Phần kết luận

Bất cứ điều gì `` Reverse () 'đang làm, nó không phải là một lựa chọn tốt để đọc phần cuối của một tệp lớn.


3
Tôi tự hỏi liệu lệnh gọi Reverse DOES có thực sự tải toàn bộ tệp vào bộ nhớ hay không. Điểm kết thúc của Enumerable có cần phải được thiết lập trước không? Tức là trong nội bộ, bảng liệt kê đầy đủ tệp để tạo một mảng tạm thời, mảng này sau đó được Đảo ngược, sau đó được liệt kê từng cái một bằng cách sử dụng từ khóa lợi nhuận để một Enumerable mới được tạo lặp lại theo thứ tự ngược lại
— applejacks01

2
Câu trả lời ban đầu là sai, nhưng tôi giữ câu trả lời ĐÃ CHỈNH SỬA ở đây vì nó có thể ngăn người khác sử dụng phương pháp này.
— Roman Gudkov

3

Để tạo một trình lặp tệp, bạn có thể làm như sau:

BIÊN TẬP:

Đây là phiên bản cố định của tôi về trình đọc tệp đảo ngược chiều rộng cố định:

public static IEnumerable<string> readFile()
{
    using (FileStream reader = new FileStream(@"c:\test.txt",FileMode.Open,FileAccess.Read))
    {
        int i=0;
        StringBuilder lineBuffer = new StringBuilder();
        int byteRead;
        while (-i < reader.Length)
        {
            reader.Seek(--i, SeekOrigin.End);
            byteRead = reader.ReadByte();
            if (byteRead == 10 && lineBuffer.Length > 0)
            {
                yield return Reverse(lineBuffer.ToString());
                lineBuffer.Remove(0, lineBuffer.Length);
            }
            lineBuffer.Append((char)byteRead);
        }
        yield return Reverse(lineBuffer.ToString());
        reader.Close();
    }
}

public static string Reverse(string str)
{
    char[] arr = new char[str.Length];
    for (int i = 0; i < str.Length; i++)
        arr[i] = str[str.Length - 1 - i];
    return new string(arr);
}

Điều đó hiện gần đúng với ISO-8859-1, nhưng không đúng với bất kỳ mã hóa nào khác. Mã hóa làm :( điều này thực sự khó khăn
— Jon Skeet

Ý bạn là gì khi nói "gần đúng với ISO-8859-1"? Những gì vẫn còn thiếu?
— Igor Zelaya

Việc xử lý không hoàn toàn đúng khi so khớp "\ r" "\ n" và "\ r \ n" trong đó phần sau chỉ được tính là một ngắt dòng.
— Jon Skeet

1
Nó cũng không bao giờ mang lại dòng trống - "\ n \ nb" nên năng suất "a", "", "b"
— Jon Skeet

mmmmmm ... Tôi chỉ cung cấp bộ đệm dòng khi tôi tìm thấy '\ n' (ASCII 10). Bạn nói đúng, tôi không quan tâm đến tài khoản '\ r'.
— Igor Zelaya

2

Tôi đặt tệp vào danh sách từng dòng, sau đó sử dụng List.Reverse ();

        StreamReader objReader = new StreamReader(filename);
        string sLine = "";
        ArrayList arrText = new ArrayList();

        while (sLine != null)
        {
            sLine = objReader.ReadLine();
            if (sLine != null)
                arrText.Add(sLine);
        }
        objReader.Close();


        arrText.Reverse();

        foreach (string sOutput in arrText)
        {

...


4
Không phải là giải pháp tốt nhất cho các tệp lớn vì bạn cần tải nó hoàn toàn vào RAM. Và OP đã chỉ định rõ ràng rằng anh ta không muốn tải nó hoàn toàn.
— CodesInChaos

1

Bạn có thể đọc ngược tệp từng ký tự một và lưu vào bộ nhớ cache tất cả các ký tự cho đến khi bạn đạt đến ký tự xuống dòng và / hoặc nguồn cấp dữ liệu dòng.

Sau đó, bạn đảo ngược chuỗi đã thu thập và kéo nó thành một dòng.


4
Tuy nhiên, đọc ngược một tệp từng ký tự một là điều khó - bởi vì bạn phải có khả năng nhận ra phần bắt đầu của một ký tự. Điều đó đơn giản như thế nào sẽ phụ thuộc vào bảng mã.
— Jon Skeet

1

Tôi biết bài đăng này rất cũ nhưng vì tôi không thể tìm thấy cách sử dụng giải pháp được bình chọn nhiều nhất, cuối cùng tôi đã tìm thấy điều này: đây là câu trả lời tốt nhất mà tôi tìm thấy với chi phí bộ nhớ thấp trong VB và C #

http://www.blakepell.com/2010-11-29-backward-file-reader-vb-csharp-source

Hy vọng, tôi sẽ giúp những người khác về điều đó vì cuối cùng tôi đã mất hàng giờ để tìm thấy bài đăng này!

[Biên tập]

Đây là mã c #:

//*********************************************************************************************************************************
//
//             Class:  BackwardReader
//      Initial Date:  11/29/2010
//     Last Modified:  11/29/2010
//     Programmer(s):  Original C# Source - the_real_herminator
//                     http://social.msdn.microsoft.com/forums/en-US/csharpgeneral/thread/9acdde1a-03cd-4018-9f87-6e201d8f5d09
//                     VB Converstion - Blake Pell
//
//*********************************************************************************************************************************

using System.Text;
using System.IO;
public class BackwardReader
{
    private string path;
    private FileStream fs = null;
    public BackwardReader(string path)
    {
        this.path = path;
        fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
        fs.Seek(0, SeekOrigin.End);
    }
    public string Readline()
    {
        byte[] line;
        byte[] text = new byte[1];
        long position = 0;
        int count;
        fs.Seek(0, SeekOrigin.Current);
        position = fs.Position;
        //do we have trailing rn?
        if (fs.Length > 1)
        {
            byte[] vagnretur = new byte[2];
            fs.Seek(-2, SeekOrigin.Current);
            fs.Read(vagnretur, 0, 2);
            if (ASCIIEncoding.ASCII.GetString(vagnretur).Equals("rn"))
            {
                //move it back
                fs.Seek(-2, SeekOrigin.Current);
                position = fs.Position;
            }
        }
        while (fs.Position > 0)
        {
            text.Initialize();
            //read one char
            fs.Read(text, 0, 1);
            string asciiText = ASCIIEncoding.ASCII.GetString(text);
            //moveback to the charachter before
            fs.Seek(-2, SeekOrigin.Current);
            if (asciiText.Equals("n"))
            {
                fs.Read(text, 0, 1);
                asciiText = ASCIIEncoding.ASCII.GetString(text);
                if (asciiText.Equals("r"))
                {
                    fs.Seek(1, SeekOrigin.Current);
                    break;
                }
            }
        }
        count = int.Parse((position - fs.Position).ToString());
        line = new byte[count];
        fs.Read(line, 0, count);
        fs.Seek(-count, SeekOrigin.Current);
        return ASCIIEncoding.ASCII.GetString(line);
    }
    public bool SOF
    {
        get
        {
            return fs.Position == 0;
        }
    }
    public void Close()
    {
        fs.Close();
    }
}

Bạn nên bao gồm các phần có liên quan từ liên kết trong câu trả lời của mình và thêm liên kết chỉ để tham khảo, để câu trả lời của bạn vẫn thêm giá trị ngay cả khi liên kết thay đổi.
— Thomas Flinkow

Nếu bạn có các IDisposabletrường riêng tư , bạn cũng nên triển khai IDisposablevà xử lý đúng cách các trường này.
— thomasb

1

Giải pháp rất nhanh cho các tệp lớn . Sử dụng lệnh ghép ngắn PowerShell Get-Content với tùy chọn Tail. Gọi powershell sẽ mang lại một chút chi phí, nhưng đối với các tệp lớn thì nó vô giá trị

using System.Management.Automation;

const string FILE_PATH = @"d:\temp\b_media_27_34_0000_25393.txt";
var ps = PowerShell.Create();
ps.AddCommand("Get-Content")
    .AddParameter("Path", FILE_PATH)
    .AddParameter("Tail", 1);
var psResults = ps.Invoke();
var lastLine = psResults.FirstOrDefault()?.BaseObject.ToString();

ps.Dispose();

Tham chiếu quyền hạn bắt buộc

C: \ Program Files (x86) \ Reference Assemblies \ Microsoft \ WindowsPowerShell \ 3.0 \ System.Management.Automation.dll


1

Tôi cũng thêm giải pháp của tôi. Sau khi đọc một số câu trả lời, không có gì thực sự phù hợp với trường hợp của tôi. Tôi đang đọc từng byte từ phía sau cho đến khi tôi tìm thấy một LineFeed, sau đó tôi đang kiểm tra lại các byte đã thu thập dưới dạng chuỗi, mà không sử dụng bộ đệm .

Sử dụng:

var reader = new ReverseTextReader(path);
while (!reader.EndOfStream)
{
    Console.WriteLine(reader.ReadLine());  
}

Thực hiện:

public class ReverseTextReader
{
    private const int LineFeedLf = 10;
    private const int LineFeedCr = 13;
    private readonly Stream _stream;
    private readonly Encoding _encoding;

    public bool EndOfStream => _stream.Position == 0;

    public ReverseTextReader(Stream stream, Encoding encoding)
    {
        _stream = stream;
        _encoding = encoding;
        _stream.Position = _stream.Length;
    }

    public string ReadLine()
    {
        if (_stream.Position == 0) return null;

        var line = new List<byte>();
        var endOfLine = false;
        while (!endOfLine)
        {
            var b = _stream.ReadByteFromBehind();

            if (b == -1 || b == LineFeedLf)
            {
                endOfLine = true;
            } 
            line.Add(Convert.ToByte(b));
        }

        line.Reverse();
        return _encoding.GetString(line.ToArray());
    }
}

public static class StreamExtensions
{
    public static int ReadByteFromBehind(this Stream stream)
    {
        if (stream.Position == 0) return -1;

        stream.Position = stream.Position - 1;
        var value = stream.ReadByte();
        stream.Position = stream.Position - 1;
        return value;
    }
}

0

Tôi muốn làm điều tương tự. Đây là mã của tôi. Lớp này sẽ tạo các tệp tạm thời chứa các phần của tệp lớn. Điều này sẽ tránh cho trí nhớ bị đầy hơi. Người dùng có thể chỉ định xem họ có muốn đảo ngược tệp hay không. Theo đó nó sẽ trả về nội dung theo cách ngược lại.

Lớp này cũng có thể được sử dụng để ghi dữ liệu lớn trong một tệp duy nhất mà không làm đầy bộ nhớ.

Vui lòng cung cấp thông tin phản hồi.

        using System;
        using System.Collections.Generic;
        using System.Diagnostics;
        using System.IO;
        using System.Linq;
        using System.Text;
        using System.Threading.Tasks;

        namespace BigFileService
        {    
            public class BigFileDumper
            {
                /// <summary>
                /// Buffer that will store the lines until it is full.
                /// Then it will dump it to temp files.
                /// </summary>
                public int CHUNK_SIZE = 1000;
                public bool ReverseIt { get; set; }
                public long TotalLineCount { get { return totalLineCount; } }
                private long totalLineCount;
                private int BufferCount = 0;
                private StreamWriter Writer;
                /// <summary>
                /// List of files that would store the chunks.
                /// </summary>
                private List<string> LstTempFiles;
                private string ParentDirectory;
                private char[] trimchars = { '/', '\\'};


                public BigFileDumper(string FolderPathToWrite)
                {
                    this.LstTempFiles = new List<string>();
                    this.ParentDirectory = FolderPathToWrite.TrimEnd(trimchars) + "\\" + "BIG_FILE_DUMP";
                    this.totalLineCount = 0;
                    this.BufferCount = 0;
                    this.Initialize();
                }

                private void Initialize()
                {
                    // Delete existing directory.
                    if (Directory.Exists(this.ParentDirectory))
                    {
                        Directory.Delete(this.ParentDirectory, true);
                    }

                    // Create a new directory.
                    Directory.CreateDirectory(this.ParentDirectory);
                }

                public void WriteLine(string line)
                {
                    if (this.BufferCount == 0)
                    {
                        string newFile = "DumpFile_" + LstTempFiles.Count();
                        LstTempFiles.Add(newFile);
                        Writer = new StreamWriter(this.ParentDirectory + "\\" + newFile);
                    }
                    // Keep on adding in the buffer as long as size is okay.
                    if (this.BufferCount < this.CHUNK_SIZE)
                    {
                        this.totalLineCount++; // main count
                        this.BufferCount++; // Chunk count.
                        Writer.WriteLine(line);
                    }
                    else
                    {
                        // Buffer is full, time to create a new file.
                        // Close the existing file first.
                        Writer.Close();
                        // Make buffer count 0 again.
                        this.BufferCount = 0;
                        this.WriteLine(line);
                    }
                }

                public void Close()
                {
                    if (Writer != null)
                        Writer.Close();
                }

                public string GetFullFile()
                {
                    if (LstTempFiles.Count <= 0)
                    {
                        Debug.Assert(false, "There are no files created.");
                        return "";
                    }
                    string returnFilename = this.ParentDirectory + "\\" + "FullFile";
                    if (File.Exists(returnFilename) == false)
                    {
                        // Create a consolidated file from the existing small dump files.
                        // Now this is interesting. We will open the small dump files one by one.
                        // Depending on whether the user require inverted file, we will read them in descending order & reverted, 
                        // or ascending order in normal way.

                        if (this.ReverseIt)
                            this.LstTempFiles.Reverse();

                        foreach (var fileName in LstTempFiles)
                        {
                            string fullFileName = this.ParentDirectory + "\\" + fileName;
// FileLines will use small memory depending on size of CHUNK. User has control.
                            var fileLines = File.ReadAllLines(fullFileName);

                            // Time to write in the writer.
                            if (this.ReverseIt)
                                fileLines = fileLines.Reverse().ToArray();

                            // Write the lines 
                            File.AppendAllLines(returnFilename, fileLines);
                        }
                    }

                    return returnFilename;
                }
            }
        }

Dịch vụ này có thể được sử dụng như sau:

void TestBigFileDump_File(string BIG_FILE, string FOLDER_PATH_FOR_CHUNK_FILES)
        {
            // Start processing the input Big file.
            StreamReader reader = new StreamReader(BIG_FILE);
            // Create a dump file class object to handle efficient memory management.
            var bigFileDumper = new BigFileDumper(FOLDER_PATH_FOR_CHUNK_FILES);
            // Set to reverse the output file.
            bigFileDumper.ReverseIt = true;
            bigFileDumper.CHUNK_SIZE = 100; // How much at a time to keep in RAM before dumping to local file.

            while (reader.EndOfStream == false)
            {
                string line = reader.ReadLine();
                bigFileDumper.WriteLine(line);
            }
            bigFileDumper.Close();
            reader.Close();

            // Get back full reversed file.
            var reversedFilename = bigFileDumper.GetFullFile();
            Console.WriteLine("Check output file - " + reversedFilename);
        }

0

Đã có những câu trả lời hay ở đây và đây là một lớp khác tương thích với LINQ mà bạn có thể sử dụng, lớp này tập trung vào hiệu suất và hỗ trợ cho các tệp lớn. Nó giả định một dấu chấm hết dòng "\ r \ n".

Cách sử dụng :

var reader = new ReverseTextReader(@"C:\Temp\ReverseTest.txt");
while (!reader.EndOfStream)
    Console.WriteLine(reader.ReadLine());

Lớp 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

Trong trường hợp bất kỳ ai khác gặp phải điều này, tôi đã giải quyết nó bằng tập lệnh PowerShell sau đây có thể dễ dàng sửa đổi thành tập lệnh C # với một chút nỗ lực.

[System.IO.FileStream]$fileStream = [System.IO.File]::Open("C:\Name_of_very_large_file.log", [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
[System.IO.BufferedStream]$bs = New-Object System.IO.BufferedStream $fileStream;
[System.IO.StreamReader]$sr = New-Object System.IO.StreamReader $bs;


$buff = New-Object char[] 20;
$seek = $bs.Seek($fileStream.Length - 10000, [System.IO.SeekOrigin]::Begin);

while(($line = $sr.ReadLine()) -ne $null)
{
     $line;
}

Về cơ bản, điều này bắt đầu đọc từ 10.000 ký tự cuối cùng của tệp, xuất ra từng dòng.


Điều này sẽ đọc tiếp từ 10.000 byte cuối cùng, không lùi từ cuối đến đầu. Ngoài ra, tại sao không chỉ .Seek(-10000, [System.IO.SeekOrigin]::End);?
— AKX
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookie và Chính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.