【问题标题】:Efficiently searching a massive file for a string in C#在 C# 中有效地在海量文件中搜索字符串
【发布时间】:2021-01-31 20:22:31
【问题描述】:

我正在构建一个通过比较哈希值来扫描文件的应用程序。我需要在超过 1GB 的散列中搜索文件的散列。我为此找到了其他解决方案,例如 Aho-Corasick,但它比 File.ReadLines(file).Contains(str) 慢。

这是迄今为止最快的代码,使用File.ReadLines。扫描一个文件大约需要 8 秒,而使用 Aho-Corasick 扫描一个文件大约需要 2 分钟。由于显而易见的原因,我无法将整个哈希文件读入内存。

IEnumerable<DirectoryInfo> directories = new DirectoryInfo(scanPath).EnumerateDirectories();
IEnumerable<FileInfo> files = new DirectoryInfo(scanPath).EnumerateFiles();

FileInfo hashes = new FileInfo(hashPath);
await Task.Run(() =>
{
    IEnumerable<string> lines = File.ReadLines(hashes.FullName);
    
    foreach (FileInfo file in files) {
        if (!AuthenticodeTools.IsTrusted(file.FullName))
        {
            string hash = getHash(file.FullName);
            if (lines.Contains(hash)) flaggedFiles.Add(file.FullName);
        }
        filesScanned += 1;
    }
});
foreach (DirectoryInfo directory in directories)
{
    await scan(directory.FullName, hashPath);
    directoriesScanned += 1;
}

编辑:根据请求,以下是文件内容的示例:

5c269c9ec0255bbd9f4e20420233b1a7
63510b1eea36a23b3520e2b39c35ef4e
0955924ebc1876f0b849b3b9e45ed49d

它们是 MD5 哈希。

【问题讨论】:

  • 您应该测量读取文件的时间和搜索字符串的时间。没有算法比你的硬盘更快。为此,例如,只需删除 lines.Contains 代码行。
  • 我会说你应该反转代码......当你编写代码时,每个文件都会重新读取 1gb 哈希文件。您可以首先枚举所有文件,计算每个名称的哈希,将这两个信息(名称+哈希)放入字典中,然后将其与哈希列表进行比较
  • 或者你真的可以将哈希文件加载到内存中......如果做得好,磁盘上的 1gb 小于 500mb 的内存(因为磁盘上的哈希是十六进制格式,而在内存中你会保存它们以二进制格式)
  • 如果我们进行二分搜索,我们不需要分配太多。每个散列可以二进制压缩为 16 个字节。所以我们只需要一个那么大的缓冲区
  • @TheodorZoulias 我们正处于高级原型设计阶段,甚至被称为“将 s##t 扔到墙上,看看哪个更好”:-)

标签: c# performance search large-files


【解决方案1】:

由于哈希值固定为 32 个十六进制数字(16 个字节),它们应该以二进制格式存储,没有空格。我们可以通过简单的乘法对每个哈希值进行直接查找.

如果我们然后按顺序对文件中的哈希进行排序,我们可以通过对每个哈希进行二分搜索来加快速度。

可以使用下面的CompareHashes 函数作为比较函数进行排序。


完成后,我们可以进行二分搜索。

Binary search 是一种搜索排序列表的简单算法。它具有 O(log2 n) 复杂度,因此,对于您拥有的哈希数量,最多只需要大约 25 次查找。算法如下:

  1. 从中间开始。
  2. 如果我们要找的项目在那里,那就太好了。
  3. 如果更早,则将搜索的高点更改为前一个。将差值除以 2,然后循环回到第 2 步。
  4. 如果较晚,则将搜索的低点更改为后一个。将差值除以 2,然后循环回到第 2 步。
  5. 如果我们到达最后一个,则找不到该项目。

(为此,我已从 .Net Framework 中的 ArraySortHelper 中提取并修改了一些代码。)

public static bool ContainsHash(FileStream hashFile, byte[] hash)
{
    const long hashSize = 16;
    var curHash = new byte[hashSize];
    long lo = 0;
    long hi = hashFile.Length / hashSize - 1;
    while (lo <= hi)
    {
        long i = lo + ((hi - lo) >> 1);
        hashFile.Read(curHash, i * hashSize, hashSize);

        int order = CompareHashes(curHash, hash);
 
        if (order == 0) return true;
        if (order < 0)
        {
            lo = i + 1;
        }
        else
        {
            hi = i - 1;
        }
    }
    return false;
}

public static int CompareHashes(byte[] b1, byte[] b2)
{
    var comp = 0;
    for (int i = 0; i < b1.Length; i++)
    {
        comp = b1[i].CompareTo(b2[i]);
        if(comp != 0) return comp;
    }
    return comp;
}

我们只需要打开哈希文件一次,并将哈希值的FileStream 传递给函数,加上一个哈希值进行比较。


我可能有一些小错误,因为我没有测试过。我希望其他人可以测试和编辑这个答案。

【讨论】:

  • 如果代码正确,这是个好主意。唯一的问题是保持哈希文件排序。问题:订购一个充满哈希的文件需要将整个文件加载到内存中(或者进行归并排序,但实现起来很痛苦)
  • 嗯...如果需要对哈希文件进行排序,我可以编写一个简单的节点脚本对其进行排序。
  • 取决于你在做什么。如果这是应用程序中唯一的用例,那么 IMO 就太过分了。
  • @Zer0 我可以说 Charlieface 的解决方案只不过是一个小型的自制数据库,由一个包含一个字段的表组成。如果这个答案在 OP 问题的范围内,那么内存映射文件和Disk Based Data Structures 等等也是如此。
  • @Zer0 我实际上是在考虑 MM 文件,但并没有真正的帮助,因为我们经常需要移动窗口,我们正在努力节省 RAM,所以无法容纳整个东西.请记住,我们没有关于成功与不成功匹配比例的数据,所以这会改变事情。另外,我们还没有考虑磁盘缓冲,通常在 10 秒 MB 内,也没有考虑操作系统缓冲。 另外,更重要的是,最后几个查找将非常接近, 从查找 17 开始,我们的大小小于 4KB,因此在典型的块大小内。
【解决方案2】:

您似乎将处理目录中的所有文件,所以您为什么不改变您的方法。首先,使用以下内容填充所有不受信任的文件的字典:

var hashDict = files.Where(fi => !IsTrusted(fi.FullName))
                    .ToDictionary(fi=>fi.FullName,fi=>getHash(fi.FullName));

现在您有了要检查的哈希列表,将它们传递给获取标记文件的方法。

using(var stream = File.OpenRead(hashPath) )
{
    var flaggedFiles = GetHashesInStream(stream, hashDict);
    // Do whatever you need to do with the list.
}

这里是搜索方法:

private static List<string> GetFilesWithMatchingHashes(Stream s, Dictionary<string,string> hashes)
{
    var results = new List<string>();
    var bufsize = (1024 * 1024 / 34)*34; // Each line should be 32 characters for the hash and 2 for cr-lf
                                         // Adjust if this isn't the case
    var buffer = new byte[bufsize];
    s.Seek(0, SeekOrigin.Begin);

    var readcount = bufsize;
    var keyList = hashes.Keys.ToList();
    while (keyList.Count > 0 && (readcount = s.Read(buffer, 0, bufsize)) > 0)
    {
        var str = Encoding.ASCII.GetString(buffer, 0, readcount);
        for (var i = keyList.Count - 1; i >= 0; i--)
        {
            var k = keyList[i];
            if (str.Contains(hashes[k]))
            {
                results.Add(k);
                keyList.RemoveAt(i);
            }
        }
    }
    return results; // This should contain a list of the files with found hashes.
}

此解决方案的好处是您只需扫描文件一次。我做了一些测试,在 1,020,000,000 字节的文件中搜索最后一个哈希。仅搜索一个哈希值比您的 readlines 方法快两倍多。一次获取它们应该更快。

【讨论】:

    猜你喜欢
    • 2013-03-02
    • 2018-08-05
    • 2011-02-01
    • 2011-01-12
    • 2011-01-31
    • 1970-01-01
    • 1970-01-01
    • 2020-10-13
    • 2010-12-10
    相关资源
    最近更新 更多