【问题标题】:Search for string in multiple text files of size 150 MB each C#在每个 C# 大小为 150 MB 的多个文本文件中搜索字符串
【发布时间】:2012-11-26 18:02:33
【问题描述】:

我有多个 150MB 大小的 .txt 文件。使用 C# 我需要从每个文件中检索包含字符串模式的所有行,然后将这些行写入新创建的文件。

我已经研究过类似的问题,但他们建议的答案都不能给我最快的获取结果的方法。我尝试了正则表达式、linq 查询、包含方法、使用字节数组搜索,但所有这些都需要 30 多分钟来读取和比较文件内容。

我的测试文件没有任何特定的格式,它就像我们不能基于分隔符拆分的原始数据和基于 DataViews 的过滤器。下面是该文件中每一行的示例格式。

Sample.txt

LTYY;;0,0,;123456789;;;;;;;20121002 02:00;;
ptgh;;0,0,;123456789;;;;;;;20121002 02:00;;
HYTF;;0,0,;846234863;;;;;;;20121002 02:00;;
Multiple records......

我的代码

using (StreamWriter SW = new StreamWriter(newFile))
            {
                using(StreamReader sr = new StreamReader(sourceFilePath))
                {
                while (sr.Peek() >= 0) 
                {
                   if (sr.ReadLine().Contains(stringToSearch))
                     SW.WriteLine(sr.ReadLine().ToString());
                 }
}
}

我想要一个示例代码,它可以在不到一分钟的时间内从 Sample.txt 中搜索 123456789。如果我的要求不清楚,请告诉我。提前致谢!

编辑

我找到了根本原因,因为文件驻留在远程服务器中会花费更多时间来读取它们,因为当我将文件复制到本地计算机时,所有比较方法都很快完成,所以这不是问题以我们阅读或比较内容的方式,它们或多或少花费了相同的时间。

但是现在我该如何解决这个问题,我无法将所有这些文件复制到我的机器上进行比较并获得 OutOfMemory 异常

【问题讨论】:

  • 使用数据库,大声呼喊。
  • 没有分隔符?示例显示分隔符,这是否意味着您希望同时匹配:;123456789;和;abc1234567890abc;?
  • @Blah_Blah 确实如此!即使没有,读取所有文件内容以将数据拆分为字符串或字节数组然后进行比较是否需要大量时间,因为我认为读取所有文件内容需要更多时间?

标签: c# copy


【解决方案1】:

最快的搜索方法是使用Boyer–Moore string search algorithm,因为这种方法不需要从文件中读取所有字节,但需要随机访问字节您可以尝试使用Rabin Karp Algorithm p>

或者您可以尝试从this answer 执行以下代码:

  public static int FindInFile(string fileName, string value)
  {   // returns complement of number of characters in file if not found
    // else returns index where value found
  int index = 0;
   using (System.IO.StreamReader reader = new System.IO.StreamReader(fileName))
   {
    if (String.IsNullOrEmpty(value))
        return 0;
    StringSearch valueSearch = new StringSearch(value);
    int readChar;
    while ((readChar = reader.Read()) >= 0)
    {
        ++index;
        if (valueSearch.Found(readChar))
            return index - value.Length;
    }
}
return ~index;
}
 public class StringSearch
 {   // Call Found one character at a time until string found
private readonly string value;
private readonly List<int> indexList = new List<int>();
public StringSearch(string value)
{
    this.value = value;
}
public bool Found(int nextChar)
{
    for (int index = 0; index < indexList.Count; )
    {
        int valueIndex = indexList[index];
        if (value[valueIndex] == nextChar)
        {
            ++valueIndex;
            if (valueIndex == value.Length)
            {
                indexList[index] = indexList[indexList.Count - 1];
                indexList.RemoveAt(indexList.Count - 1);
                return true;
            }
            else
            {
                indexList[index] = valueIndex;
                ++index;
            }
        }
        else
        {   // next char does not match
            indexList[index] = indexList[indexList.Count - 1];
            indexList.RemoveAt(indexList.Count - 1);
        }
    }
    if (value[0] == nextChar)
    {
        if (value.Length == 1)
            return true;
        indexList.Add(1);
    }
    return false;
}
public void Reset()
{
    indexList.Clear();
}
}

【讨论】:

    【解决方案2】:

    我不知道这需要运行多长时间,但这里有一些改进:

    using (StreamWriter SW = new StreamWriter(newFile))
    {
        using (StreamReader sr = new StreamReader(sourceFilePath))
        {
            while (!sr.EndOfStream)
            {
                var line = sr.ReadLine();
                if (line.Contains(stringToSearch))
                    SW.WriteLine(line);
            }
        }
    }
    

    请注意,您不需要PeekEndOfStream 会给您您想要的。你打电话给ReadLine 两次(可能不是你想要的)。并且无需在string 上致电ToString()

    【讨论】:

    • 您可以通过以下方式缩短它:` File.AppendAllLines("b.txt", File.ReadLines("a.txt").Where(x => x.Contains("123456789" ))); `
    • @YoryeNathan 您应该将其发布为答案。
    • 乔恩,感谢您的回复,我提出了您建议的更改,但所需时间没有任何改进。
    【解决方案3】:

    正如我已经说过的,你应该有一个数据库,但无论如何。

    最快、最短和最好的方法(即使是单行)是这样的:

    File.AppendAllLines("b.txt", File.ReadLines("a.txt")
                                     .Where(x => x.Contains("123456789")));
    

    但是快吗? 150MB 就是 150MB。这需要一段时间。 您可以用自己的方法替换 Contains 方法,以便更快地进行比较,但这是一个完全不同的问题。

    其他可能的解决方案...

    var sb = new StringBuilder();
    
    foreach (var x in File.ReadLines("a.txt").Where(x => x.Contains("123456789")))
    {
        sb.AppendLine(x);
    }
    
    File.WriteAllText("b.txt", sb.ToString()); // That is one heavy operation there...
    

    用 150MB 的文件大小测试它,它在 3 秒内找到了所有结果。需要时间的事情是将结果写入第二个文件(以防有很多结果)。

    【讨论】:

    • Yorye,我在 File 类中没有看到 AppendAllLines 方法。我有 .NET 框架 3.5,应该在我的问题中说明这一点。你能建议替代解决方案吗?
    • @user841311 - 您应该更新您的问题以反映这一事实。
    • 完成。结果令人满意。
    • 奇怪!我不知道为什么,但对我来说,使用您的解决方案 10-15 分钟后它仍在运行。是不是因为文件驻留在远程机器上,所以读取内容需要更长的时间。
    • 是的 IO 是什么消耗了更多时间,因为我删除了比较部分并像@Ωmega 建议的那样逐行读取它,它仍然运行了一段时间..我会尝试复制文件到本地机器试一试。
    【解决方案4】:

    150MB 是 150MB。如果您有一个线程逐行遍历整个 150MB(“行”由换行符/组或 EOF 终止),您的进程必须读入并旋转所有 150MB 数据(不是全部在一次,它不必同时持有所有这些)。线性搜索 157,286,400 个字符非常简单,需要时间,而且您说您有很多这样的文件。

    第一件事;您正在从流中读取该行两次。在大多数情况下,这实际上会导致您在匹配时读取两行;写入新文件的内容将是包含搜索字符串的行之后的行。这可能不是您想要的(再次,它可能是)。如果要写入实际包含搜索字符串的行,请在执行包含检查之前将其读入变量。

    其次,String.Contains() 将根据需要执行线性搜索。在您的情况下,该行为实际上会接近 N^2,因为在字符串中搜索字符串时,必须找到第一个字符,并且在它所在的位置,然后将每个字符与后续字符一个接一个地匹配,直到所有字符搜索字符串已匹配或找到不匹配的字符;当发生不匹配时,算法必须回到初始匹配后的字符以避免跳过可能的匹配,这意味着在检查长字符串与具有许多部分匹配的较长字符串时,它可以多次测试相同的字符。因此,该策略在技术上是一种“蛮力”解决方案。不幸的是,当您不知道在哪里查找时(例如在未排序的数据文件中),没有更有效的解决方案。

    除了能够对文件的数据进行排序然后执行索引搜索之外,我可以建议的唯一可能的加速方法是多线程解决方案;如果您只在一个查看每个文件的线程上运行此方法,那么不仅只有一个线程在做这项工作,而且该线程一直在等待硬盘驱动器提供所需的数据。有 5 或 10 个线程,每个线程一次处理一个文件,不仅可以更有效地利用现代多核 CPU 的真正功能,而且当一个线程在硬盘驱动器上等待时,另一个已加载数据的线程可以执行,进一步提高这种方法的效率。请记住,数据离 CPU 越远,CPU 获取数据所需的时间就越长,当您的 CPU 每秒可以处理 2 到 40 亿件事情时,甚至需要为硬盘驱动器等待几毫秒就意味着您每秒会丢失数百万条潜在指令。

    【讨论】:

    • 感谢 Keith 的回复,我会尝试创建多个线程,但除了多线程之外,您能告诉我如何对包含没有共同模式的行的文本文件进行排序。
    • 老实说,你不能。那就是问题所在;旨在存储大量数据的系统,如 RDBMS,以有序的方式存储(该顺序可能对您或我没有多大意义,但计算机可以快速钻取它)。对于处理平面文件,您几乎正在做您应该做的事情;一次读一行。问题在于,这种策略要求您一次读取每个文件中的每一行。
    【解决方案5】:

    我没有给你示例代码,但你尝试过对文件内容进行排序吗?

    尝试从价值 150MB 的文件中搜索字符串将花费一些时间来分割它,如果正则表达式对您来说花费的时间太长,那么我建议您对文件的内容进行排序,这样您在实际搜索之前大致知道"123456789" 将出现在哪里,这样您就不必搜索不重要的部分。

    【讨论】:

    • @JonB 我错过了他们每个 150 的部分,我认为它是 150mb 分布在多个文件中
    【解决方案6】:

    不要同时读写。先搜索,保存匹配行列表,最后写入文件。

    using System;
    using System.Collections.Generic;
    using System.IO;
    ...
    List<string> list = new List<string>();
    using (StreamReader reader = new StreamReader("input.txt")) {
      string line;
      while ((line = reader.ReadLine()) != null) {
        if (line.Contains(stringToSearch)) {
          list.Add(line); // Add to list.
        }
      }
    }
    using (StreamWriter writer = new StreamWriter("output.txt")) {
      foreach (string line in list) {
        writer.WriteLine(line);
      }
    }
    

    【讨论】:

    • 我尝试将它们分开,但没有改进搜索操作的时间。感谢您的回复。
    • @user841311 - 所以只做空白阅读而不进行搜索,以确定 IO 操作是否不需要这么长时间。 while ((line = reader.ReadLine()) != null) { ; }
    【解决方案7】:

    在进行字符串比较时,您将在阻止来自这些文件的输入的方法中遇到性能问题。

    但是 Windows 有一个非常高性能的类似 GREP 的工具,用于对名为 FINDSTR 的文本文件进行字符串搜索,它可能足够快。您可以简单地将其作为 shell 命令调用或将命令的结果重定向到您的输出文件。

    预处理(排序)或将大文件加载到数据库中会更快,但我假设您已经有需要搜索的现有文件。

    【讨论】:

    • 我认为这是正确的方向,操作系统工具可能会更快地做到这一点。在 linux 中,这将是一个简单的 grep 命令管道输出到 txt 文件,并且可能会更快地完成。另一种选择是将整个文件复制到内存中,然后在那里进行字符串比较,完成后刷新。
    猜你喜欢
    • 1970-01-01
    • 2014-03-17
    • 2011-04-28
    • 1970-01-01
    • 1970-01-01
    • 2013-04-27
    • 1970-01-01
    • 2012-09-28
    • 2013-02-11
    相关资源
    最近更新 更多