如何使用RandomAccessFile读取UTF8编码的文件?

时间:2012-04-01 13:52:24

标签: java unicode utf-8 io textfield

我有用UTF8编码的文本文件(用于特定于语言的字符)。 我需要使用RandomAccessFile来寻找特定的位置并从中读取。

我想逐行阅读。

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

8 个答案:

答案 0 :(得分:17)

您可以使用以下代码将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: Привет из Украины

答案 1 :(得分:4)

API文档为readUTF8

说了以下内容
  

从此文件中读取字符串。字符串已使用a编码   修改了UTF-8格式。

     

从当前文件指针开始读取前两个字节,   好像是通过readUnsignedShort。该值给出了以下数量   编码字符串中的字节数,而不是结果的长度   串。然后将以下字节解释为字节编码   修改后的UTF-8格式的字符并转换为   字符。

     

此方法将阻塞,直到读取所有字节为止   被检测到,或者抛出异常。

你的字符串是否以这种方式格式化?

这似乎可以解释你的EOF除外。

您的文件是文本文件,因此您的实际问题是解码。

我知道的最简单的答案是:

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"是UTF-8中的5个字节,而"Obiwan\n"是UTF-8中的7个字节。并且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中是可变的,具体取决于你的字符串中的字符),如果不是所有的空间都需要你填充它:

通过这种方式,您可以轻松计算给定线的偏移量,因为它们都会占用相同的空间量。

答案 2 :(得分:4)

你不可能这样做。 seek函数将为您定位一些字节数。无法保证您与UTF-8字符边界对齐。

答案 3 :(得分:1)

我意识到这是一个古老的问题,但它似乎仍然有一些兴趣,而且没有被接受的答案。

您所描述的内容实质上是数据结构问题。这里对UTF8的讨论是一个红色的鲱鱼 - 使用固定长度编码(如ASCII)会遇到同样的问题,因为你有可变长度的行。你需要的是某种索引。

如果您绝对无法更改文件本身(“字符串文件”) - 似乎就是这种情况 - 您可以始终构建外部索引。第一次(并且第一次)访问字符串文件,您一直读取(顺序),记录每行开头的字节位置,并通过记录文件结束位置(使生活更简单)。这可以通过以下代码实现:

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

然后将这些整数写入一个单独的文件(“索引文件”),您将在以后的每个启动程序并打算访问该字符串文件时读回该文件。要访问n字符串,请从索引文件中选择nn+1索引(称为AB)。然后,您尝试在字符串文件中定位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等数据库,它可以为您创建和维护索引。这样,您可以添加和修改额外的“行”,而无需重新创建整个索引。有关Java实现,请参阅https://www.sqlite.org/cvstrac/wiki?p=SqliteWrappers

答案 4 :(得分:1)

通过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());

答案 5 :(得分:1)

RandomAccessFile的readUTF()方法将当前指针的前两个字节视为当前位置的两个字节后的字节大小,以字符串形式读取并返回。

为了使此方法有效,应使用writeUTF()方法编写内容,因为它使用当前位置之后的前两个字节来保存内容大小,然后写入内容。否则,大多数时候你会得到EOFException。

有关详细信息,请参阅StringBuilder

答案 6 :(得分:1)

一旦您位于给定的行上(这意味着您已经回答了问题的第一部分,请参阅@martinjs答案),您可以阅读整行,并使用一条语句在其中加上String @Matthieu在答案中给出。但是要检查所讨论的陈述是否正确,我们必须问自己四个问题。这是不言而喻的。

请注意,如果您需要随机且快速地访问许多行,那么从一行开始就可能需要分析文本以建立索引。

读取一行并将其变成String的语句是:

String utf8 = new String(raf.readLine().getBytes("ISO-8859-1"), "UTF-8");
  1. UTF-8中的字节是什么?这意味着允许哪些值。一旦我们回答问题2,我们就会发现该问题实际上是没有用的。
  2. readLine()。 UTF-8字节→UTF-16字节好吗?是。因为如果最高指示字节(MSB)为0,则UTF-16赋予2字节编码的所有0到255的整数含义。这由readLine()保证。
  3. getBytes("ISO-8859-1")。用UTF-16编码的字符(每个字符1或2个String(代码单元)的Java char)→ISO-8859-1字节可以吗?是。 Java字符串中字符的代码点≤255,ISO-8859-1是“原始”编码,这意味着它可以将每个字符编码为单个字节。
  4. new String(..., "UTF-8")。 ISO-8859-1字节→UTF-8字节好吗?是。由于原始字节来自UTF-8编码的文本,并且已按原样提取,因此它们仍代表使用UTF-8编码的文本。

关于将每个字节(值0到255)映射到一个字符的ISO-8859-1的原始性质,我将在@Matthieu对答案所作的评论下方复制/粘贴。

有关使用ISO-8859-1的“原始”编码的概念,请参见this question。请注意,ISO / IEC 8859-1(定义为191个字节)和ISO-8859-1(定义为256个字节)之间的差异。您可以在RFC1345中找到ISO-8859-1的定义,并看到控制代码C0和C1映射到ISO / IEC 8859-1的65个未使用字节中。

答案 7 :(得分:0)

我发现RandomAccessFile的API具有挑战性。

如果您的文本实际上仅限于UTF-8值0-127(UTF-8的最低7位),那么使用readLine()是安全的,但请仔细阅读这些Javadoc:这是一个奇怪的方法。引用:

  

此方法从文件开始连续读取字节,从当前文件指针开始,直到它到达行终止符或文件末尾。通过取字符的低8位的字节值并将字符的高8位设置为零,将每个字节转换为字符。因此,此方法不支持完整的Unicode字符集。

为了安全地读取UTF-8,我建议您使用length()read(byte[])的组合读取(部分或全部)原始字节。然后使用以下构造函数将您的UTF-8字节转换为Java String

要安全地编写UTF-8,首先使用new String(byte[], "UTF-8")将Java String转换为正确的字节。最后,使用someText.getBytes("UTF-8")写入字节。