Як читати текстовий файл навпаки за допомогою ітератора в C #


87

Мені потрібно обробити великий файл, близько 400 тис. Рядків і 200 М. Але іноді мені доводиться обробляти знизу вгору. Як я можу тут використовувати ітератор (return yield)? В основному я не люблю завантажувати все в пам'ять. Я знаю, що ефективніше використовувати ітератор у .NET.



Однією з можливостей було б прочитати достатньо велику кількість з кінця, а потім скористатися String.LastIndexOf, щоб повернутися назад, шукаючи "\ r \ n".
user34660

Дивіться мій коментар у дублікаті stackoverflow.com/questions/398378/…
Xan-Kun Clark-Davis

Відповіді:


136

Читати текстові файли назад дуже складно, якщо ви не використовуєте кодування фіксованого розміру (наприклад, ASCII). Коли у вас кодування змінного розміру (наприклад, UTF-8), вам доведеться перевіряти, перебуваєте ви в середині символу чи ні, коли ви отримуєте дані.

У фреймворк нічого не вбудовано, і я думаю, вам доведеться робити окреме жорстке кодування для кожного кодування змінної ширини.

РЕДАГУВАТИ: Це було дещо перевірено, але це не означає, що в ньому все ще немає деяких тонких помилок. Він використовує StreamUtil від MiscUtil, але я включив лише необхідний (новий) метод звідти внизу. О, і для цього потрібен рефакторинг - як ви побачите, є один досить здоровенний метод:

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;
        }
    }
}

Дуже вітаємо. Це було весело :)


Правильно - я планую розпочати приблизно за годину. Я повинен мати можливість підтримувати однобайтове кодування, Encoding.Unicode та Encoding.UTF8. Інші добуле-байтові кодування не підтримуються. Я очікую, що тестування буде болем :(
Джон Скіт,

4
Оце Так! Я знаю, що йому вже більше трьох років, але цей шматок коду похитнувся! Дякую!! (ps Я щойно змінив File.OpenRead (ім'я файлу) на File.Open (ім'я файлу, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), щоб ітератор читав уже відкриті файли
Стефано

1
@GrimaceofDespair: Більше "тому що тоді мені довелося б розробити проект для успадкування, що додає дуже значні витрати як з точки зору дизайну, так і з точки зору майбутньої гнучкості". Часто навіть незрозуміло, як спадщину можна розумно використовувати для типу - краще заборонити, поки ця чіткість не буде знайдена, IMO.
Джон Скіт,

3
@rahularyansharma: Тоді як я люблю розділяти проблеми на ортогональні аспекти. Після того, як ви вирішите, як відкрити файл у вашому випадку, я сподіваюся, що мій код буде працювати просто для вас.
Jon Skeet,

1
Якщо хтось хоче мати можливість поділитися файлом з іншим процесом, наприклад, коли ви хочете прочитати файл журналу, відкритий для запису батьком, просто замініть: File.OpenRead (ім'я файлу) на: new FileStream (ім'я файлу, FileMode. Open, FileAccess.ReadWrite, FileShare.ReadWrite)
гама

9

Увага: цей підхід не працює (пояснено в EDIT)

Ви можете використовувати File.ReadLines, щоб отримати ітератор рядків

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

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

РЕДАГУВАТИ:

Прочитавши коментар applejacks01 , я запускаю кілька тестів, і це виглядає так, ніби .Reverse() насправді завантажується цілий файл.

Раніше я File.ReadLines()друкував перший рядок файлу розміром 40 Мб - обсяг пам'яті консольної програми становив 5 Мб . Потім використовується File.ReadLines().Reverse()для друку останнього рядка того самого файлу - використання пам'яті становило 95 Мб .

Висновок

Що б не робив "Reverse ()", це не найкращий вибір для читання нижньої частини великого файлу.


3
Цікаво, чи насправді виклик Reverse DOES завантажує весь файл у пам’ять. Чи не потрібно спочатку встановлювати кінцеву точку Чисельного? Тобто внутрішньо, enumerable повністю перелічує файл для створення тимчасового масиву, який потім повертається назад, який потім перераховується по одному за допомогою ключового слова yield так, що новий Enumerable створюється з ітерацією у зворотному порядку
applejacks01,

2
Оригінальна відповідь була помилковою, але я зберігаю тут РЕДАГОВАНУ відповідь, оскільки це може перешкодити іншим людям використовувати цей підхід.
Роман Гудков

3

Щоб створити ітератор файлів, ви можете зробити це:

РЕДАГУВАТИ:

Це моя фіксована версія пристрою для зчитування зворотних файлів із фіксованою шириною:

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);
}

Зараз це майже правильно для ISO-8859-1, але не для будь-якого іншого кодування. Кодування робить це справді хитро :(
Джон Скіт,

Що ви маєте на увазі під терміном "наближений до правильності ISO-8859-1"? Чого ще не вистачає?
Ігор Зелая

Обробка не зовсім правильна для відповідності "\ r" "\ n" та "\ r \ n", де останнє в кінцевому підсумку вважається лише розривом одного рядка.
Джон Скіт,

1
Він також ніколи не дає порожніх рядків - "a \ n \ nb" повинно давати "a", "", "b"
Джон Скіт,

мммммм ... Я видаю рядок-буфер, лише коли знаходжу '\ n' (ASCII 10). Ви маєте рацію, я не беру до уваги '\ r'.
Ігор Зелая

2

Я помістив файл у список, рядок за рядком, а потім використав 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
Не найкраще рішення для великих файлів, оскільки вам потрібно повністю завантажити його в оперативну пам’ять. І OP чітко вказав, що він не хоче завантажувати його повністю.
CodesInChaos

1

Ви можете читати файл по одному символу назад і кешувати всі символи, поки не дійдете до каретки повернення та / або подачі рядків.

Потім ви повертаєте зібраний рядок назад і видаєте його як лінію.


4
Читання файлу по одному символу назад навпаки важко - адже ви повинні мати можливість розпізнати початок символу. Наскільки це просто буде залежати від кодування.
Джон Скіт,

1

Я знаю, що цей пост дуже старий, але оскільки я не міг знайти, як використовувати найбільш голосоване рішення, я нарешті знайшов це: ось найкраща відповідь, яку я знайшов із низькою вартістю пам'яті у VB та C #

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

Сподіваюся, я допоможу іншим у цьому, тому що це значить, що мені знадобиться години, щоб нарешті знайти цю публікацію!

[Редагувати]

Ось код 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();
    }
}

Ви повинні включити відповідні частини із посилання у свою відповідь і додати посилання лише для довідки, щоб ваша відповідь все одно додала значення, навіть якщо посилання змінюється.
Thomas Flinkow,

Якщо у вас є приватні IDisposableполя, вам слід також реалізувати IDisposableі правильно розпоряджатися цими полями.
thomasb

1

Дуже швидке рішення для величезних файлів . Використовуйте командлет Get-Content PowerShell з опцією Tail. Виклик PowerShell дасть трохи накладних витрат, але для величезних файлів це ні до чого

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();

Необхідне посилання на PowerShell

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


1

Я також додаю своє рішення. Після прочитання деяких відповідей нічого насправді не відповідало моєму випадку. Я читаю байт за байтом ззаду, поки не знаходжу LineFeed, потім відновлюю зібрані байти як рядок, не використовуючи буферизацію .

Використання:

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

Реалізація:

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

Я хотів зробити подібне. Ось мій код. Цей клас створить тимчасові файли, що містять фрагменти великого файлу. Це дозволить уникнути здуття пам’яті. Користувач може вказати, чи бажає він змінити файл. Відповідно, він поверне вміст у зворотному порядку.

Цей клас також може використовуватися для запису великих даних в один файл без здуття пам'яті.

Будь ласка, надайте відгук.

        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;
                }
            }
        }

Цією послугою можна скористатися наступним чином -

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

Тут вже є хороші відповіді, і ось ще один сумісний з LINQ клас, який ви можете використовувати, який зосереджується на продуктивності та підтримці великих файлів. Він передбачає завершення рядка "\ r \ n".

Використання :

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

Клас 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

Якщо хтось ще стикається з цим, я вирішив це за допомогою наведеного нижче сценарію PowerShell, який можна легко змінити на сценарій 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;
}

Це в основному починає читати з останніх 10000 символів файлу, виводячи кожен рядок.


Це буде читатися вперед з останніх 10 000 байт, а не назад від кінця до початку. Крім того, чому не просто .Seek(-10000, [System.IO.SeekOrigin]::End);?
AKX
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.