【问题标题】:Reading a single UTF-8 character with RandomAccessFile使用 RandomAccessFile 读取单个 UTF-8 字符
【发布时间】:2017-02-17 23:24:01
【问题描述】:

我已经设置了一个顺序扫描器,其中指向我的文件的 RandomAccessFile 能够通过以下方法读取单个字符:

public char nextChar() {
    try {
        seekPointer++;
        int i = source.read();
        return i > -1 ? (char) i : '\0'; // INFO: EOF character is -1.
    } catch (IOException e) {
        e.printStackTrace();
    }
    return '\0';
}

seekPointer 只是我的程序的参考,但该方法将source.read() 存储在int 中,然后如果它不是文件末尾,则将其返回为char。但是我收到的这些字符是 ASCII 格式的,实际上它太糟糕了,我什至不能使用诸如 ç 之类的符号。

有没有一种方法可以让我接收 单个 字符,即 UTF-8 格式或至少一些标准化的字符,而不仅仅是 ASCII 字符集?

我知道我可以使用readUTF(),但这会将整行作为字符串返回,这不是我想要的。

另外,我不能简单地使用另一个流阅读器,因为我的程序需要一个seek(int) 函数,允许我在文件中来回移动。

【问题讨论】:

  • 输入流阅读器?
  • @TamasHegedus 更新了问题。我需要一个搜索功能。
  • 正如@WillisBlackburn 在下面的详细回答中指出的那样,您不能在 UTF-8 文件中选择随机字节偏移量并保证获得“字符”。您可能需要备份才能找到多字节序列的开始。这是你的想法吗?
  • @JimGarrison 好吧,我正在尝试根据他的答案制作一个算法,但效果不是很好。所以不,不是我的想法,更多的是亚当的回答。我只是在看看目前有效的方法。
  • 那是因为你需要使用String(byte[] bytes, Charset c)构造函数并指定UTF-8。否则它将假定您的平台默认字符集。

标签: java utf-8 randomaccessfile


【解决方案1】:

根据 Willis Blackburn 的回答,我可以简单地进行一些整数检查以确保它们超过某个数字,以获得我需要提前检查的字符数量。

从下表判断:

first byte starts with 0                         1 byte char
first byte starts with 10    >= 128 && <= 191    ? byte(s) char
first byte starts with 11        >= 192          2 bytes char
first byte starts with 111       >= 224          3 bytes char
first byte starts with 1111      >= 240          4 bytes char

我们可以检查从RandomAccessFile.read() 读取的整数,方法是将其与中间列中的数字进行比较,这些数字实际上只是一个字节的整数表示。这让我们可以完全跳过字节转换,节省时间。

以下代码将从 RandomAccessFile 中读取一个字符,字节长度为 1-4:

int seekPointer = 0;
RandomAccessFile source; // initialise in your own way

public void seek(int shift) {
    seekPointer += shift;
    if (seekPointer < 0) seekPointer = 0;
    try {
        source.seek(seekPointer);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

private int byteCheck(int chr) {
    if (chr == -1) return 1; // eof
    int i = 1; // theres always atleast one byte
    if (chr >= 192) i++; // 2 bytes
    if (chr >= 224) i++; // 3 bytes
    if (chr >= 240) i++; // 4 bytes
    if (chr >= 128 && chr <= 191) i = -1; // woops, we're halfway through a char!
    return i;
}

public char nextChar() {
    try {
        seekPointer++;
        int i = source.read();

        if (byteCheck(i) == -1) {
            boolean malformed = true;
            for (int k = 0; k < 4; k++) { // Iterate 3 times.
                // we only iterate 3 times because the maximum size of a utf-8 char is 4 bytes.
                // any further and we may possibly interrupt the other chars.
                seek(-1);
                i = source.read();
                if (byteCheck(i) != -1) {
                    malformed = false;
                    break;
                }
            }
            if (malformed) {
                seek(3);
                throw new UTFDataFormatException("Malformed UTF char at position: " + seekPointer);
            }
        }

        byte[] chrs = new byte[byteCheck(i)];
        chrs[0] = (byte) i;

        for (int j = 1; j < chrs.length; j++) {
            seekPointer++;
            chrs[j] = (byte) source.read();
        }

        return i > -1 ? new String(chrs, Charset.forName("UTF-8")).charAt(0) : '\0'; // EOF character is -1.
    } catch (IOException e) {
        e.printStackTrace();
    }
    return '\0';
}

【讨论】:

  • 这可能是正确的。如果字节以 10 开头(换句话说 >= 128),您应该决定要做什么。在这种情况下,您正在查看字符中间的一个字节,应该备份或向前读取,直到找到一个起始字节。
  • @WillisBlackburn 按照我设计程序的方式,我实际上并不需要它,但它会是一个很好的学习曲线,所以我现在就去做!
  • @WillisBlackburn 已经有了。你得到了一些反对意见。不过我也会接受你的回答,因为没有它我会被卡住。非常感谢。
  • 欣赏它。这都是关于积分的。 :-) 我希望反对的选民发表评论以解释原因。
  • 我很高兴回答您的问题,因为 UTF-8 是一种非常优雅的字符编码解决方案,解释它的工作原理很有趣。它可以直接读取ASCII,对ASCII集合中的字符进行编码与ASCII一样高效,并且阅读器可以区分多字节字符中的初始字节和后续字节。据说是 Ken Thompson 在新泽西一家餐馆的餐垫上设计的。
【解决方案2】:

我不完全确定你想做什么,但让我给你一些可能有帮助的信息。

UTF-8 编码将字符表示为 1、2、3 或 4 个字节,具体取决于字符的 Unicode 值。

  • 对于字符 0x00-0x7F,UTF-8 将字符编码为单个字节。这是一个非常有用的属性,因为如果您只处理 7 位 ASCII 字符,则 UTF-8 和 ASCII 编码是相同的。
  • 对于字符 0x80-0x7FF,UTF-8 使用 2 个字节:第一个字节是二进制 110,后跟字符的 5 个高位,而第二个字节是二进制 10,后跟字符的 6 个低位。
  • 3 字节和 4 字节编码与 2 字节编码类似,只是 3 字节编码的第一个字节以 1110 开头,而 4 字节编码的第一个字节以 11110 开头。李>
  • 请参阅 Wikipedia 了解所有详细信息。

现在这可能看起来很拜占庭式,但它的结果是:您可以读取 UTF-8 文件中的 任何 字节,并知道您是否正在查看独立字符,即第一个字节多字节字符,或多字节字符的其他字节之一。

如果您读取的字节以二进制 0 开头,那么您正在查看的是单字节字符。如果它以 110、1110 或 11110 开头,那么您分别拥有 2、3 或 4 个字节的多字节字符的第一个字节。如果它以 10 开头,那么它是多字节字符的后续字节之一;向后扫描以找到它的开头。

因此,如果您想让您的调用者查找文件中的任何随机位置并在那里读取 UTF-8 字符,您可以应用上面的算法来查找该字符的第一个字节(如果它不是指定位置),然后读取并解码该值。

有关从源字节解码 UTF-8 的方法,请参阅 Java Charset 类。可能有更简单的方法,但 Charset 会起作用。

更新:此代码应处理 1 字节和 2 字节 UTF-8 情况。根本没有测试,YMMV。

for (;;) {
    int b = source.read();
    // Single byte character starting with binary 0.
    if ((b & 0x80) == 0)
        return (char) b;
    // 2-byte character starting with binary 110.
    if ((b & 0xE0) == 0xC0)
        return (char) ((b & 0x1F) << 6 | source.read() & 0x3F);
    // 3 and 4 byte encodings left as an exercise...
    // 2nd, 3rd, or 4th byte of a multibyte char starting with 10. 
    // Back up and loop.
    if ((b & 0xC0) == 0xF0) 
        source.seek(source.getFilePosition() - 2);
}

我不会打扰 seekPointer。 RandomAccessFile 知道它是什么;只需在需要时调用 getFilePosition。

【讨论】:

  • 你能举个例子吗?我正在尝试使用字节检查制作一个“算法”,但它不会去任何地方......
  • 看来我已经成功创建了一个算法,我将做一些检查,看看它是否完全有效。
  • 是的,seekPointer 用于我正在使用的其他东西,我只包含它是因为我在方法中使用了它。我用它在文件的字符和行之间寻找,所以我可以参考 where 字符实际在文件行/位置中的位置。
  • 您声明“如果它以 10 开头,则它是后续字节”。所以用 int 术语来说,“如果它 >= 128,它是一个后续字节”。 但是,可以像第一个字节一样以110、1110、1111开头吗?
  • 你是对的——我想象的逻辑看起来像“如果 >= 240 则执行 4 字节的操作,否则 if >= 224 执行 3 字节的操作,否则如果 > = 192 做 2 字节的事情,否则如果 >= 128 那么它是一个中间字节,否则它是一个单字节字符。”
【解决方案3】:

java.io.DataInputStream.readUTF(DataInput) 中的案例陈述中,您可以得出类似的内容

public static char readUtf8Char(final DataInput dataInput) throws IOException {
    int char1, char2, char3;

    char1 = dataInput.readByte() & 0xff;
    switch (char1 >> 4) {
        case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
            /* 0xxxxxxx*/
            return (char)char1;
        case 12: case 13:
            /* 110x xxxx   10xx xxxx*/
            char2 = dataInput.readByte() & 0xff;
            if ((char2 & 0xC0) != 0x80) {
                throw new UTFDataFormatException("malformed input");
            }
            return (char)(((char1 & 0x1F) << 6) | (char2 & 0x3F));
        case 14:
            /* 1110 xxxx  10xx xxxx  10xx xxxx */
            char2 = dataInput.readByte() & 0xff;
            char3 = dataInput.readByte() & 0xff;
            if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80)) {
                throw new UTFDataFormatException("malformed input");
            }
            return (char)(((char1 & 0x0F) << 12) | ((char2 & 0x3F) << 6) | ((char3 & 0x3F) << 0));
        default:
            /* 10xx xxxx,  1111 xxxx */
            throw new UTFDataFormatException("malformed input");
    }
}

请注意,RandomAccessFile 实现了DataInput,因此您可以将其传递给上述方法。在为第一个字符调用它之前,您需要读取一个表示 UTF 字符串长度的无符号 short。

请注意,此处使用的编码是 modified-UTF-8,如 DataInput 的 Javadoc 中所述。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-06-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多