使用FileStream.Seek

3

我正在尝试使用FileStream.Seek快速跳转到一行并读取它。

然而,我没有得到正确的结果。我已经试着看了一段时间,但是不明白我做错了什么。

环境:
操作系统:Windows 7
框架:.NET 4.0
IDE:Visual C# Express 2010

文件位置中的示例数据:C:\Temp\Temp.txt

0001|100!2500
0002|100!2500
0003|100!2500
0004|100!2500
0005|100!2500
0006|100!2500
0007|100!2500
0008|100!2500
0009|100!2500
0010|100!2500

代码:

class PaddedFileSearch
{
    private int LineLength { get; set; }
    private string FileName { get; set; }

    public PaddedFileSearch()
    {
        FileName = @"C:\Temp\Temp.txt";     // This is a padded file.  All lines are of the same length.

        FindLineLength();
        Debug.Print("File Line length: {0}", LineLength);

        // TODO: This purely for testing.  Move this code out.
        SeekMethod(new int[] { 5, 3, 4 });
        /*  Expected Results:
         *  Line No     Position        Line
         *  -------     --------        -----------------
         *  3           30              0003|100!2500
         *  4           15              0004|100!2500
         *  5           15              0005|100!2500 -- This was updated after the initial request.
         */

        /* THIS DOES NOT GIVE THE EXPECTED RESULTS */
        SeekMethod(new int[] { 5, 3 });
        /*  Expected Results:
         *  Line No     Position        Line
         *  -------     --------        -----------------
         *  3           30              0003|100!2500
         *  5           30              0005|100!2500
         */
    }

    private void FindLineLength()
    {
        string line;

        // Add check for FileExists

        using (StreamReader reader = new StreamReader(FileName))
        {
            if ((line = reader.ReadLine()) != null)
            {
                LineLength = line.Length + 2;
                // The 2 is for NewLine(\r\n)
            }
        }

    }

    public void SeekMethod(int[] lineNos)
    {
        long position = 0;
        string line = null;

        Array.Sort(lineNos);

        Debug.Print("");
        Debug.Print("Line No\t\tPosition\t\tLine");
        Debug.Print("-------\t\t--------\t\t-----------------");

        using (FileStream fs = new FileStream(FileName, FileMode.Open, FileAccess.Read, FileShare.None))
        {
            using (StreamReader reader = new StreamReader(fs))
            {
                foreach (int lineNo in lineNos)
                {
                    position = (lineNo - 1) * LineLength - position;
                    fs.Seek(position, SeekOrigin.Current);

                    if ((line = reader.ReadLine()) != null)
                    {
                        Debug.Print("{0}\t\t\t{1}\t\t\t\t{2}", lineNo, position, line);
                    }
                }
            }
        }
    }
}
我得到的输出:
文件行长度:15
行号 位置 行 ------- -------- ----------------- 3 30 0003|100!2500 4 15 0004|100!2500 5 45 0005|100!2500
行号 位置 行 ------- -------- ----------------- 3 30 0003|100!2500 5 30 0004|100!2500
我的问题是以下输出:
行号       位置          行
-------     --------        -----------------
5           30              0004|100!2500

行的输出应该是:0005|100!2500

我不明白为什么会出现这种情况。

我做错了什么吗? 有没有解决方法? 另外,是否有更快的方法可以使用类似于seek的东西来完成?
(我正在寻找基于代码的选项,而不是Oracle或SQL Server。为了论证的目的,让我们也说文件大小为1 GB。)

非常感谢任何帮助。

谢谢。

更新:
我在这里找到了4个很好的答案。非常感谢。

样本计时:
根据几次运行,以下是从最佳到最好的方法。即使是好的,也非常接近最佳。
在包含10K行、2.28 MB的文件中,我使用所有选项搜索了相同的5000个随机行。

  1. Seek4: 时间已过:00:00:00.0398530 ms -- Ritch Melton
  2. Seek3: 时间已过:00:00:00.0446072 ms -- Valentin Kuzub
  3. Seek1: 时间已过:00:00:00.0538210 ms -- Jake
  4. Seek2: 时间已过:00:00:00.0889589 ms -- bitxwise

下面是代码。保存代码后,您可以通过输入TestPaddedFileSeek.CallPaddedFileSeek();来简单地调用它。您还必须指定命名空间和“using引用”。

/// <summary>
/// This class multiple options of reading a by line number in a padded file (all lines are the same length).
/// The idea is to quick jump to the file.
/// Details about the discussions is available at: https://dev59.com/qVTTa4cB1Zd3GeqPw-tq
/// </summary>
class PaddedFileSeek
{
    public FileInfo File {get; private set;}
    public int LineLength { get; private set; }

    #region Private methods
    private static int FindLineLength(FileInfo fileInfo)
    {
        using (StreamReader reader = new StreamReader(fileInfo.FullName))
        {
            string line;
            if ((line = reader.ReadLine()) != null)
            {
                int length = line.Length + 2;   // The 2 is for NewLine(\r\n)
                return length;
            }
        }
        return 0;
    }

    private static void PrintHeader()
    {
       /*
        Debug.Print("");
        Debug.Print("Line No\t\tLine");
        Debug.Print("-------\t\t--------------------------");
       */ 
    }

    private static void PrintLine(int lineNo, string line)
    {
        //Debug.Print("{0}\t\t\t{1}", lineNo, line);
    }

    private static void PrintElapsedTime(TimeSpan elapsed)
    {
        Debug.WriteLine("Time elapsed: {0} ms", elapsed);
    }
    #endregion

    public PaddedFileSeek(FileInfo fileInfo)
    {
        // Possibly might have to check for FileExists
        int length = FindLineLength(fileInfo);
        //if (length == 0) throw new PaddedProgramException();
        LineLength = length;
        File = fileInfo;
    }

    public void CallAll(int[] lineNoArray, List<int> lineNoList)
    {
        Stopwatch sw = new Stopwatch();

        #region Seek1
        // Create new stopwatch
        sw.Start();

        Debug.Write("Seek1: ");
        // Print Header
        PrintHeader();

        Seek1(lineNoArray);

        // Stop timing
        sw.Stop();

        // Print Elapsed Time
        PrintElapsedTime(sw.Elapsed);

        sw.Reset();
        #endregion

        #region Seek2
        // Create new stopwatch
        sw.Start();

        Debug.Write("Seek2: ");
        // Print Header
        PrintHeader();

        Seek2(lineNoArray);

        // Stop timing
        sw.Stop();

        // Print Elapsed Time
        PrintElapsedTime(sw.Elapsed);

        sw.Reset();
        #endregion

        #region Seek3
        // Create new stopwatch
        sw.Start();

        Debug.Write("Seek3: ");
        // Print Header
        PrintHeader();

        Seek3(lineNoArray);

        // Stop timing
        sw.Stop();

        // Print Elapsed Time
        PrintElapsedTime(sw.Elapsed);

        sw.Reset();
        #endregion

        #region Seek4
        // Create new stopwatch
        sw.Start();

        Debug.Write("Seek4: ");

        // Print Header
        PrintHeader();

        Seek4(lineNoList);

        // Stop timing
        sw.Stop();

        // Print Elapsed Time
        PrintElapsedTime(sw.Elapsed);

        sw.Reset();
        #endregion

    }

    /// <summary>
    /// Option by Jake
    /// </summary>
    /// <param name="lineNoArray"></param>
    public void Seek1(int[] lineNoArray)
    {
        long position = 0;
        string line = null;

        Array.Sort(lineNoArray);

        using (FileStream fs = new FileStream(File.FullName, FileMode.Open, FileAccess.Read, FileShare.None))
        {
            using (StreamReader reader = new StreamReader(fs))
            {
                foreach (int lineNo in lineNoArray)
                {
                    position = (lineNo - 1) * LineLength;
                    fs.Seek(position, SeekOrigin.Begin);

                    if ((line = reader.ReadLine()) != null)
                    {
                        PrintLine(lineNo, line);
                    }

                    reader.DiscardBufferedData();
                }
            }
        }

    }

    /// <summary>
    /// option by bitxwise
    /// </summary>
    public void Seek2(int[] lineNoArray)
    {
        string line = null;
        long step = 0;

        Array.Sort(lineNoArray);

        using (FileStream fs = new FileStream(File.FullName, FileMode.Open, FileAccess.Read, FileShare.None))
        {
            // using (StreamReader reader = new StreamReader(fs))
            // If you put "using" here you will get WRONG results.
            // I would like to understand why this is.
            {
                foreach (int lineNo in lineNoArray)
                {
                    StreamReader reader = new StreamReader(fs);
                    step = (lineNo - 1) * LineLength - fs.Position;
                    fs.Position += step;

                    if ((line = reader.ReadLine()) != null)
                    {
                        PrintLine(lineNo, line);
                    }
                }
            }
        }
    }

    /// <summary>
    /// Option by Valentin Kuzub
    /// </summary>
    /// <param name="lineNoArray"></param>
    #region Seek3
    public void Seek3(int[] lineNoArray)
    {
        long position = 0; // totalPosition = 0;
        string line = null;
        int oldLineNo = 0;

        Array.Sort(lineNoArray);

        using (FileStream fs = new FileStream(File.FullName, FileMode.Open, FileAccess.Read, FileShare.None))
        {
            using (StreamReader reader = new StreamReader(fs))
            {
                foreach (int lineNo in lineNoArray)
                {
                    position = (lineNo - oldLineNo - 1) * LineLength;
                    fs.Seek(position, SeekOrigin.Current);
                    line = ReadLine(fs, LineLength);
                    PrintLine(lineNo, line);
                    oldLineNo = lineNo;

                }
            }
        }

    }

    #region Required Private methods
    /// <summary>
    /// Currently only used by Seek3
    /// </summary>
    /// <param name="stream"></param>
    /// <param name="length"></param>
    /// <returns></returns>
    private static string ReadLine(FileStream stream, int length)
    {
        byte[] bytes = new byte[length];
        stream.Read(bytes, 0, length);
        return new string(Encoding.UTF8.GetChars(bytes));
    }
    #endregion
    #endregion

    /// <summary>
    /// Option by Ritch Melton
    /// </summary>
    /// <param name="lineNoArray"></param>
    #region Seek4
    public void Seek4(List<int> lineNoList)
    {
        lineNoList.Sort();

        using (var fs = new FileStream(File.FullName, FileMode.Open))
        {
            lineNoList.ForEach(ln => OutputData(fs, ln));
        }

    }

    #region Required Private methods
    private void OutputData(FileStream fs, int lineNumber)
    {
        var offset = (lineNumber - 1) * LineLength;

        fs.Seek(offset, SeekOrigin.Begin);

        var data = new byte[LineLength];
        fs.Read(data, 0, LineLength);

        var text = DecodeData(data);
        PrintLine(lineNumber, text);
    }

    private static string DecodeData(byte[] data)
    {
        var encoding = new UTF8Encoding();
        return encoding.GetString(data);
    }

    #endregion

    #endregion
}



static class TestPaddedFileSeek
{
    public static void CallPaddedFileSeek()
    {
        const int arrayLenght = 5000;
        int[] lineNoArray = new int[arrayLenght];
        List<int> lineNoList = new List<int>();
        Random random = new Random();
        int lineNo;
        string fileName;


        fileName = @"C:\Temp\Temp.txt";

        PaddedFileSeek seeker = new PaddedFileSeek(new FileInfo(fileName));

        for (int n = 0; n < 25; n++)
        {
            Debug.Print("Loop no: {0}", n + 1);

            for (int i = 0; i < arrayLenght; i++)
            {
                lineNo = random.Next(1, arrayLenght);

                lineNoArray[i] = lineNo;
                lineNoList.Add(lineNo);
            }

            seeker.CallAll(lineNoArray, lineNoList);

            lineNoList.Clear();

            Debug.Print("");
        }
    }
}

`


你是否缺失了代码?我没有看到你的文件读取/搜索部分。 - John Arlen
它在 SeekMethod 方法中。 - geekosaur
我不理解你的立场。如果第一行是0,第二行是15,第三行是30,第四行是45(而不是15?),第五行是60(而不是30?)是正确的吗? - Ritch Melton
@Rich 你说得对。我太专注于LINE的结果了,甚至没有注意到那个。 - Pranav Shah
5个回答

3

将此代码放在SeekMethod(int[] lineNos)的内部循环中:

position = (lineNo - 1) * LineLength;
fs.Seek(position, SeekOrigin.Begin);
reader.DiscardBufferedData();

问题在于您的position变量会根据其先前的值发生变化,而StreamReader会维护一个缓冲区,因此在更改流位置时需要清除缓冲数据。

这会产生预期的输出,但会导致重新查找,我认为他不想要这个... - bitxwise
1
感谢您提醒我使用StreamReader.DiscardBufferedData。 - R. Martinho Fernandes
这种方法会降低性能,只有在绝对必要的情况下才应该使用,例如当您想要多次读取StreamReader对象的一部分内容时。去掉StreamReader,直接使用FileStream即可。请参考我的答案。 - Ritch Melton
reader.DiscardBufferedData();确实有帮助。我在循环结束前添加了它。我还会查看其他答案,只是为了检查是否可以更快地给我结果。谢谢。 - Pranav Shah

3
我对你期望的位置感到困惑,第5行在30和45位置,第4行在15位置,第3行在30位置?
以下是读取逻辑的核心:
    var offset = (lineNumber - 1) * LineLength;

    fs.Seek(offset, SeekOrigin.Begin);

    var data = new byte[LineLength];
    fs.Read(data, 0, LineLength);

    var text = DecodeData(data);
    Debug.Print("{0,-12}{1,-16}{2}", lineNumber, offset, text);

完整的示例在此处:
class PaddedFileSearch
{
    public int LineLength { get; private set; }
    public FileInfo File { get; private set; }

    public PaddedFileSearch(FileInfo fileInfo)
    {
        var length = FindLineLength(fileInfo);
        //if (length == 0) throw new PaddedProgramException();
        LineLength = length;
        File = fileInfo;
    }

    private static int FindLineLength(FileInfo fileInfo)
    {
        using (var reader = new StreamReader(fileInfo.FullName))
        {
            string line;
            if ((line = reader.ReadLine()) != null)
            {
                var length = line.Length + 2;
                return length;
            }
        }

        return 0;
    }

    public void SeekMethod(List<int> lineNumbers)
    {

        Debug.Print("");
        Debug.Print("Line No\t\tPosition\t\tLine");
        Debug.Print("-------\t\t--------\t\t-----------------");

        lineNumbers.Sort();

        using (var fs = new FileStream(File.FullName, FileMode.Open))
        {
            lineNumbers.ForEach(ln => OutputData(fs, ln));
        }
    }

    private void OutputData(FileStream fs, int lineNumber)
    {
        var offset = (lineNumber - 1) * LineLength;

        fs.Seek(offset, SeekOrigin.Begin);

        var data = new byte[LineLength];
        fs.Read(data, 0, LineLength);

        var text = DecodeData(data);
        Debug.Print("{0,-12}{1,-16}{2}", lineNumber, offset, text);
    }

    private static string DecodeData(byte[] data)
    {
        var encoding = new UTF8Encoding();
        return encoding.GetString(data);
    }
}

class Program
{
    static void Main(string[] args)
    {
        var seeker = new PaddedFileSearch(new FileInfo(@"D:\Desktop\Test.txt"));

        Debug.Print("File Line length: {0}", seeker.LineLength);

        seeker.SeekMethod(new List<int> { 5, 3, 4 });
        seeker.SeekMethod(new List<int> { 5, 3 });
    }
}

感谢您的帮助和新代码。它绝对比我之前尝试的要快。如果您不介意,我有几个后续问题,以便我更好地理解代码并提高我的技能。
  1. 为什么您决定使用List?
  2. 您能否解释一下:“ln => OutputData(fs, ln)”。还有它叫什么(Lambda表达式,LINQ)?
- Pranav Shah
还有,为什么输出之间会有额外的一行?这是从哪里来的? - Pranav Shah
在这种情况下,使用List而不是Array没有任何理由,我只是更喜欢List。我使用了一个lambda表达式,它只是定义委托的一种方式。在这种情况下,它是一个单参数委托,在其主体中调用OutputData。这只是比声明私有方法或使用匿名委托更简单的语法。希望这可以帮助到您。 - Ritch Melton
换行符来自原始文件中的换行符。我不会将它们修剪掉。缩短LineLength就可以解决这个问题。 - Ritch Melton

1

你的代码中第一行号的位置是绝对定位,而后面的行号则是相对定位,这种混合使用可能会导致问题。

请仔细检查代码并查看实际结果。

position = (lineNo - 1) * LineLength - position;
fs.Seek(position, SeekOrigin.Current);

对于值3、4、5,你得到的数字是30、15、45,但显然如果你使用相对位置,应该是30、15、15,因为线长是15或者如果你的读取方法执行了SEEK作为副作用,比如filestream.Read,那么就是30、0、0。而且你的测试输出意外地正确(只针对字符串值,而不是位置),你应该不使用序列进行测试,并仔细查看位置值,以确定显示的字符串与位置值之间没有联系。

实际上,你的StreamReader忽略了进一步的fs.Seek调用,只是逐行读取=)

这是输入3 5 9的结果:)

Line No         Position                Line
-------         --------                -----------------
3                       30                              0003|100!2500
5                       30                              0004|100!2500
9                       90                              0005|100!2500

我相信以下是最接近您想要实现的内容,一个新函数

private static string ReadLine(FileStream stream, int length)
        {
             byte[] bytes= new byte[length];
             stream.Read(bytes, 0, length);
             return new string(Encoding.UTF8.GetChars(bytes));  
        }

还有新的循环代码

int oldLine = 0;
    using (FileStream fs = new FileStream(FileName, FileMode.Open, FileAccess.Read, FileShare.None))
    {
            foreach (int lineNo in lineNos)
            {
                position = (lineNo - oldLine -1) * LineLength;
                fs.Seek(position, SeekOrigin.Current);
                line = ReadLine(fs, LineLength);
                Console.WriteLine("{0}\t\t\t{1}\t\t\t\t{2}", lineNo, position, line);
                oldLine = lineNo;
            }
    }

请注意,现在stream.Read函数等同于额外的stream.Seek(Length)

新的正确输出和逻辑位置更改

Line No         Position                Line
-------         --------                -----------------
3                       30                              0003|100!2500    
4                       0                               0004|100!2500    
5                       0                               0005|100!2500

Line No         Position                Line
-------         --------                -----------------
3                       30                              0003|100!2500  
5                       15                              0005|100!2500

附注:你认为001:行是第一行而不是第零行,这很奇怪...如果你使用程序员计数方法,整个-1可以被删除。


我不理解位置值。 - Ritch Melton
当您阅读连续的行时,很明显您不需要以任何方式改变位置,只需使用零偏移量读取下一行即可。 - Valentin Kuzub
啊哈!对我来说,这个位置值代表的含义并不明显。 - Ritch Melton
我最初是从0开始编写的。但后来我想,如果我想在非程序员用户程序中使用它,他们会认为第一行是1。 - Pranav Shah
谢谢,你的代码也非常好用。而且它比@Rich的解决方案稍微快一点。我将尝试进行适当的测试,以便为未来的读者发布结果。我没有意识到ReadLine会执行额外的Seek操作。 - Pranav Shah

1

我不会说问题在于手动管理位置值,而是StreamReader.ReadLine会更改流的Position值。如果您逐步执行代码并监视本地值,您会看到每个ReadLine调用后流的位置都会发生变化(第一个调用后为148)。

编辑

直接更改流的位置比使用Seek更好。

public void SeekMethod(int[] lineNos)
{
    string line = null;
    long step;

    Array.Sort(lineNos);

    Debug.Print("");
    Debug.Print("Line No\t\tPosition\t\tLine");
    Debug.Print("-------\t\t--------\t\t-----------------");

    using (FileStream fs = new FileStream(FileName, FileMode.Open, FileAccess.Read, FileShare.None))
    {
        foreach (int lineNo in lineNos)
        {
            StreamReader reader = new StreamReader(fs);
            step = (lineNo - 1) * LineLength - fs.Position;
            fs.Position += step;

            if ((line = reader.ReadLine()) != null) {
                Debug.Print("{0}\t\t\t{1}\t\t\t\t{2}", lineNo, step, line);
            }
        }
    }
}

0
问题在于您手动跟踪位置,但没有考虑到在读取该行后实际文件位置将向后移动一行。因此,您需要减去那个额外的读取量——但仅在实际发生读取时才需要这样做。
如果您真的想以这种方式做,那么不要保留“位置(position)”变量,而是获取实际的文件位置;或从给定的行号计算绝对文件位置,并直接从该位置进行寻找,而不是从当前文件偏移量开始。

如果我理解正确,您要求我将 // 动态定位 position = (lineNo - 1) * LineLength - position; fs.Seek(position, SeekOrigin.Current); 更改为 // 绝对定位 position = (lineNo - 1) * LineLength; fs.Seek(position, SeekOrigin.Begin); 我仍然得到相同的错误结果。 - Pranav Shah

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