我知道这有点晚了,但我刚刚发现了StreamReader
中一个令人难以置信的缺陷;使用StreamReader
时无法可靠地进行查找。就我个人而言,我的具体需求是需要读取字符,但如果满足某些条件则需要“倒退”;这是我解析某种文件格式时的副作用。
使用ReadLine()
不是一个选项,因为它只在非常简单的解析工作中有用。我需要支持可配置的记录/行分隔符序列和转义分隔符序列。此外,我不想实现自己的缓冲区,以便支持“倒退”和转义序列;这应该是StreamReader
的工作。
该方法按需计算基础字节流中的实际位置。它适用于UTF8、UTF-16LE、UTF-16BE、UTF-32LE、UTF-32BE和任何单字节编码(例如代码页1252、437、28591等),无论是否存在前导BOM。该版本不适用于UTF-7、Shift-JIS或其他可变字节编码。
当我需要在基础流中寻找任意位置时,我直接设置BaseStream.Position
,然后调用DiscardBufferedData()
以便在下一次Read()
/Peek()
调用时让StreamReader
重新同步。
友情提示:不要随意设置BaseStream.Position
。如果你分割了一个字符,你将使下一个Read()
无效,并且对于UTF-16/-32,你还将使此方法的结果无效。
public static long GetActualPosition(StreamReader reader)
{
System.Reflection.BindingFlags flags = System.Reflection.BindingFlags.DeclaredOnly | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.GetField;
char[] charBuffer = (char[])reader.GetType().InvokeMember("charBuffer", flags, null, reader, null);
int charPos = (int)reader.GetType().InvokeMember("charPos", flags, null, reader, null);
int charLen = (int)reader.GetType().InvokeMember("charLen", flags, null, reader, null);
byte[] byteBuffer = (byte[])reader.GetType().InvokeMember("byteBuffer", flags, null, reader, null);
int byteLen = (int)reader.GetType().InvokeMember("byteLen", flags, null, reader, null);
int numBytesLeft = reader.CurrentEncoding.GetByteCount(charBuffer, charPos, charLen - charPos);
int numFragments = 0;
if (byteLen > 0 && !reader.CurrentEncoding.IsSingleByte)
{
if (reader.CurrentEncoding.CodePage == 65001)
{
byte byteCountMask = 0;
while ((byteBuffer[byteLen - numFragments - 1] >> 6) == 2)
byteCountMask |= (byte)(1 << ++numFragments);
if ((byteBuffer[byteLen - numFragments - 1] >> 6) == 3)
byteCountMask |= (byte)(1 << ++numFragments);
if (numFragments > 1 && ((byteBuffer[byteLen - numFragments] >> 7 - numFragments) == byteCountMask))
numFragments = 0;
}
else if (reader.CurrentEncoding.CodePage == 1200)
{
if (byteBuffer[byteLen - 1] >= 0xd8)
numFragments = 2;
}
else if (reader.CurrentEncoding.CodePage == 1201)
{
if (byteBuffer[byteLen - 2] >= 0xd8)
numFragments = 2;
}
}
return reader.BaseStream.Position - numBytesLeft - numFragments;
}
当然,这种方法使用反射来访问私有变量,因此存在风险。但是,此方法适用于.NET 2.0、3.0、3.5、4.0、4.0.3、4.5、4.5.1、4.5.2、4.6和4.6.1。除了风险之外,另一个关键的假设是底层字节缓冲区是
byte[1024]
;如果Microsoft以错误的方式更改它,则该方法对于UTF-16/-32会出现问题。
已针对填充有
Ažテ
(10个字节:
0x41 C5 BE E3 83 86 F0 A3 98 BA
)的UTF-8文件和填充有
A
(6个字节:
0x41 00 01 D8 37 DC
)的UTF-16文件进行了测试。重点是强制沿着
byte[1024]
边界分裂字符,以所有不同的方式进行。
更新(2013-07-03):我修复了该方法,原来使用来自其他答案的错误代码。该版本已针对包含需要使用代理对的字符的数据进行了测试。数据被放入3个文件中,每个文件都有不同的编码;一个是UTF-8,一个是UTF-16LE,一个是UTF-16BE。
更新(2016-02):处理被切分的字符的唯一正确方法是直接解释底层字节。UTF-8可以正确处理,UTF-16/-32也可以(根据byteBuffer的长度)。
System.MissingFieldException已被抛出。无法找到变量charBuffer
。我正在使用标准的StreamReader实例,例如var s = new StreamReader("blah.txt");
,但是出现了这个错误。你知道是什么问题吗? - YeehawThrowOnInvalidBytes
现在被替换为将new DecoderExceptionFallback()
传递给Encoding.GetEncoding(...)
。并且您必须更新反射名称,它们现在在前面加上'_'。 - huancz