Как читать кодированный файл UTF8 с помощью RandomAccessFile?

У меня есть текстовый файл, который был закодирован с помощью UTF8 (для символов, специфичных для языка). Мне нужно использовать RandomAccessFile для поиска конкретной позиции и чтения.

Я хочу читать по очереди.

String str = myreader.readLine(); //returns wrong text, not decoded 
String str myreader.readUTF(); //An exception occurred: java.io.EOFException

Ответ 1

Вы можете преобразовать строку, прочитанную readLine в UTF8, используя следующий код:

public static void main(String[] args) throws IOException {
    RandomAccessFile raf = new RandomAccessFile(new File("MyFile.txt"), "r");
    String line = raf.readLine();
    String utf8 = new String(line.getBytes("ISO-8859-1"), "UTF-8");
    System.out.println("Line: " + line);
    System.out.println("UTF8: " + utf8);
}

Содержимое MyFile.txt: (кодировка UTF-8)

Привет из Украины

Выход консоли:

Line: ÐÑÐ¸Ð²ÐµÑ Ð¸Ð· УкÑаинÑ
UTF8: Привет из Украины

Ответ 2

Документы API говорят следующее для readUTF8

Считывает строку из этого файла. Строка была закодирована с использованием измененный формат UTF-8.

Первые два байта считываются, начиная с текущего указателя файла, как будто readUnsignedShort. Это значение дает количество следующих байты, которые находятся в закодированной строке, а не длина полученного строка. Следующие байты затем интерпретируются как кодирование байтов символов в модифицированном формате UTF-8 и преобразуются в символы.

Этот метод блокируется до тех пор, пока все байты не будут прочитаны, конец потока, или генерируется исключение.

Отформатирована ли ваша строка таким образом?

Это похоже на объяснение вашего EOF exceptuon.

Ваш файл является текстовым файлом, поэтому ваша фактическая проблема - это декодирование.

Самый простой ответ, который я знаю:

try(BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("jedis.txt"),"UTF-8"))){

    String line = null;
    while( (line = reader.readLine()) != null){
        if(line.equals("Obi-wan")){
            System.out.println("Yay, I found " + line +"!");
        }
    }
}catch(IOException e){
    e.printStackTrace();
}

Или вы можете установить текущую системную кодировку с системным свойством file.encoding на UTF-8.

java -Dfile.encoding=UTF-8 com.jediacademy.Runner arg1 arg2 ...

Вы также можете установить его как системное свойство во время выполнения с System.setProperty(...), если он нужен только для этого конкретного файла, но в таком случае я думаю, что я предпочел бы OutputStreamWriter.

Установив системное свойство, вы можете использовать FileReader и ожидать, что он будет использовать UTF-8 в качестве кодировки по умолчанию для ваших файлов. В этом случае для всех файлов, которые вы читаете и пишите.

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

Несколько как

CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
decoder.onMalformedInput(CodingErrorAction.REPORT);
decoder.onUnmappableCharacter(CodingErrorAction.REPORT);
BufeferedReader out = new BufferedReader(new InpuStreamReader(new FileInputStream("jedis.txt),decoder));

Вы можете выбирать между действиями IGNORE | REPLACE | REPORT

ИЗМЕНИТЬ

Если вы настаиваете на использовании RandomAccessFile, вам нужно знать точное смещение строки, которую вы собираетесь читать. И не только это, чтобы читать с помощью метода readUTF(), вы должны написать файл с помощью метода writeUTF(). Поскольку этот метод, как указано выше в JavaDocs, ожидает определенного форматирования, в котором первые два беззнаковых байта представляют длину в байтах строки UTF-8.

Таким образом, если вы это сделаете:

try(RandomAccessFile raf = new RandomAccessFile("jedis.bin", "rw")){

    raf.writeUTF("Luke\n"); //2 bytes for length + 5 bytes
    raf.writeUTF("Obiwan\n"); //2 bytes for length + 7 bytes
    raf.writeUTF("Yoda\n"); //2 bytes for lenght + 5 bytes

}catch(IOException e){
    e.printStackTrace();
}

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

Если вы откроете файл jedis.bin, вы заметите, что это двоичный файл, а не текстовый файл.

Теперь я знаю, что "Luke\n" составляет 5 байтов в UTF-8, а "Obiwan\n" - 7 байтов в UTF-8. И что метод writeUTF() будет вставлять 2 байта перед каждой из этих строк. Поэтому перед "Yoda\n" есть (5 + 2) + (7 + 2) = 16 байт.

Итак, я мог бы сделать что-то подобное, чтобы добраться до последней строки:

try (RandomAccessFile raf = new RandomAccessFile("jedis.bin", "r")) {

    raf.seek(16);
    String val = raf.readUTF();
    System.out.println(val); //prints Yoda

} catch (IOException e) {
    e.printStackTrace();
}

Но это не сработает, если вы написали файл с классом Writer, потому что писатели не следуют правилам форматирования метода writeUFT().

В таком случае лучше всего, чтобы ваш двоичный файл был отформатирован таким образом, чтобы все строки занимали одинаковое количество пробелов (количество байтов, а не количество символов, поскольку количество байтов является переменной в UTF-8 в зависимости от символов в вашей строке), если не все пространство необходимо, вы его вставляете:

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

Ответ 3

Вы не сможете пойти на это так. Функция seek поместит вас на некоторое количество байтов. Нет гарантии, что вы выровнены по границе символа UTF-8.

Ответ 4

Я понимаю, что это старый вопрос, но он все еще, кажется, имеет некоторый интерес, и нет принятого ответа.

То, что вы описываете, по сути является проблемой структур данных. Обсуждение UTF8 здесь - красная селедка - вы столкнулись бы с той же проблемой, используя кодировку с фиксированной длиной, такую ​​как ASCII, потому что у вас есть строки переменной длины. Вам нужен какой-то индекс.

Если вы абсолютно не можете изменить сам файл ( "строковый файл" ) - как бы то ни было, вы всегда можете создать внешний индекс. В первый раз (и только в первый раз) доступ к файлу строки, вы читаете его полностью (последовательно), записывая положение байта начала каждой строки и заканчивая записью позиции конца файла ( чтобы сделать жизнь проще). Это может быть достигнуто с помощью следующего кода:

myList.add(0); // assuming first string starts at beginning of file
while ((line = myRandomAccessFile.readLine()) != null) {
    myList.add(myRandomAccessFile.getFilePointer());
}

Затем вы записываете эти целые числа в отдельный файл ( "индексный файл" ), который вы будете читать в каждый последующий момент, когда вы запустите свою программу и собираетесь получить доступ к строковому файлу. Чтобы получить доступ к строке n th, выберите индекс n th и n+1 th из индексного файла (вызовите эти A и B). Затем вы пытаетесь позиционировать A в строковом файле и читать B-A байты, которые затем декодируете из UTF8. Например, чтобы получить строку i:

myRandomAccessFile.seek(myList.get(i));
byte[] bytes = new byte[myList.get(i+1) - myList.get(i)];
myRandomAccessFile.readFully(bytes);
String result = new String(bytes, "UTF-8");

Во многих случаях, однако, было бы лучше использовать базу данных, такую ​​как SQLite, которая создает и поддерживает индекс для вас. Таким образом, вы можете добавлять и изменять дополнительные "строки" без необходимости воссоздавать весь индекс. См. https://www.sqlite.org/cvstrac/wiki?p=SqliteWrappers для реализаций Java.

Ответ 5

Чтение файла с помощью readLine() работало для меня:

RandomAccessFile raf = new RandomAccessFile( ... );
String line;
while ((line = raf.readLine()) != null) { 
    String utf = new String(line.getBytes("ISO-8859-1"));
    ...
}

// my file content has been created with:
raf.write(myStringContent.getBytes());

Ответ 6

Метод readUTF() метода RandomAccessFile обрабатывает первые два байта из текущего указателя как размер байтов, после двух байтов из текущей позиции, которые будут считаны и возвращены как строка.

Чтобы этот метод работал, содержимое должно быть записано с использованием метода writeUTF(), так как он использует первые два байта после текущей позиции для сохранения размера содержимого, а затем записывает содержимое. В противном случае, в большинстве случаев вы получите EOFException.

См. Http://www.zoftino.com/java-random-access-files для деталей.

Ответ 7

Я нахожу API для RandomAccessFile сложным.

Если ваш текст фактически ограничен значениями UTF-8 0-127 (самые младшие 7 бит UTF-8), тогда безопасно использовать readLine(), но внимательно прочитайте эти Javadocs: Это один странный метод. Цитировать:

Этот метод последовательно считывает байты из файла, начиная с текущего указателя файла, до тех пор, пока он не достигнет терминатора строки или конца файла. Каждый байт преобразуется в символ, беря байтовое значение для младших восьми бит символа и устанавливая высокие восемь бит символа равным нулю. Поэтому этот метод не поддерживает полный набор символов Unicode.

Чтобы безопасно читать UTF-8, я предлагаю вам прочитать (некоторые или все) необработанные байты с комбинацией length() и read(byte[]). Затем преобразуйте ваши байты UTF-8 в Java String с помощью этого конструктора: new String(byte[], "UTF-8").

Чтобы безопасно писать UTF-8, сначала преобразуйте Java String в правильные байты с помощью someText.getBytes("UTF-8"). Наконец, напишите байты с помощью write(byte[]).

Ответ 8

Как только вы окажетесь в определенной строке (это означает, что вы ответили на первую часть своей проблемы, см. Ответ @martinjs), вы можете прочитать всю строку и сделать из нее String, используя оператор, заданный в ответе @Matthieu., Но чтобы проверить правильность данного утверждения, мы должны задать себе 4 вопроса. Это не самоочевидно.

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

Оператор, который читает строку и превращает ее в String:

String utf8 = new String(raf.readLine().getBytes("ISO-8859-1"), "UTF-8");
  1. Что такое байт в UTF-8? Это означает, что значения допустимы. Мы увидим, что вопрос фактически бесполезен, как только мы ответим на вопрос 2.
  2. readLine(). UTF-8 байтов → UTF-16 байтов ок? Да. Поскольку UTF-16 дает значение всем целым числам от 0 до 255, закодированным в 2 байта, если байт значащего значения (MSB) равен 0. Это гарантируется readLine().
  3. getBytes("ISO-8859-1"). Символы, закодированные в UTF-16 (Java String, 1 или 2 char (код единицы) на символ) → ISO-8859-1 байт нормально? Да. Точки кода символов в строке Java ≤ 255, а ISO-8859-1 - это "сырая" кодировка, которая означает, что она может кодировать каждый символ в виде одного байта.
  4. new String(..., "UTF-8"). ISO-8859-1 байты → UTF-8 байтов в порядке? Да. Поскольку исходные байты взяты из текста в кодировке UTF-8 и были извлечены как есть, они все равно представляют текст, закодированный в кодировке UTF-8.

Что касается необработанного характера ISO-8859-1, в котором каждый байт (значение от 0 до 255) отображается на символ, я копирую/вставляю ниже комментария, который я сделал к ответу @Matthieu.

Смотрите этот вопрос, касающийся понятия "сырого" кодирования с ISO-8859-1. Обратите внимание на разницу между ISO/IEC 8859-1 (определены 191 байт) и ISO-8859-1 (определены 256 байт). Вы можете найти определение ISO-8859-1 в RFC1345 и увидеть, что управляющие коды C0 и C1 отображаются на 65 неиспользуемых байтов ISO/IEC 8859-1.