【问题标题】:Read multiple lines from a large file in non-ascending order以非升序从大文件中读取多行
【发布时间】:2023-04-08 00:33:01
【问题描述】:

我有一个非常大的文本文件,超过 1GB,我有一个表示行号的整数列表,需要在新文件中生成另一个包含原始文件行号文本的文件。

原始大文件示例:

ogfile line 1
some text here
another line
blah blah

所以当我得到一个“2,4,4,1”列表时,输出文件应该是:

some text here
blah blah
blah blah
ogfile line 1

我试过了 string lineString = File.ReadLines(filename).Skip(lineNumList[i]-1).Take(1).First();

但这需要很长时间,因为必须读入文件,跳到有问题的行,然后下一次重新读取......我们正在谈论 1GB 文件中的数百万行,而我的 List<int> 是数以千计的行号。

是否有更好/更快的方法来阅读单行,或者让读者跳到特定的行号而不逐行“跳过”?

【问题讨论】:

  • 读取文件一次,然后从该数组中获取您想要的行
  • 我会说只是将整个内容读入内存 (File.ReadAllLines()) 并按索引抓取
  • 我需要大量 RAM 才能将整个文件存储到内存中。如果文件是 10GB 怎么办?
  • 停止使用文本文件作为数据库,开始使用数据库作为数据库.
  • 但这就是说:你对这份名单有什么了解吗?例如,您是否知道它将有许多“重复”(例如您的“4、4”场景),或者几乎没有任何重复?您是否大致知道将提取原始行的哪一部分?您似乎暗示它约为 0.1%;那准确吗?等等。构建可扩展的解决方案通常需要利用输入的已知特征。

标签: c# file filereader streamreader


【解决方案1】:

这里最重要的是:您正在尝试使用文本文件解决数据库问题。数据库旨在解决大数据问题;正如您所发现的,文本文件在随机访问时非常糟糕。 使用数据库,而不是文本文件

如果您一心想要使用文本文件,那么您要做的就是利用您了解的有关可能的问题参数的资料。例如,如果你知道,正如你所暗示的,有 ~1M 行,每行 ~1KB,要提取的行集是总行数的 ~0.1%,那么你可以想出一个有效的解决方案,比如这个:

  • 创建一个包含要读取的行号的集合。该集合必须快速检查成员资格。
  • 制作一个从行号映射到行内容的字典。这必须快速通过键查找并快速添加新的键/值对。
  • 一次读取文件的每一行;如果行号在集合中,则将内容添加到字典中。
  • 现在迭代行号列表并映射字典内容;现在我们有了一个字符串序列。
  • 将该序列转储到目标文件。

我们有五个操作,所以希望它是大约五行代码。

void DoIt(string pathIn, IEnumerable<int> lineNumbers, string pathOut)
{
  var lines = new HashSet<int>(lineNumbers);
  var dict = File.ReadLines(pathIn)
    .Select((lineText, index) => new KeyValuePair<int, string>(index, lineText))
    .Where(p => lines.Contains(p.Key))
    .ToDictionary(p => p.Key, p => p.Value);
  File.WriteAllLines(pathOut, lineNumbers.Select(i => dict[i]));
}

好的,六分了。挺好的。


请注意,我利用了所有这些假设; 如果违反假设,那么这将不再是一个好的解决方案。特别是我们假设字典与输入文件的大小相比会很小。如果这不是真的,那么您将需要更复杂的技术来提高效率。

相反,我们能否提高效率? 是的,只要我们知道有关可能输入的事实。例如,假设我们知道同一个文件将被迭代多次,但具有不同的行号集,但这些集很可能有重叠。在那种情况下我们可以重复使用字典而不是重建它们。也就是说,假设先前的操作留下了为行 (10, 20, 30, 40) 和文件 X 计算的 Dictionary&lt;int, string&gt;。如果随后对文件 X 的行 (30, 20, 10) 提出请求,我们已经把字典放在内存里。

我想在这个答案中表达的关键是您必须了解一些关于输入的信息,以便构建一个有效的解决方案;您对输入的限制越多,您构建的解决方案就越有效。充分利用您对问题领域的所有知识。

【讨论】:

  • 如果你想节省内存,使用File.ReadLines而不是File.ReadAllLines是否有意义,因为前者将逐行遍历文件而不是将其全部读入内存一次?
  • @germi:是的,我本来打算按你说的写,不知道为什么我的手指不听使唤!
【解决方案2】:

使用StreamReader,这样您就不必读取整个文件,只需要读取最后一行,然后将它们存储在字典中,以便以后快速搜索。

编辑:感谢 Erick Lippert,我包含了一个用于快速查找的 HashSet。

List<int> lineNumbers = new List<int>{2,4,4,1};
HashSet<int> lookUp = new HashSet<int>(lineNumbers);
Dictionary<int,string> lines = new Dictionary<int,string>();

using(StreamReader sr = new StreamReader(inputFile)){
    int lastLine = lookUp.Max();
    for(int currentLine=1;currentLine<=lastLine;currentLine++){
        if(lookUp.Contains(currentLine)){
            lines[currentLine]=sr.ReadLine();
        }
        else{
            sr.ReadLine();
        }       
    }   
}
using(StreamWriter sw = new StreamWriter(outputFile)){
    foreach(var line in lineNumbers){
        sw.WriteLine(lines[line]);
    }
}

【讨论】:

  • 正如我在 cmets 中对给出此解决方案的另一个答案所指出的:问题是,如果列表很大,则检查列表中的包含可能效率低下。请参阅我的回答,了解一些可以使此代码更短、更快且更易于遵循的方法。
  • @EricLippert 感谢您的提示。我对其进行了编辑以使用 HashSet。您的回答得到了很好的评价,点赞。
【解决方案3】:

您可以使用StreamReaderReadLine 方法逐行读取而不会影响内存:

var lines = new Dictionary<int, string>();
var indexesProcessed = new HashSet<int>();
var indexesNew = new List<int> { 2, 4, 4, 1 };

using ( var reader = new StreamReader(@"c:\\file.txt") )
  for ( int index = 1; index <= indexesNew.Count; index++ )
    if ( reader.Peek() >= 0 )
    {
      string line = reader.ReadLine();
      if ( indexesNew.Contains(index) && !indexesProcessed.Contains(index) )
      {
        lines[index] = line;
        indexesProcessed.Add(index);
      }
    }

using ( var writer = new StreamWriter(@"c:\\file-new.txt", false) )
  foreach ( int index in indexesNew )
    if ( indexesProcessed.Contains(index) )
      writer.WriteLine(lines[index]);

它读取文件并选择所需的索引,然后按所需的顺序保存它们。

我们使用 HashSet 存储已处理的索引以加速包含调用,因为您指出文件可能超过 1GB。

编写代码是为了避免在源文件和所需索引之间不匹配的情况下索引越界,但它会减慢处理速度。如果您确定不会有问题,您可以优化。在这种情况下,您可以删除所有使用 indexesProcessed

输出:

some text here
blah blah
blah blah
ogfile line 1

【讨论】:

  • 这是一个很好的尝试,但是最好有三个数据结构:你的字典,你的索引列表,和一个索引集 .这里的问题是indexesNew.Contains(index) 检查是 O(n)。如果您从 indexesNew 初始化 HashSet&lt;int&gt;,则检查哈希集中的包含是 O(1)。
  • 我不习惯使用 HashSet,谢谢。答案已更新。发现:stackoverflow.com/questions/150750/hashset-vs-list-performance.
  • 查看我的回答,了解如何使这段代码更短、更容易理解。
  • 但是indexesNew 不能是HashSet 因为重复,不是吗?
  • Contains 上的List 调用替换为HashSet 是个好主意。对于大型集合而言,差异是巨大的,因为HashSet 保持相同的速度,而List 会随着时间的推移不断增长!
【解决方案4】:

一种方法是简单地读取输入文件一次(并将结果存储在变量中),然后获取您需要的行并将它们写入输出文件。

由于行号是基于1,而数组是基于0(即行号1是数组索引0),我们在指定数组索引时从行号中减去1

static void Main(string[] args)
{
    var inputFile = @"f:\private\temp\temp.txt";
    var outputFile = @"f:\private\temp\temp2.txt";

    var fileLines = File.ReadAllLines(inputFile);
    var linesToDisplay = new[] {2, 4, 4, 1};

    // Write each specified line in linesToDisplay from fileLines to the outputFile
    File.WriteAllLines(outputFile, 
        linesToDisplay.Select(lineNumber => fileLines[lineNumber - 1]));

    GetKeyFromUser("\n\nDone! Press any key to exit...");
}

另一种更有效的方法是只读取文件的最大行号(使用ReadLines方法),而不是读取整个文件(使用ReadAllLines方法),并且仅将我们关心的行保存在将行号映射到行文本的字典中:

static void Main(string[] args)
{
    var inputFile = @"f:\private\temp\temp.txt";
    var outputFile = @"f:\private\temp\temp2.txt";

    var linesToDisplay = new[] {2, 4, 4, 1};
    var maxLineNumber = linesToDisplay.Max();
    var fileLines = new Dictionary<int, string>(linesToDisplay.Distinct().Count());

    // Start lineNumber at 1 instead of 0
    int lineNumber = 1;

    // Just read up to the largest line number we need 
    // and save the lines we care about in our dictionary
    foreach (var line in File.ReadLines(inputFile))
    {
        if (linesToDisplay.Contains(lineNumber))
        {
            fileLines[lineNumber] = line;
        }

        // Increment our lineNumber and break if we're done
        if (++lineNumber > maxLineNumber) break;
    }

    // Write the output to our file
    File.WriteAllLines(outputFile, linesToDisplay.Select(line => fileLines[line]));

    GetKeyFromUser("\n\nDone! Press any key to exit...");
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-01
    • 2014-12-10
    • 1970-01-01
    • 1970-01-01
    • 2015-11-26
    相关资源
    最近更新 更多