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
}

он быстро приходит в ад с Java Io APIs ;) на самом деле, я вижу два способа чтения строк (заканчивая 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, или что-то в этом роде - эти счетчики, похоже, округляются с шагом 16K.

мне действительно нужно реализовать все это самостоятельно, т. е. интерфейс чтения файлов, который:

  • позвольте мне получить/установить позицию в файле
  • операции чтения буферных файлов
  • разрешить чтение строк 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. Примечание: Я не думаю, что это самый эффективный способ, я все еще получаю мою голову вокруг НИО.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 ищите в правильном месте, потому что он предварительно загружает все в память.


Я думаю, что путаница вызвана кодировкой UTF-8 и возможностью двухбайтовых символов.

UTF8 не указывает, сколько байтов находится в одном символе. Я предполагаю из Вашего сообщения, что вы используете однобайтовые символы. Например, 412 байт означают 411 символов. Но если строка использует двухбайтовые символы,вы получите символ 206.

исходная java.пакет io не справлялся с этой многобайтовой путаницей. Таким образом, они добавили больше классов для работы со строками. Пакет смешивает два разных типа обработчиков файлов (и они могут быть запутанными, пока номенклатура не будет отсортирована). The поток классы обеспечивают прямой ввод-вывод данных без преобразования. The читатель классы конвертировать файлы в строки с полной поддержкой многобайтовых символов. Это может помочь прояснить часть проблемы.

поскольку вы заявляете, что используете символы 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 );
...

Я опаздываю на вечеринку здесь, но я столкнулся с этой проблемой в своем собственном проекте.

после Большого обхода Javadocs и переполнения стека, я думаю, что нашел простое решение.

после поиска подходящего места в вашем RandomAccessFile, который я здесь называю raFile сделайте следующее:

FileDescriptor fd = raFile.getFD();
FileReader     fr = new FileReader(fd);
BufferedReader br = new BufferedReader(fr);

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

одна вещь Я не уверен, правильно ли обрабатываются строки UTF8.


API Java IO очень гибкий. К сожалению, иногда гибкость делает его многословным. Основная идея здесь заключается в том, что есть много потоков, писателей и читателей, которые реализуют фантик скороговоркой. Например BufferedInputStream обертывает любой другой InputStream. То же самое касается выходных потоков.

разница между потоками и читателями / писателями заключается в том, что потоки работают с байтами, а читатели/писатели работают с символами.

к счастью некоторые потоки, писатели и читатели имеют удобные конструкторы, которые упрощают кодирование. Если вы хотите прочитать файл, вам просто нужно сказать

    InputStream in = new FileInputStream("/usr/home/me/myfile.txt");
    if (in.markSupported()) {
        in.skip(1024);
        in.read();
    }

Это не так сложно, как вы боитесь.

каналы - это что-то другое. Это часть так называемого" нового IO " или nio. Новый IO не блокируется - это его главное преимущество. Вы можете искать в интернете любой "NIO java tutorial" и читать об этом. Но это сложнее, чем обычный IO и не требуется для большинства приложений.