Я понимаю, что это действительно запоздало, но я наткнулся на этот невероятный недостаток в StreamReader
сам; тот факт, что вы не можете надежно искать при использовании StreamReader
. Лично моя конкретная потребность в том, чтобы у меня была возможность читать символы, но затем выполнять «резервное копирование», если выполняется определенное условие; это побочный эффект одного из форматов файлов, которые я анализирую.
Использование ReadLine()
не вариант, потому что он полезен только в действительно тривиальных задачах разбора. Я должен поддерживать настраиваемые последовательности записей / разделителей строк и поддерживать escape-последовательности. Кроме того, я не хочу реализовывать свой собственный буфер, чтобы поддерживать «резервное копирование» и escape-последовательности; это должно быть работой StreamReader
.
Этот метод вычисляет фактическую позицию в базовом потоке байтов по требованию. Он работает для UTF8, UTF-16LE, UTF-16BE, UTF-32LE, UTF-32BE и любого однобайтового кодирования (например, кодовых страниц 1252, 437, 28591 и т. Д.), Независимо от наличия преамбулы / спецификации. Эта версия не будет работать для UTF-7, Shift-JIS или других кодировок с переменными байтами.
Когда мне нужно найти произвольную позицию в базовом потоке, я непосредственно устанавливаю BaseStream.Position
, а затем вызываю DiscardBufferedData()
, чтобы вернуть StreamReader
в синхронизацию для следующего вызова Read()
/ Peek()
.
И дружеское напоминание: не устанавливайте произвольно 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;
// The current buffer of decoded characters
char[] charBuffer = (char[])reader.GetType().InvokeMember("charBuffer", flags, null, reader, null);
// The index of the next char to be read from charBuffer
int charPos = (int)reader.GetType().InvokeMember("charPos", flags, null, reader, null);
// The number of decoded chars presently used in charBuffer
int charLen = (int)reader.GetType().InvokeMember("charLen", flags, null, reader, null);
// The current buffer of read bytes (byteBuffer.Length = 1024; this is critical).
byte[] byteBuffer = (byte[])reader.GetType().InvokeMember("byteBuffer", flags, null, reader, null);
// The number of bytes read while advancing reader.BaseStream.Position to (re)fill charBuffer
int byteLen = (int)reader.GetType().InvokeMember("byteLen", flags, null, reader, null);
// The number of bytes the remaining chars use in the original encoding.
int numBytesLeft = reader.CurrentEncoding.GetByteCount(charBuffer, charPos, charLen - charPos);
// For variable-byte encodings, deal with partial chars at the end of the buffer
int numFragments = 0;
if (byteLen > 0 && !reader.CurrentEncoding.IsSingleByte)
{
if (reader.CurrentEncoding.CodePage == 65001) // UTF-8
{
byte byteCountMask = 0;
while ((byteBuffer[byteLen - numFragments - 1] >> 6) == 2) // if the byte is "10xx xxxx", it's a continuation-byte
byteCountMask |= (byte)(1 << ++numFragments); // count bytes & build the "complete char" mask
if ((byteBuffer[byteLen - numFragments - 1] >> 6) == 3) // if the byte is "11xx xxxx", it starts a multi-byte char.
byteCountMask |= (byte)(1 << ++numFragments); // count bytes & build the "complete char" mask
// see if we found as many bytes as the leading-byte says to expect
if (numFragments > 1 && ((byteBuffer[byteLen - numFragments] >> 7 - numFragments) == byteCountMask))
numFragments = 0; // no partial-char in the byte-buffer to account for
}
else if (reader.CurrentEncoding.CodePage == 1200) // UTF-16LE
{
if (byteBuffer[byteLen - 1] >= 0xd8) // high-surrogate
numFragments = 2; // account for the partial character
}
else if (reader.CurrentEncoding.CodePage == 1201) // UTF-16BE
{
if (byteBuffer[byteLen - 2] >= 0xd8) // high-surrogate
numFragments = 2; // account for the partial character
}
}
return reader.BaseStream.Position - numBytesLeft - numFragments;
}
Конечно, это использует Reflection, чтобы получить частные переменные, так что здесь есть риск. Однако этот метод работает с .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.
Это было проверено в отношении файла UTF-8, заполненного Ažテ?
(10 байт: 0x41 C5 BE E3 83 86 F0 A3 98 BA
), и файла UTF-16, заполненного A?
(6 байтов: 0x41 00 01 D8 37 DC
). Смысл в том, чтобы принудительно фрагментировать символы вдоль границ byte[1024]
, разными способами, какими они могли бы быть.
ОБНОВЛЕНИЕ (2013-07-03) : Я исправил метод, который первоначально использовал неработающий код из этого другого ответа. Эта версия была протестирована с данными, содержащими символы, требующие использования суррогатных пар. Данные были помещены в 3 файла, каждый с различной кодировкой; один UTF-8, один UTF-16LE и один UTF-16BE.
ОБНОВЛЕНИЕ (2016-02) : Единственный правильный способ обработки пополам символов - это непосредственная интерпретация нижележащих байтов. UTF-8 правильно обрабатывается, а UTF-16 / -32 работает (учитывая длину byteBuffer).