.NET C# - 文本文件的随机访问-没有简单的方法?

21

我有一个文本文件,里面包含几个“记录”。每个记录都包含一个名称和一组数字数据。

我想建立一个类,读取文件,仅显示所有记录的名称,并允许用户选择需要的记录数据。

第一次遍历文件时,我只读取标题名称,但我可以跟踪标题在文件中的“位置”。当用户请求后,我需要随机访问文本文件以查找每个记录的开头。

我必须这样做,因为该文件太大,无法完全读入内存(1GB+),而应用程序还有其他内存需求。

我已经尝试使用.NET StreamReader类来实现这一点(它提供了非常易于使用的“ReadLine”功能),但是由于该类使用缓冲区,因此无法捕获文件的真正位置(BaseStream属性的位置不准确)。

.NET中没有简单的方法吗?

10个回答

13

虽然有一些不错的答案,但是我没有找到适用于我这个非常简单情况的源代码。以下是源代码,希望能帮助其他花费时间搜索的人。

我所说的"非常简单情况"是:文本编码为定宽格式,并且行结束字符在整个文件中都相同。这段代码在我的情况下(我正在解析日志文件,有时需要跳转文件,然后回来),运作良好。我只实现了必要的部分(例如:仅一个构造函数,仅重写ReadLine()方法),所以您很可能需要添加代码...但我认为这是个合理的起点。

public class PositionableStreamReader : StreamReader
{
    public PositionableStreamReader(string path)
        :base(path)
        {}

    private int myLineEndingCharacterLength = Environment.NewLine.Length;
    public int LineEndingCharacterLength
    {
        get { return myLineEndingCharacterLength; }
        set { myLineEndingCharacterLength = value; }
    }

    public override string ReadLine()
    {
        string line = base.ReadLine();
        if (null != line)
            myStreamPosition += line.Length + myLineEndingCharacterLength;
        return line;
    }

    private long myStreamPosition = 0;
    public long Position
    {
        get { return myStreamPosition; }
        set
        {
            myStreamPosition = value;
            this.BaseStream.Position = value;
            this.DiscardBufferedData();
        }
    }
}

以下是如何使用 PositionableStreamReader 的示例:

PositionableStreamReader sr = new PositionableStreamReader("somepath.txt");

// read some lines
while (something)
    sr.ReadLine();

// bookmark the current position
long streamPosition = sr.Position;

// read some lines
while (something)
    sr.ReadLine();

// go back to the bookmarked position
sr.Position = streamPosition;

// read some lines
while (something)
    sr.ReadLine();

8

FileStream有seek()方法。


当我们不知道在哪里寻找时,这就毫无用处了。 - Jon Skeet
1
也许我们对随机访问的定义有所不同。我(以及显然还有Jason)认为它是指具有特定字节大小的记录文件,因此记录的起始位置为(recnum-1)* recsize。 - Powerlord
更重要的是,OP建议他们可以记录单个记录开始的流索引,因此在这种情况下,知道要寻找的位置是一个已解决的问题。 - Mike Burton
4
@Jon:“第一次浏览文件时,我只读取标题名称,但我可以追踪标题所在的文件‘位置’。我需要随机访问文本文件,在用户要求之后定位到每个记录的开头。” 听起来我们知道该到哪里去寻找了。 - LeppyR64
1
由于类使用的缓冲区,BaseStream 属性中的位置出现了偏差。听起来好像我们不知道要寻找哪里。 - Kcats
这很老旧,但你可以使用文件流手动循环遍历行,甚至将文件流传递给流读取器并使用读取器遍历行,但使用文件流获取偏移量(stream.Position)。然后稍后您可以使用文件流进行查找,然后使用读取器读取这些行。诀窍是同时使用两者。在查找后可能需要创建新的读取器,不确定其行为。 - mhand

6

您可以使用System.IO.FileStream代替StreamReader。如果您确切地知道文件包含的内容(例如编码),则可以像使用StreamReader一样执行所有操作。


5

如果您对数据文件的编写方式灵活,并且不介意它不太适合文本编辑器,您可以使用BinaryWriter编写记录:

using (BinaryWriter writer = 
    new BinaryWriter(File.Open("data.txt", FileMode.Create)))
{
    writer.Write("one,1,1,1,1");
    writer.Write("two,2,2,2,2");
    writer.Write("three,3,3,3,3");
}

那么,最初读取每个记录很简单,因为您可以使用BinaryReader的ReadString方法:

using (BinaryReader reader = new BinaryReader(File.OpenRead("data.txt")))
{
    string line = null;
    long position = reader.BaseStream.Position;
    while (reader.PeekChar() > -1)
    {
        line = reader.ReadString();

        //parse the name out of the line here...

        Console.WriteLine("{0},{1}", position, line);
        position = reader.BaseStream.Position;
    }
}

BinaryReader没有缓冲,因此您可以获取正确的位置以便稍后存储和使用。唯一麻烦的是从行中解析名称,这可能需要使用StreamReader。


如果您完全控制文件,也可以在文件开头编写索引。 - mhand

2

编码是固定大小的吗(例如ASCII或UCS-2)?如果是这样,您可以跟踪字符索引(基于您看到的字符数),并根据此找到二进制索引。

否则,不行 - 你基本上需要编写自己的StreamReader实现,让你可以窥视二进制索引。我同意StreamReader没有实现这一点,这很遗憾。


1

以下是一些可能会感兴趣的内容。

1)如果行的长度是固定的字符集大小不同(如UTF-8),那么这并不是必要的有用信息。所以请检查您的字符集。

2)您可以通过使用BaseStream.Position值从StreamReader确定文件光标的确切位置,但前提是您首先Flush()缓冲区(这将强制当前位置在下一个读取开始的地方——在上次读取的最后一个字节之后的一个字节)。

3)如果您事先知道每个记录的确切长度将是相同数量的字符,并且字符集使用固定宽度字符(因此每行具有相同数量的字节长),则可以使用FileStream与固定缓冲区大小匹配一行的大小,每次读取结束时光标的位置将自然而然地成为下一行的开头。

4)如果行的长度相同(假设这里是按字节计算),那么是否有任何特殊原因不直接使用行号并根据行大小x行号计算文件中的字节偏移量?


1

1

从.NET 6开始,System.IO.RandomAccess类中的方法是随机读写文件的官方和支持方式。这些API与Microsoft.Win32.SafeHandles.SafeFileHandle一起使用,可以通过新的System.IO.File.OpenHandle函数获得,该函数也在.NET 6中引入。


0

你确定文件真的“太大”了吗?你试过这种方式吗?它会引起问题吗?

如果你分配了大量的内存,而现在又没有使用它,Windows 将把它交换到磁盘上。因此,通过从“内存”中访问它,你将实现你想要的 -- 对磁盘上的文件进行随机访问。


2
如果文件大小超过1GB,并且您正在32位系统上运行,即使Windows不停地交换内存,您也很可能会耗尽地址空间。 - Roger Lipscombe

0

这个问题在2006年曾经在这里提出过:http://www.devnewsgroups.net/group/microsoft.public.dotnet.framework/topic40275.aspx

总结:

“问题在于StreamReader缓存数据,因此BaseStream.Position属性返回的值总是超过实际处理的行。”

然而,“如果文件采用固定宽度的文本编码,您可以跟踪已读取的文本量并将其乘以宽度”

如果不是这种情况,您可以使用FileStream,每次读取一个字符,然后BaseStream.Position属性应该是正确的。


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