Мне нужно обработать большой файл, около 400 КБ строк и 200 М. Но иногда мне приходится обрабатывать снизу вверх. Как я могу использовать здесь итератор (yield return)? В основном я не люблю загружать все в память. Я знаю, что в .NET более эффективно использовать итератор.
Одна из возможностей - прочитать достаточно большой объем с конца, а затем использовать String.LastIndexOf для поиска "\ r \ n" в обратном направлении.
Смотрите мой комментарий в дубликате stackoverflow.com/questions/398378/…





Чтобы создать файловый итератор, вы можете сделать это:
Обновлено:
Это моя фиксированная версия реверсивного чтения файлов с фиксированной шириной:
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", где последнее в конечном итоге считается только одним разрывом строки.
Он также никогда не дает пустых строк - "a \ n \ nb" должно давать "a", "", "b"
мммммм ... Я передаю lineBuffer только тогда, когда нахожу '\ n' (ASCII 10). Вы правы, я не беру в расчет "\ r".
ммммммм. Я также не очень уверен в получении пустых строк. Это поведение класса StreamReader по умолчанию при вызове метода ReadLine ()?
Чтение текстовых файлов в обратном направлении действительно сложно, если вы не используете кодировку фиксированного размера (например, 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;
}
}
}
Обратная связь очень приветствуется. Было весело :)
Righto - Планирую начать примерно через час. Я должен поддерживать однобайтовые кодировки, Encoding.Unicode и Encoding.UTF8. Другие кодировки добул-байта не поддерживаются. Я ожидаю, что тестирование будет головной болью :(
@Jon: подойдет ли мой код .... stackoverflow.com/questions/2241012/…
+1, но запрос функции: удалить спецификацию - если последний (т.е. первый) символ равен 0xFEFF, игнорировать его. Эта версия добавляет? в начало последней строки.
@peenut: Думаю, я бы справился с этим с помощью итератора, обернутого вокруг этого. Постарайтесь таким образом разделить проблемы.
Не упоминайте внесенные мной изменения, я как раз читал этот ответ и наткнулся на невыделенный bool, который тоже хотел выделить. Используя языковой тег, мы можем указать: meta.stackexchange.com/questions/63800/… Но после указания языка C# он все равно не раскрасился. Откатитесь, если хотите. :)
ух ты! Я знаю, что ему больше трех лет, но этот кусок кода потрясающий! Спасибо!! (p.s. Я только что изменил File.OpenRead (filename) на File.Open (filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), чтобы итератор мог читать уже открытые файлы
Прекрасно (даже через 4 года), но мне интересно: в чем смысл слова «запечатанный»? Он поднимает свой уродливый хвост во всей .NET Framework, и теперь я вижу, что даже Джон Скит активно применяет его (по крайней мере, 4 года назад). Неужели это действительно так: «Да, я сделал некоторую рабочую реализацию. Нет, я не позволю вам наследовать, потому что тогда мне придется добавить правильный дизайн»?
@GrimaceofDespair: Больше, «потому что тогда мне пришлось бы проектировать для наследования, что добавляло бы очень значительные затраты с точки зрения как времени разработки, так и будущей гибкости». Часто даже не ясно, как можно разумно использовать наследование для типа - лучше запретить его, пока эта ясность не будет найдена, ИМО.
Тем не менее, fСделать замечательный дизайн очень легко. Так редко я вижу дополнительную ценность в защите от подобного кода. ИМО, затраты на работу с герметичными и внутренними устройствами только для того, чтобы настроить что-то, что в первую очередь должно было работать, часто является таким бременем, что я бы предпочел увидеть неправильный дизайн, который * можно расширять. Но опять же, это еще одно оживленное обсуждение ТАК ...
@JonSkeet Ваш код выдает ошибку, что файл используется другим процессом. Как правило, ваш код используется для чтения файлов журнала, которые также могут быть записаны какой-либо другой службой, поэтому необходимо также обработать этот случай. Спасибо
@rahularyansharma: Мне же нравится разбивать задачи на ортогональные аспекты. Как только вы придумаете, как открыть файл в вашем случае, я ожидаю, что мой код будет работать на вас.
@JonSkeet похоже, что этот класс удаляет поток только после завершения перечисления. Итак, если я преждевременно сломаю foreach, не будет ли поток не удален? В моем случае я просто хочу прочитать последние X строк файла, а затем выйти. Нужно ли это изменить, чтобы обрабатывать преждевременное закрытие потока?
@DLeh: он размещается в блоке finally, который также будет выполняться, если перечислитель удален, что произойдет, если вы выйдете из цикла foreach.
@JonSkeet, ладно, спасибо. Я не был на 100% в том, как счетчики работают с finally. Спасибо!
Есть ли где-нибудь «главная» копия этого, в которой содержатся исправления ошибок?
@MattHouser: У меня есть частное репозиторий с системой управления версиями, которое я хотел бы в какой-то момент открыть, но на данный момент у меня нет времени для этого. jonskeet.uk/csharp/miscutil - текущая "домашняя страница" библиотеки, но, как вы можете видеть, она не обновлялась с 2009 года ...
Что более актуально: эта версия или сайт jonskeet.uk? Я взял этот код и прогнал его через файл UTF8, и когда я нажал на начало файла, он включил последний байт 3-байтового «cookie» UTF8 в качестве первого байта первой строки.
@MattHouser: Насколько я знаю, это одно и то же. Это символ спецификации (метка порядка байтов) (не байт), и да, я явно не пытаюсь удалить его в данный момент. Мне никогда не ясно, когда процедуры ввода-вывода должен удаляют его - в конце концов, он присутствует в файле как символ. Я предлагаю прямо сейчас либо убедиться, что ваши файлы не начинаются с спецификации, либо заменить ее самостоятельно (line = line.Replace("\ufeff", ""))
Это сработало. Спасибо. Отлично работает. Пожалуйста, загрузите этот код в GitHub (или аналогичный) и NuGet :) При поиске в Google 'ReverseLineReader' несколько копий разбросаны повсюду.
@MattHouser: он уже в NuGet, хотя и в предварительной форме, поскольку я, вероятно, хотел бы изменить пространства имен: nuget.org/packages/JonSkeet.MiscUtil
Если кто-то хочет иметь возможность поделиться файлом с другим процессом, например, когда вы хотите прочитать файл журнала, открытый для записи родителем, просто замените: File.OpenRead (filename) на: new FileStream (filename, FileMode. Открыть, FileAccess.ReadWrite, FileShare.ReadWrite)
Кажется, я не могу избавиться от базового потока, кроме случаев, когда я полностью зацикливаюсь или разрываю цикл. Я попробовал сделать GetEnumerator, затем позвонил на него Dispose, но после этого файл все еще используется.
@ user276648: Все должно быть в порядке ... вы входите в цикл вообще? Если вы не позвоните в MoveNext() ни разу, я пойму, как это мог может быть проблемой ...
Вы правы, извините. Я попробовал еще раз, и он работает: GetEnumerator, затем MoveNext, затем Dispose. Файл больше не используется. Однако, если вы никогда не вызываете MoveNext, даже вызов Dispose не удалит базовый поток, как вы упомянули, но на самом деле этого не должно происходить.
@ user276648: Это все еще считается ошибкой IMO, но я не в состоянии исправить это прямо сейчас :(
Нет проблем, в любом случае это довольно просто. Для всех, кто интересуется, вы создаете дочерний класс, реализующий IEnumerator<string>, который использует GetEnumeratorImpl (которому вам нужно передать bufferSize, encoding, characterStartDetector), а в методе Dispose вы удаляете поток.
@JonSkeet UTF-16 может использовать 2 байта или 4 байта на символ, но ваш characterStartDetector рассматривает все 2 байтовые границы как начало символа. Разве это не было бы некорректно при чтении 4-х байтовых символов?
@ Вихрам: Вполне возможно. У меня нет времени исправить это прямо сейчас, но я добавлю TODO.
@JonSkeet Спасибо, Джон
Замечательный код ... но ... Этот код закрывает мой тщательно управляемый поток, который я ему предоставляю. И это без использования оператора using. Он должен реализовывать IDisposable .. так что у меня есть хоть какой-то контроль. Пример использования: я обрабатываю огромный файл данных, который содержит даты ... Я прочитал первую строку, я прочитал последнюю строку ... поэтому я могу показать индикатор выполнения. После прочтения последней строчки .. поток закрывается, и мне приходится его снова открывать. : '(
@JDC: Да, в большинстве случаев я считаю, что это самое полезное. Но, очевидно, вы можете изменить код в соответствии со своим вариантом использования. Обратите внимание, что каждый раз, когда вы используете foreach, вы неявно делать используете оператор using, поэтому я не считаю неразумным закрывать поток. Вот почему конструктор принимает Func<Stream>, а не Stream. Возможно, вы захотите использовать код как есть, но имея оболочку Stream, которая делегирует вызовы Другие, чем Close / Dispose, в базовый поток.
Вот пример того, как использовать его для перебора строк в файле журнала: github.com/projectkudu/kudu/blob/…
Вы можете читать файл по одному символу в обратном направлении и кэшировать все символы, пока не дойдете до возврата каретки и / или перевода строки.
Затем вы переворачиваете собранную строку и превращаете ее в линию.
Однако читать файл по одному символу в обратном направлении сложно - потому что вы должны уметь распознавать начало символа. Насколько это просто, будет зависеть от кодировки.
Я помещал файл в список построчно, затем использовал 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)
{
...
Не лучшее решение для больших файлов, так как вам нужно полностью загружать их в оперативную память. И ОП явно указал, что не хочет загружать его полностью.
Attention: this approach doesn't work (explained in 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 МБ.
Conclusion
Whatever `Reverse()' is doing, it is not a good choice for reading bottom of a big file.
Интересно, действительно ли при вызове Reverse загружается весь файл в память. Разве не нужно сначала установить конечную точку Enumerable? То есть внутри enumerable полностью перечисляет файл для создания временного массива, который затем Reversed, который затем перечисляется один за другим с использованием ключевого слова yield, так что создается новый Enumerable, повторяющийся в обратном порядке
Первоначальный ответ был неправильным, но я сохраняю EDITED ответ здесь, поскольку это может помешать другим людям использовать этот подход.
Я хотел сделать то же самое. Вот мой код. Этот класс будет создавать временные файлы, содержащие фрагменты большого файла. Это позволит избежать раздувания памяти. Пользователь может указать, хочет ли он / она реверсировать файл. Соответственно, он вернет контент в обратном порядке.
Этот класс также можно использовать для записи больших данных в один файл без увеличения объема памяти.
Пожалуйста, оставьте отзыв.
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);
}
Здесь уже есть хорошие ответы, и вот еще один 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
}
Я знаю, что этот пост очень старый, но, поскольку я не мог найти, как использовать решение, получившее наибольшее количество голосов, я наконец нашел следующее: вот лучший ответ, который я нашел с низкой стоимостью памяти в 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();
}
}
Вы должны включить соответствующие части из ссылки в свой ответ и добавить ссылку только для справки, чтобы ваш ответ по-прежнему имел ценность, даже если ссылка изменится.
Если у вас есть частные поля IDisposable, вам также следует реализовать IDisposable и правильно избавиться от этих полей.
Если кто-то еще столкнется с этим, я решил это с помощью следующего сценария 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;
}
Это в основном начинает чтение с последних 10 000 символов файла с выводом каждой строки.
Это будет читать вперед от последних 10 000 байтов, а не назад от конца до начала. Кроме того, почему не только .Seek(-10000, [System.IO.SeekOrigin]::End);?
Очень быстрое решение для больших файлов. Используйте командлет PowerShell Get-Content с опцией 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
Я тоже свой раствор добавляю. Прочитав несколько ответов, ничего не подходило к моему случаю. Я читаю байт за байтом сзади, пока не найду 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;
}
}
См. Также: Получить последние 10 строк очень большого текстового файла> 10 ГБ C#