Java: чтение строк из файла с произвольным доступом с буферизованным вводом
У меня никогда не было близкого опыта работы с Java IO API, и сейчас я очень расстроен. Мне трудно поверить, насколько это странно и сложно, и как трудно было бы выполнить простую задачу.
Моя задача: у меня есть 2 позиции (начальный байт, конечный байт), pos1
а также pos2
, Мне нужно прочитать строки между этими двумя байтами (включая начальный, не включая конечный) и использовать их в качестве строковых объектов UTF8.
Например, в большинстве языков сценариев это будет очень простой 1-2-3-строчный аналог (в Ruby, но по сути он будет таким же для Python, Perl и т. Д.):
f = File.open("file.txt").seek(pos1)
while f.pos < pos2 {
s = f.readline
# do something with "s" here
}
Это быстро приходит к черту с API Java IO;) На самом деле, я вижу два способа чтения строк (заканчивая \n
) из обычных локальных файлов:
- RandomAccessFile имеет
getFilePointer()
а такжеseek(long pos)
, но он readLine() читает строки не-UTF8 (и даже не байтовые массивы), но очень странные строки со сломанной кодировкой, и у него нет буферизации (что, вероятно, означает, что каждыйread*()
вызов будет переведен в единую ОСread()
=> довольно медленно). - BufferedReader имеет большой
readLine()
метод, и он может даже искать сskip(long n)
, но он не может определить четное число уже прочитанных байтов, не говоря уже о текущей позиции в файле.
Я пытался использовать что-то вроде:
FileInputStream fis = new FileInputStream(fileName);
FileChannel fc = fis.getChannel();
BufferedReader br = new BufferedReader(
new InputStreamReader(
fis,
CHARSET_UTF8
)
);
... а затем с помощью fc.position()
чтобы получить текущую позицию чтения файла и fc.position(newPosition)
установить его, но в моем случае это не работает: похоже, он возвращает позицию предварительного заполнения буфера, выполненного BufferedReader, или что-то в этом роде - эти счетчики, кажется, округлены с шагом 16К.
Должен ли я реализовать все это самостоятельно, то есть интерфейс чтения файлов, который бы:
- позвольте мне получить / установить позицию в файле
- операции чтения буферного файла
- разрешить чтение строк UTF8 (или, по крайней мере, разрешить такие операции, как "читать все до следующего"
\n
")
Есть ли более быстрый способ, чем реализовать все это самому? Я что-то наблюдаю?
7 ответов
import org.apache.commons.io.input.BoundedInputStream
FileInputStream file = new FileInputStream(filename);
file.skip(pos1);
BufferedReader br = new BufferedReader(
new InputStreamReader(new BoundedInputStream(file,pos2-pos1))
);
Если тебя не волнует pos2
тогда вам не нужен Apache Commons IO.
Я написал этот код для чтения utf-8, используя randomaccessfiles
//File: CyclicBuffer.java
public class CyclicBuffer {
private static final int size = 3;
private FileChannel channel;
private ByteBuffer buffer = ByteBuffer.allocate(size);
public CyclicBuffer(FileChannel channel) {
this.channel = channel;
}
private int read() throws IOException {
return channel.read(buffer);
}
/**
* Returns the byte read
*
* @return byte read -1 - end of file reached
* @throws IOException
*/
public byte get() throws IOException {
if (buffer.hasRemaining()) {
return buffer.get();
} else {
buffer.clear();
int eof = read();
if (eof == -1) {
return (byte) eof;
}
buffer.flip();
return buffer.get();
}
}
}
//File: UTFRandomFileLineReader.java
public class UTFRandomFileLineReader {
private final Charset charset = Charset.forName("utf-8");
private CyclicBuffer buffer;
private ByteBuffer temp = ByteBuffer.allocate(4096);
private boolean eof = false;
public UTFRandomFileLineReader(FileChannel channel) {
this.buffer = new CyclicBuffer(channel);
}
public String readLine() throws IOException {
if (eof) {
return null;
}
byte x = 0;
temp.clear();
while ((byte) -1 != (x = (buffer.get())) && x != '\n') {
if (temp.position() == temp.capacity()) {
temp = addCapacity(temp);
}
temp.put(x);
}
if (x == -1) {
eof = true;
}
temp.flip();
if (temp.hasRemaining()) {
return charset.decode(temp).toString();
} else {
return null;
}
}
private ByteBuffer addCapacity(ByteBuffer temp) {
ByteBuffer t = ByteBuffer.allocate(temp.capacity() + 1024);
temp.flip();
t.put(temp);
return t;
}
public static void main(String[] args) throws IOException {
RandomAccessFile file = new RandomAccessFile("/Users/sachins/utf8.txt",
"r");
UTFRandomFileLineReader reader = new UTFRandomFileLineReader(file
.getChannel());
int i = 1;
while (true) {
String s = reader.readLine();
if (s == null)
break;
System.out.println("\n line " + i++);
s = s + "\n";
for (byte b : s.getBytes(Charset.forName("utf-8"))) {
System.out.printf("%x", b);
}
System.out.printf("\n");
}
}
}
Для @Ken Bloom Очень быстрый переход на версию Java 7. Примечание: я не думаю, что это самый эффективный способ, я все еще разбираюсь в NIO.2, Oracle начал свое обучение здесь
Также обратите внимание, что здесь не используется новый синтаксис ARM в Java 7 (который заботится об обработке исключений для файловых ресурсов), он не работал в последней сборке openJDK, которая у меня есть. Но если люди хотят увидеть синтаксис, дайте мне знать.
/*
* Paths uses the default file system, note no exception thrown at this stage if
* file is missing
*/
Path file = Paths.get("C:/Projects/timesheet.txt");
ByteBuffer readBuffer = ByteBuffer.allocate(readBufferSize);
FileChannel fc = null;
try
{
/*
* newByteChannel is a SeekableByteChannel - this is the fun new construct that
* supports asynch file based I/O, e.g. If you declared an AsynchronousFileChannel
* you could read and write to that channel simultaneously with multiple threads.
*/
fc = (FileChannel)file.newByteChannel(StandardOpenOption.READ);
fc.position(startPosition);
while (fc.read(readBuffer) != -1)
{
readBuffer.rewind();
System.out.println(Charset.forName(encoding).decode(readBuffer));
readBuffer.flip();
}
}
Начните с RandomAccessFile
и использовать read
или же readFully
чтобы получить байтовый массив между pos1
а также pos2
, Допустим, мы сохранили прочитанные данные в переменной с именем rawBytes
,
Затем создайте свой BufferedReader
с помощью
new BufferedReader(new InputStreamReader(new ByteArrayInputStream(rawBytes)))
Тогда вы можете позвонить readLine
на BufferedReader
,
Предостережение: это, вероятно, использует больше памяти, чем если бы вы могли сделать BufferedReader
ищите правильное местоположение непосредственно, потому что это предварительно загружает все в память.
Я опоздал на вечеринку, но столкнулся с этой проблемой в своем собственном проекте.
После большого обхода Javadocs и переполнения стека, я думаю, что нашел простое решение.
После поиска подходящего места в вашем RandomAccessFile, который я здесь называю raFile
, сделайте следующее:
FileDescriptor fd = raFile.getFD();
FileReader fr = new FileReader(fd);
BufferedReader br = new BufferedReader(fr);
Тогда вы сможете позвонить br.readLine()
к вашему сердцу, что будет намного быстрее, чем звонить raFile.readLine()
,
В одном я не уверен, правильно ли обрабатываются строки UTF8.
Я думаю, что путаница вызвана кодировкой UTF-8 и возможностью двухбайтовых символов.
UTF8 не указывает, сколько байтов в одном символе. Из вашего поста я предполагаю, что вы используете однобайтовые символы. Например, 412 байтов означали бы 411 символов. Но если в строке используются двухбайтовые символы, вы получите 206 символов.
Оригинальный пакет java.io не справился с этой многобайтовой путаницей. Таким образом, они добавили больше классов для работы со строками. Пакет смешивает два разных типа обработчиков файлов (и они могут сбивать с толку, пока номенклатура не отсортирована). Классы потока обеспечивают прямой ввод / вывод данных без какого-либо преобразования. Классы чтения конвертируют файлы в строки с полной поддержкой многобайтовых символов. Это может помочь прояснить часть проблемы.
Поскольку вы заявляете, что используете символы UTF-8, вам нужны классы читателей. В этом случае я предлагаю FileReader. Метод skip() в FileReader позволяет пропустить символы X, а затем начать читать текст. В качестве альтернативы я предпочитаю перегруженный метод read(), поскольку он позволяет вам захватить весь текст за один раз.
Если вы предполагаете, что ваши "байты" являются отдельными символами, попробуйте что-то вроде этого:
FileReader fr = new FileReader( new File("x.txt") );
char[] buffer = new char[ pos2 - pos ];
fr.read( buffer, pos, buffer.length );
...
API Java-ввода очень гибкий. К сожалению, иногда гибкость делает его многословным. Основная идея здесь заключается в том, что существует множество потоков, писателей и читателей, которые реализуют скороговорку. Например, BufferedInputStream переносит любой другой InputStream. То же самое касается выходных потоков.
Разница между потоками и программами чтения / записи заключается в том, что потоки работают с байтами, а программы чтения / записи работают с символами.
К счастью, некоторые потоки, писатели и читатели имеют удобные конструкторы, которые упрощают кодирование. Если вы хотите прочитать файл, вы просто должны сказать
InputStream in = new FileInputStream("/usr/home/me/myfile.txt");
if (in.markSupported()) {
in.skip(1024);
in.read();
}
Это не так сложно, как ты боишься.
Каналы это что-то другое. Это часть так называемого "нового ввода-вывода" или nio. Новый IO не заблокирован - это его главное преимущество. Вы можете найти в интернете любой "учебник по nio java" и прочитать о нем. Но это сложнее, чем обычный ввод-вывод и не требуется для большинства приложений.