Произвольный доступ к файлам Java: получить байтовое смещение начала строки

Мне нужно произвольно обращаться к определенным записям в текстовом (ASCII) файле, а затем читать оттуда, пока не будет найдена определенная «последовательность остановки» (разделитель записей). Файл содержит многострочные записи, и каждая запись отделена разделителем. Каждая запись также занимает разное количество строк! Это общеизвестный формат файла в конкретной области знаний, и его нельзя изменить.

Я хочу проиндексировать файл, чтобы быстро перейти к запрошенной записи.

В подобных вопросах, как

Как получить доступ к строке в файле по позиции в Java

и ссылки в нем, ответ всегда ссылается на метод seek() различных классов, таких как RandomAccessFile. Я знаю об этом!

У меня проблема в том, как получить смещение, необходимое для поиска! (индексирует файл)

BufferedReader не имеет метода getFilePointer() или любого другого для получения текущего смещения в байтах от начала файла. RandomAccessFile имеет метод readLine(), но его производительность просто ужасна. Для моего случая вообще не годится.

Мне нужно будет прочитать файл построчно, и каждый раз, когда будет найден разделитель записи, мне нужно получить смещение байта. Как я могу этого добиться?


person beginner_    schedule 20.09.2013    source источник
comment
Можно ли разделить файл на записи и сохранить записи в базе данных? Может быть, какой-то вкус NoSQL?   -  person Viktor Seifert    schedule 20.09.2013
comment
Кэшировать индексы: либо в памяти, либо в каком-либо объекте-держателе индекса; или в каком-либо постоянном хранилище, таком как база данных, отдельный файл или заголовок; смотря для чего вам это нужно.   -  person blgt    schedule 20.09.2013
comment
@ViktorSeifert должно быть простым решением, как в автономном режиме   -  person beginner_    schedule 22.09.2013
comment
Индекс @blgt кэшируется в памяти, но планируется добавление поддержки его сохранения и восстановления. Но проблема в том, чтобы получить приемлемую производительность при его построении.   -  person beginner_    schedule 22.09.2013


Ответы (4)


Вы можете попытаться создать подкласс класса BufferedReader, чтобы запомнить позицию чтения. Но у вас не будет функции поиска.

Как вы упомянули, запись может быть многострочной, но все записи разделены стоп-последовательностью. Учитывая это, вы можете использовать RandomAccessFile следующим образом:

  1. иметь байтовый буфер byte b[], скажем, размером 8 КБ (это по соображениям производительности)

  2. прочитать 8к из файла в этот буфер и попытаться найти разделитель, если не нашел, прочитать еще блок 8к, но предварительно дописать данные в какую-нибудь StringBuilder или другую структуру.

  3. когда вы нашли разделитель, позиция разделителя определяется количеством байтов, обработанных с момента последнего найденного разделителя (вам нужно выполнить простую математику).

Сложная часть будет, если разделитель записи длиннее 1 символа, но это должно быть большой проблемой.

person Claudiu    schedule 20.09.2013
comment
разделитель 4 символа. И теоретически он может (но очень маловероятно, а также глупо) появиться в записи, то есть быть на 100% уверенным, что это должны быть 4 символа в отдельной строке. - person beginner_; 20.09.2013
comment
так что в основном ваш разделитель newline + your 4 characters. не должно быть проблемой. каждый раз, когда запись превышает 8 КБ и уже добавляет часть из них к StringBuilder, о котором я упоминал на шаге № 2, вы можете попытаться увидеть, заканчивается ли это StringBuilder началом разделителя, а вновь считанные данные начинаются с остальной части разделитель. Это может быть метод из нескольких строк кода. Дайте мне знать, если это было ясно. - person Claudiu; 20.09.2013

После долгих гуглений, проб и ошибок и многого другого я нашел решение, которое просто обертывает RandomAccessFile и предоставляет все методы. Однако метод readLine() был значительно улучшен путем разговора с методом BufferedReader с небольшими изменениями. Производительность теперь идентична ему.

Этот так называемый класс OptimizedRandomAccessFile буферизует вызовы readLine() до тех пор, пока не вызываются другие методы, требующие или влияющие на позицию в файле. например в:

OptimizedRandomAccessFile raf = new OptimizedRandomAccessFile(filePath, "r");
String line = raf.readLine();
int nextByte = raf.read();

nextByte будет содержать первый байт следующей строки в файле.

Полный код можно найти на bitbucket.

person beginner_    schedule 22.09.2013

Я бы использовал следующую последовательность декораторов java.io:

   InputStreamReader    <-- reader, the top reader
   CountingInputStream  <-- cis, stores the position (from Google Guava)
   BufferedInputStream  <-- speeds up file reading
   FileInputStream

Затем вы читаете этот верхний ридер, реализуя метод readLine(), который считывает символы один за другим до разделителя строк. Я бы не стал использовать BufferedReader, так как он испортит текущую позицию, прочитав полный буфер фиксированного размера.

Итак, если я правильно решу задачу, алгоритм будет таким же простым, как

  1. long lineStartPosition = cis.getCount();
  2. String s = readLine(reader);
  3. if(s.equals(DELIMITER)) { storeToIndex(lineStartPosition, recordData); }
person Andrey Chaschev    schedule 20.09.2013
comment
InputStreamReader выполняет буферизацию в некоторых условиях. Чтобы обеспечить эффективное преобразование байтов в символы, из базового потока может быть прочитано больше байтов, чем необходимо для выполнения текущей операции чтения. - person Jeow Li Huan; 07.03.2014

Вы можете прочитать весь файл данных и записать, где находится разделитель, и сохранить эти метаданные в другом файле. Теперь вы можете использовать метаданные для навигации по файлу (переход от одного разделителя к другому). Каждый раз, когда файл данных изменяется, вам придется повторно сканировать его и заново генерировать метаданные.

person aUserHimself    schedule 20.09.2013