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())) &amp;&amp; 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" и прочитать о нем. Но это сложнее, чем обычный ввод-вывод и не требуется для большинства приложений.

Другие вопросы по тегам