.NET С# - Случайный доступ в текстовых файлах - нелегкий путь?
У меня есть текстовый файл, содержащий несколько "записей" внутри него. Каждая запись содержит имя и набор чисел в качестве данных.
Я пытаюсь создать класс, который будет читать файл, представлять только имена всех записей, а затем разрешать пользователю выбирать, какие данные записи он/она хочет.
В первый раз, когда я просматриваю файл, я только читаю имена заголовков, но я могу отслеживать "позицию" в файле, где находится заголовок. Мне нужен произвольный доступ к текстовому файлу, чтобы искать начало каждой записи после того, как пользователь запросит ее.
Мне нужно сделать это так, потому что файл слишком велик для полного чтения в памяти (1 ГБ +) с другими требованиями к памяти приложения.
Я попытался использовать класс .NET StreamReader для выполнения этого (что обеспечивает очень удобную функциональность "ReadLine", но невозможно зафиксировать истинную позицию файла (позиция в свойстве BaseStream перекошена из-за буфера, который использует класс).
Нет ли простого способа сделать это в .NET?
Ответы
Ответ 1
Есть несколько хороших ответов, но я не мог найти исходный код, который бы работал в моем очень упрощенном случае. Вот он, надеясь, что он спасет кого-то еще в тот час, который я потратил на поиски.
"Очень упрощенный случай", который я имею в виду: текстовое кодирование имеет фиксированную ширину, а символы окончания строки одинаковы во всем файле. Этот код хорошо работает в моем случае (где я разбираю файл журнала, и мне когда-нибудь нужно искать в файле, а затем вернуться. Я выполнил достаточно, чтобы сделать то, что мне нужно было сделать (например: только один конструктор, и только переопределить ReadLine()), поэтому, скорее всего, вам нужно будет добавить код... но я думаю, что это разумная отправная точка.
public class PositionableStreamReader : StreamReader
{
public PositionableStreamReader(string path)
:base(path)
{}
private int myLineEndingCharacterLength = Environment.NewLine.Length;
public int LineEndingCharacterLength
{
get { return myLineEndingCharacterLength; }
set { myLineEndingCharacterLength = value; }
}
public override string ReadLine()
{
string line = base.ReadLine();
if (null != line)
myStreamPosition += line.Length + myLineEndingCharacterLength;
return line;
}
private long myStreamPosition = 0;
public long Position
{
get { return myStreamPosition; }
set
{
myStreamPosition = value;
this.BaseStream.Position = value;
this.DiscardBufferedData();
}
}
}
Вот пример использования PositionableStreamReader:
PositionableStreamReader sr = new PositionableStreamReader("somepath.txt");
// read some lines
while (something)
sr.ReadLine();
// bookmark the current position
long streamPosition = sr.Position;
// read some lines
while (something)
sr.ReadLine();
// go back to the bookmarked position
sr.Position = streamPosition;
// read some lines
while (something)
sr.ReadLine();
Ответ 2
FileStream имеет метод seek().
Ответ 3
Вместо StreamReader вы можете использовать System.IO.FileStream. Если вы точно знаете, какой файл содержит (например, кодировку), вы можете выполнять все операции, например, с помощью StreamReader.
Ответ 4
Если вы гибки в том, как записывается файл данных, и не против, чтобы он был немного менее удобным для редактирования текста, вы могли записать свои записи с помощью BinaryWriter:
using (BinaryWriter writer =
new BinaryWriter(File.Open("data.txt", FileMode.Create)))
{
writer.Write("one,1,1,1,1");
writer.Write("two,2,2,2,2");
writer.Write("three,3,3,3,3");
}
Затем сначала чтение каждой записи прост, потому что вы можете использовать метод ReadString BinaryReader:
using (BinaryReader reader = new BinaryReader(File.OpenRead("data.txt")))
{
string line = null;
long position = reader.BaseStream.Position;
while (reader.PeekChar() > -1)
{
line = reader.ReadString();
//parse the name out of the line here...
Console.WriteLine("{0},{1}", position, line);
position = reader.BaseStream.Position;
}
}
BinaryReader не буферизируется, поэтому вы получаете правильную позицию для хранения и использования позже. Единственная проблема состоит в анализе имени из строки, что может быть связано с StreamReader.
Ответ 5
Является ли кодирование фиксированным размером (например, ASCII или UCS-2)? Если это так, вы можете отслеживать индекс символа (в зависимости от количества символов, которые вы видели) и находить на нем двоичный индекс.
В противном случае нет - вам в принципе нужно написать собственную реализацию StreamReader, которая позволит вам заглянуть в двоичный индекс. Позор, который StreamReader не реализует, согласен.
Ответ 6
Я думаю, что функция записи в библиотеке файлов FileHelpers может помочь вам. http://filehelpers.sourceforge.net/runtime_classes.html
Ответ 7
Несколько вопросов, которые могут представлять интерес.
1) Если строки являются фиксированным набором символов в длину, это необязательно полезная информация, если набор символов имеет переменные размеры (например, UTF-8). Поэтому проверьте свой набор символов.
2) Вы можете определить точное положение курсора с помощью StreamReader с помощью значения BaseStream.Position IF. Сначала вы будите() буферов (что заставит текущую позицию быть там, где начнется следующее чтение - по одному байту после последнего байта).
3) Если вы заранее знаете, что точная длина каждой записи будет одинакового количества символов, а набор символов использует символы фиксированной ширины (поэтому каждая строка имеет такое же количество байтов), вы можете использовать FileStream с фиксированным размером буфера, чтобы соответствовать размеру строки, и позиция курсора в конце каждого чтения будет, perforce, началом следующей строки.
4) Есть ли какая-то особая причина, почему, если строки имеют одинаковую длину (в байтах здесь), вы не просто используете номера строк и вычисляете смещение байта в файле на основе номера строки x строки
Ответ 8
Вы уверены, что файл слишком большой? Вы пробовали это таким образом, и это вызвало проблему?
Если вы выделяете большой объем памяти, и вы не используете его прямо сейчас, Windows просто заменит его на диск. Следовательно, обратившись к нему из "памяти", вы достигнете того, что хотите - произвольного доступа к файлу на диске.
Ответ 9
Этот точный вопрос был задан в 2006 году здесь: http://www.devnewsgroups.net/group/microsoft.public.dotnet.framework/topic40275.aspx
Резюме:
"Проблема заключается в том, что StreamReader буферизует данные, поэтому значение, возвращаемое в
Свойство BaseStream.Position всегда опережает фактическую обработанную строку.
Однако, "если файл закодирован в текстовой кодировке с фиксированной шириной, вы можете отслеживать, сколько текста было прочитано и умножить на ширину"
а если нет, вы можете просто использовать FileStream и читать char за раз, а затем свойство BaseStream.Position должно быть правильным