[英]c# - StreamReader and seeking
您可以使用StreamReader
读取普通文本文件,然后在读取过程中保存当前位置后关闭StreamReader
,然后再次打开StreamReader
并从该位置开始读取吗?
如果不是,我还可以使用什么来完成相同的案例而不锁定文件?
我试过这个但它不起作用:
var fs = File.Open(@ "C:\testfile.txt", FileMode.Open, FileAccess.Read);
var sr = new StreamReader(fs);
Debug.WriteLine(sr.ReadLine()); //Prints:firstline
var pos = fs.Position;
while (!sr.EndOfStream)
{
Debug.WriteLine(sr.ReadLine());
}
fs.Seek(pos, SeekOrigin.Begin);
Debug.WriteLine(sr.ReadLine());
//Prints Nothing, i expect it to print SecondLine.
这是我也尝试过的其他代码:
var position = -1;
StreamReaderSE sr = new StreamReaderSE(@ "c:\testfile.txt");
Debug.WriteLine(sr.ReadLine());
position = sr.BytesRead;
Debug.WriteLine(sr.ReadLine());
Debug.WriteLine(sr.ReadLine());
Debug.WriteLine(sr.ReadLine());
Debug.WriteLine(sr.ReadLine());
Debug.WriteLine("Wait");
sr.BaseStream.Seek(position, SeekOrigin.Begin);
Debug.WriteLine(sr.ReadLine());
我意识到这真的很晚,但我自己偶然发现了StreamReader
这个令人难以置信的缺陷; 使用StreamReader
时无法可靠地查找的事实。 就我个人而言,我的具体需求是具有读取字符的能力,但如果满足某个条件则“备份”; 这是我正在解析的一种文件格式的副作用。
使用ReadLine()
不是一个选项,因为它只在真正琐碎的解析作业中有用。 我必须支持可配置的记录/行分隔符序列并支持转义分隔符序列。 另外,我不想实现自己的缓冲区,所以我可以支持“备份”和转义序列; 那应该是StreamReader
的工作。
此方法按需计算底层字节流中的实际位置。 它适用于 UTF8、UTF-16LE、UTF-16BE、UTF-32LE、UTF-32BE 和任何单字节编码(例如代码页 1252、437、28591 等),无论是否存在序言/BOM。 此版本不适用于 UTF-7、Shift-JIS 或其他可变字节编码。
当我需要寻找底层流中的任意位置时,我直接设置BaseStream.Position
然后调用DiscardBufferedData()
以使StreamReader
重新同步以进行下一个Read()
/ Peek()
调用。
并BaseStream.Position
提醒:不要随意设置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;
}
当然,这使用反射来获取私有变量,因此存在风险。 但是,此方法适用于 .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 文件进行A𐐷
. 重点是沿着byte[1024]
边界强制分割字符,它们可能是所有不同的方式。
更新(2013-07-03) :我修复了该方法,该方法最初使用的是其他答案中的损坏代码。 此版本已针对包含需要使用代理对的字符的数据进行了测试。 数据被放入 3 个文件中,每个文件都有不同的编码; 一种 UTF-8、一种 UTF-16LE 和一种 UTF-16BE。
更新(2016-02) :处理二等分字符的唯一正确方法是直接解释底层字节。 正确处理 UTF-8,并且 UTF-16/-32 工作(考虑到 byteBuffer 的长度)。
是的,你可以,看这个:
var sr = new StreamReader("test.txt");
sr.BaseStream.Seek(2, SeekOrigin.Begin); // Check sr.BaseStream.CanSeek first
更新:请注意,您不一定可以将sr.BaseStream.Position
用于任何有用的东西,因为StreamReader
使用缓冲区,因此它不会反映您实际阅读的内容。 我猜你在找到真正的位置时会遇到问题。 因为您不能只计算字符(不同的编码以及字符长度)。 我认为最好的方法是使用FileStream
本身。
更新:从这里使用TGREER.myStreamReader
: http : TGREER.myStreamReader
这个类添加了BytesRead
等(适用于ReadLine()
但显然不适用于其他读取方法)和那么你可以这样做:
File.WriteAllText("test.txt", "1234\n56789");
long position = -1;
using (var sr = new myStreamReader("test.txt"))
{
Console.WriteLine(sr.ReadLine());
position = sr.BytesRead;
}
Console.WriteLine("Wait");
using (var sr = new myStreamReader("test.txt"))
{
sr.BaseStream.Seek(position, SeekOrigin.Begin);
Console.WriteLine(sr.ReadToEnd());
}
如果您只想搜索文本流中的开始位置,我将这个扩展添加到 StreamReader 以便我可以确定应该在哪里编辑流。 当然,这是基于字符作为逻辑的递增方面,但就我的目的而言,它非常有效,用于根据字符串模式获取基于文本/ASCII 的文件中的位置。 然后,您可以使用该位置作为读取的起点,编写一个新文件,该文件排除了起点之前的数据。
流中返回的位置可以提供给 Seek,以从基于文本的流读取中的该位置开始。 有用。 我已经测试过了。 但是,在匹配算法期间匹配到非 ASCII Unicode 字符时可能会出现问题。 这是基于美式英语和相关的字符页面。
基础知识:它逐个字符地扫描文本流,仅通过流向前查找序列字符串模式(与字符串参数匹配)。 一旦模式与字符串参数不匹配(即前进,逐个字符),它将重新开始(从当前位置)尝试获得一个逐个字符的匹配项。 如果在流中找不到匹配项,它将最终退出。 如果找到匹配项,则它返回流中当前的“字符”位置,而不是 StreamReader.BaseStream.Position,因为该位置在前面,基于 StreamReader 所做的缓冲。
如注释中所述,此方法将影响 StreamReader 的位置,并将在方法结束时将其设置回开头 (0)。 StreamReader.BaseStream.Seek 应该用于运行到此扩展返回的位置。
注意:此扩展返回的位置也适用于 BinaryReader.Seek 作为处理文本文件时的起始位置。 在丢弃 PJL 标头信息以使文件成为可以被 GhostScript 使用的“正确”PostScript 可读文件之后,我实际上为此目的使用了此逻辑将 PostScript 文件重写回磁盘。 :)
要在 PostScript 中搜索的字符串(在 PJL 标头之后)是:“%!PS-”,后跟“Adobe”和版本。
public static class StreamReaderExtension
{
/// <summary>
/// Searches from the beginning of the stream for the indicated
/// <paramref name="pattern"/>. Once found, returns the position within the stream
/// that the pattern begins at.
/// </summary>
/// <param name="pattern">The <c>string</c> pattern to search for in the stream.</param>
/// <returns>If <paramref name="pattern"/> is found in the stream, then the start position
/// within the stream of the pattern; otherwise, -1.</returns>
/// <remarks>Please note: this method will change the current stream position of this instance of
/// <see cref="System.IO.StreamReader"/>. When it completes, the position of the reader will
/// be set to 0.</remarks>
public static long FindSeekPosition(this StreamReader reader, string pattern)
{
if (!string.IsNullOrEmpty(pattern) && reader.BaseStream.CanSeek)
{
try
{
reader.BaseStream.Position = 0;
reader.DiscardBufferedData();
StringBuilder buff = new StringBuilder();
long start = 0;
long charCount = 0;
List<char> matches = new List<char>(pattern.ToCharArray());
bool startFound = false;
while (!reader.EndOfStream)
{
char chr = (char)reader.Read();
if (chr == matches[0] && !startFound)
{
startFound = true;
start = charCount;
}
if (startFound && matches.Contains(chr))
{
buff.Append(chr);
if (buff.Length == pattern.Length
&& buff.ToString() == pattern)
{
return start;
}
bool reset = false;
if (buff.Length > pattern.Length)
{
reset = true;
}
else
{
string subStr = pattern.Substring(0, buff.Length);
if (buff.ToString() != subStr)
{
reset = true;
}
}
if (reset)
{
buff.Length = 0;
startFound = false;
start = 0;
}
}
charCount++;
}
}
finally
{
reader.BaseStream.Position = 0;
reader.DiscardBufferedData();
}
}
return -1;
}
}
FileStream.Position(或等效的 StreamReader.BaseStream.Position)通常会在 TextReader 位置之前 - 可能远远领先 - 因为底层缓冲发生。
如果您可以确定如何处理文本文件中的换行符,则可以根据行长度和行尾字符将读取的字节数相加。
File.WriteAllText("test.txt", "1234" + System.Environment.NewLine + "56789");
long position = -1;
long bytesRead = 0;
int newLineBytes = System.Environment.NewLine.Length;
using (var sr = new StreamReader("test.txt"))
{
string line = sr.ReadLine();
bytesRead += line.Length + newLineBytes;
Console.WriteLine(line);
position = bytesRead;
}
Console.WriteLine("Wait");
using (var sr = new StreamReader("test.txt"))
{
sr.BaseStream.Seek(position, SeekOrigin.Begin);
Console.WriteLine(sr.ReadToEnd());
}
对于更复杂的文本文件编码,您可能需要比这更有趣,但它对我有用。
来自 MSDN:
StreamReader 设计用于特定编码中的字符输入,而 Stream 类设计用于字节输入和输出。 使用 StreamReader 从标准文本文件中读取信息行。
在大多数涉及StreamReader
的示例中,您将看到使用 ReadLine() 逐行读取。 Seek 方法来自Stream
类,它基本上用于以字节为单位读取或处理数据。
我发现上面的建议对我不起作用——我的用例是只需要备份一个读取位置(我使用默认编码一次读取一个字符)。 我的简单解决方案受到上述评论的启发......你的里程可能会有所不同......
我在读取之前保存了 BaseStream.Position,然后确定是否需要备份...如果是,则设置位置并调用 DiscardBufferedData()。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.