如何从无限字节流中读取UTF-8字符 - C#

6

通常,要从字节流中读取字符,您可以使用StreamReader。在此示例中,我正在从无限流中读取以'\r'分隔的记录。

using(var reader = new StreamReader(stream, Encoding.UTF8))
{
    var messageBuilder = new StringBuilder();
    var nextChar = 'x';
    while (reader.Peek() >= 0)
    {
        nextChar = (char)reader.Read()
        messageBuilder.Append(nextChar);

        if (nextChar == '\r')
        {
            ProcessBuffer(messageBuilder.ToString());
            messageBuilder.Clear();
        }
    }
}

问题在于StreamReader有一个很小的内部缓冲区,因此,如果代码等待“记录结束”分隔符(在本例中是'\r'),则必须等到StreamReader的内部缓冲区被刷新(通常是因为更多字节已经到达)。
这种替代实现适用于单字节UTF-8字符,但对于多字节字符将失败。
int byteAsInt = 0;
var messageBuilder = new StringBuilder();
while ((byteAsInt = stream.ReadByte()) != -1)
{
    var nextChar = Encoding.UTF8.GetChars(new[]{(byte) byteAsInt});
    Console.Write(nextChar[0]);
    messageBuilder.Append(nextChar);

    if (nextChar[0] == '\r')
    {
        ProcessBuffer(messageBuilder.ToString());
        messageBuilder.Clear();
    }
}

如何修改这段代码,使其适用于多字节字符?


标题不应该修改为多字节或UTF-16字符,而不是UTF-8吗?看起来有误导性。 - Tim S.
1
@TimS。UTF-8字符可以超过一个字节。 - Iridium
@TimS. 你是什么意思?一个多字节UTF-8字符不会自动变成UTF-16字符。维基百科 - CodeCaster
3
UTF-8字符可以是多字节的。http://en.wikipedia.org/wiki/Utf-8 - Mike Hadlow
@MikeHadlow 啊,感谢您的纠正和信息。我没意识到 UTF-8 可以包含多字节字符。 - Tim S.
4个回答

10
与其使用专门用于转换完整缓冲区的Encoding.UTF8.GetChars方法,不如获取一个Decoder实例,并反复调用其成员方法GetChars。这将利用Decoder的内部缓冲区来处理从上一次调用结束到下一次调用开始的部分多字节序列。

谢谢Richard,这很好用。请看我的回答以了解我的实现。 - Mike Hadlow

7

多亏 Richard,我现在有一个正常工作的无限流读取器。就像他所解释的那样,诀窍是使用一个 Decoder 实例并调用它的 GetChars 方法。我已经测试了它,包括多字节的日文文本,都能正常工作。

int byteAsInt = 0;
var messageBuilder = new StringBuilder();
var decoder = Encoding.UTF8.GetDecoder();
var nextChar = new char[1];

while ((byteAsInt = stream.ReadByte()) != -1)
{
    var charCount = decoder.GetChars(new[] {(byte) byteAsInt}, 0, 1, nextChar, 0);
    if(charCount == 0) continue;

    Console.Write(nextChar[0]);
    messageBuilder.Append(nextChar);

    if (nextChar[0] == '\r')
    {
        ProcessBuffer(messageBuilder.ToString());
        messageBuilder.Clear();
    }
}

1

Mike,我发现你的解决方案也非常适合我的情况。但是我注意到有时需要四次GetChar()调用才能确定要返回的字符。这意味着 charCount 是2,而我的 nextChar 缓冲区大小为1。所以我得到了错误“输出字符缓冲区太小,无法包含编码后的字符。因此采用了 Unicode 回退 System.Text.DecoderReplacementFallback."

我把代码改成了:

    // ...
    var nextChar = new char[4];  // 2 might suffice

    for (var i = startPos; i < bytesRead; i++)
    {
        int charCount;
        //...
        charCount = decoder.GetChars(buffer, i, 1, nextChar, 0);

        if (charCount == 0)
        {
            bytesSkipped++;
            continue;
        }

        for (int ic = 0; ic < charCount; ic++)
        {
            char c = nextChar[ic];
            charPos++;

            // Process character here...
        }
    }

1

我不理解为什么你不使用流读取器的ReadLine方法。如果有一个很好的理由不这样做,然而,反复调用解码器上的GetChars方法似乎是低效的。为什么不利用'\r'的字节表示不能是多字节序列的事实呢?(多字节序列中的字节必须大于127;也就是说,它们具有最高位设置。)

var messageBuilder = new List<byte>();

int byteAsInt;
while ((byteAsInt = stream.ReadByte()) != -1)
{
    messageBuilder.Add((byte)byteAsInt);

    if (byteAsInt == '\r')
    {
        var messageString = Encoding.UTF8.GetString(messageBuilder.ToArray());
        Console.Write(messageString);
        ProcessBuffer(messageString);
        messageBuilder.Clear();
    }
}

等等,你是认真地说在解码器上调用GetChars是低效的吗?而读取流时逐字节放入字节列表中,然后从该列表构建字节数组并调用Encoding.GetString?看起来你忽略了小问题背后的大性能问题 :) ...哦,我发现OP也做了同样的事情。没关系。 - Luaan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接