繁体   English   中英

c#所有文件中最快的字符串搜索

[英]c# Fastest string search in all files

问题( 检查编辑是否有澄清

我有一个大约1500个字符串的列表,对于每个字符串,我必须检查目录(和子目录)中的任何文件是否包含该字符串(大约有4000个文件)。

我现在拥有的是这两种变体:

原版的

foreach(var str in stringList)
{
    allFiles.Any(f => File.ReadAllText(f).Contains(str));
}

第二个变体 (使用ReadLines而不是ReadAllText,正如VladL在这个问题中所建议的那样)

foreach(var string in stringList)
{
    allFiles.SelectMany(File.ReadLines).Any(line => line.Contains(str));
}

我只测试了原始变体的完整程序执行,花了21分钟才完成。 然后我测试了一个语句(检查任何文件中是否包含1个字符串),搜索一个我知道它不包含的字符串来检查最坏的情况,这是我的时间(每次执行3次):

原文1285,1369,1336 ms

第二个变体2718,2804,2831 ms

我还尝试在原始语句中用ReadAllLines替换ReadAllText(不更改任何其他内容),但没有性能更改。

有没有更快的方法来检查字符串是否包含在任何文件(大量的大文件)中?

编辑

我承认我没有像我想的那样表达自己,说我有一个字符串列表。 我实际拥有的是一个csv文件列表,然后我将其删除,然后遍历这些文件的每一行(忽略第一行)。 对于每一行,我创建一个字符串,用该行的某些字段组成它,然后查看是否有任何文件包含该字符串。

foreach(var csvFile in csvFiles)
{
    var lines = File.ReadAllLines(csvFile);
    foreach(var line in lines)
    {
        if (IsHeader(line)) continue;
        var str = ComposeString(line);
        var bool = allFiles.Any(f => File.ReadAllText(f).Contains(str));
        // do stuff with the line and bool
     }
 }

编辑2

public void ExecuteAhoCorasick()
{
    var table = CreateDataTable();
    var allFiles = GetAllFiles();
    var csvFiles = GetCsvFiles();
    var resList = new List<string>();

    foreach(var csvFile in csvFiles)
    {
        if (file.Contains("ValueList_")) continue;
        var lines = File.ReadAllLines(file);
        foreach (var line in lines)
        {
            if (line == HeaderLine) continue;
            var res = line.Split(';');
            if (res.Length <= 7) continue;
            var resPath = $"{res[0]}.{res[1]}.{res[2]}".Trim('.');
            resList.Add(resPath);

            var row = table.NewRow();
            row[0] = res[0]; // Group
            row[1] = res[1]; // Type
            row[2] = res[2]; // Key
            row[3] = res[3]; // Global
            row[4] = res[4]; // De
            row[5] = res[5]; // Fr
            row[6] = res[6]; // It
            row[7] = res[7]; // En
            row[8] = resPath; // Resource Path
            row[9] = false;
            row[10] = ""; // Comment
            row[11] = file; // File Path

            table.Rows.Add(row);
        }
    }

    var foundRes = new List<string>();

    foreach (var file in allFiles)
    {
        // var chars = File.ReadLines(file).SelectMany(line => line);
        var text = File.ReadAllText(file);

        var trie = new Trie();
        trie.Add(resList);

        foundRes.AddRange(trie.Find(text));
        // foundRes.AddRange(trie.Find(chars));
    }

    // update row[9] to true foreach res in foundRes
}

文件是否包含任何字符串?

private static bool ContainsLine(string file, List<string> wordsToFind) {
  return File
    .ReadLines(file)
    .Any(line => wordsToFind.Any(word => line.Contains(word))); 
}

我们有任何包含任何字符串的文件吗?

bool result = allFiles
  .AsParallel() // worth trying: we have a lot of files to be proceed
  .Any(file => ContainsLine(file, stringList));

编辑 :经常.AsParallel()值得尝试这样的问题(许多文件要测试),但是,如果AsParallel()没有带来任何好处,只需将其注释掉

bool result = allFiles
  //.AsParallel() // comment out in case of no gain
  .Any(file => ContainsLine(file, stringList));

我认为最快的方法是:

  1. 将每个文件完全读入内存。 这简化了代码。
  2. 使用Aho-Corasick算法在文本中搜索每个文件的关键字。

这里有Aho-Corasick的实现。

我编写了一个简单的程序,使用Github的实现来测试最坏情况的性能(也就是说,当文本中没有关键字时)来比较Aho-Corasick和Contains() ):

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using ConsoleApp1;

namespace Demo
{
    class Program
    {
        static void Main()
        {
            string[] needles = createNeedles(1500).ToArray();
            string haystack = createHaystack(100000);

            var sw = Stopwatch.StartNew();
            anyViaContains(needles, haystack);
            Console.WriteLine("anyViaContains() took " + sw.Elapsed);

            sw.Restart();
            anyViaAhoCorasick(needles, haystack);
            Console.WriteLine("anyViaAhoCorasick() took " + sw.Elapsed);
        }

        static bool anyViaContains(string[] needles, string haystack)
        {
            return needles.Any(haystack.Contains);
        }

        static bool anyViaAhoCorasick(string[] needles, string haystack)
        {
            var trie = new Trie();
            trie.Add(needles);
            trie.Build();
            return trie.Find(haystack).Any();
        }

        static IEnumerable<string> createNeedles(int n)
        {
            for (int i = 0; i < n; ++i)
                yield return n + "." + n + "." + n;
        }

        static string createHaystack(int n)
        {
            var sb = new StringBuilder();

            for (int i = 0; i < n; ++i)
                sb.AppendLine("Sample Text Sample Text Sample Text Sample Text Sample Text Sample Text Sample Text Sample Text");

            return sb.ToString();
        }
    }
}

我得到的64位RELEASE版本(在调试器外部运行)得到的结果如下:

anyViaContains()花了00:00:09.8216836

anyViaAhoCorasick()花了00:00:00.4002765

对于这个测试用例,似乎Aho-Corasick比使用Contains()快25倍。 但是,这是一个有点人为的测试用例,您的实际结果可能会有所不同。 您应该检测实际数据,看看它是否真的有用。

请注意,在使用Aho-Corasick实现时,您实际上可以避免将整个文件加载到内存中,因为它的Find()方法接受IEnumerable<char>

您可以将文件的内容转换为IEnumerable<char> ,如下所示:

var chars = File.ReadLines(filename).SelectMany(line => line);

这实际上删除了所有换行符,这对您的应用程序来说可能没问题。 如果你想保留换行符,你必须像以下那样把它们放回去:

IEnumerable<char> newline = Enumerable.Repeat('\n', 1);
var chars = File.ReadLines(filename).SelectMany(line => Enumerable.Concat(line, newline));

值得比较将每个文件完全加载到内存中,并枚举每个文件中的字符(如上所示),看看是否有任何差异。 对于非常大的文件,避免将其全部内容加载到内存中可能很重要。

您正在读取每个字符串的所有文件。

如何以相反的方式做到这一点? 循环遍历所有文件:

bool exists = 
    allFiles.SelectMany(File.ReadLines).Any(l=> stringList.Any(str=> l.Contains(str));

OP编辑后:

正如您在评论中提到的,您应该首先从CSV文件中模拟所有字符串,然后按照建议继续:

var stringList =
  csvFiles.SelectMany(f=>File.ReadAllLines(f).Where(l=>!IsHeader(l)).Select(ComposString))
          .ToList();

您可能还想使用.Distinct ,以防这些单词中的某些单词不是唯一的,以使其更快。 但这取决于这个列表的大小,以及有多少单词确实在重复。

var stringList =
  csvFiles.SelectMany(f=>File.ReadAllLines(f).Where(l=>!IsHeader(l)).Select(ComposString))
          .Distinct()
          .ToList();

这在很大程度上取决于您的确切用例。 如果你试图匹配整个单词,可以轻松区分,你可以构建一些哈希索引(例如Dictionary<string, WhatEver> ),你可以轻松搜索。 无论如何 - 取决于大小 - 这可能是非常密集的RAM

以下代码将介绍如何构建它

class FileReference
{
    // elided 

    string File { get; } // may be set in constructor
    IEnumerable<int> Indices { get; } // will get the contents of _index

    public void Add(int index)
    {
        _indices.Add(index);
    }
}

class ReferenceIndex
{
    Dictionary<string, FileReference> _fileReferences = new Dictionary<string, FileReference>();

    public void Add(string fileName, string index)
    {
        if(!_fileReferences.ContainsKey(fileName))
        {
            _fileReferences.Add(fileName, new FileReference(fileName));
        }
        _fileReferences[fileName].Add(index);
    }

    // elided
}

FileReference跟踪单个文件中字符串的索引, ReferenceIndex为单个字符串保存FileReference 对于Dictionary<TKey, TValue>进行哈希处理,访问它的速度非常快。 您可以使用这些类来构建Dictionary<string, ReferenceIndex> ,它跟踪文件中的所有字符串并对这些字符串进行文件引用

Dictionary<string, ReferenceIndex> stringIndex = BuildIndex(fileName);
foreach(var s in searchStrings)
{
    if(stringIndex.ContainsKey(s))
    {
        // do something
    }
}

我刚刚遇到了类似的问题。 代表每个可搜索的文件如下:

public class SearchableFile {
    private readonly HashSet<string> _uniqueLines;
    //private readonly HashSet<string> _uniqueString;

    public string FilePath { get; }

    public SearchableFile(string filePath) {
        _uniqueLines = new HashSet<string>(File.ReadAllLines(filePath));
        //↑You can also split each line if you have many repeating words in each line.
        //_uniqueString = File.ReadAllLines(filePath).SelectMany(singleLine => singleLine.Split(' '));
        FilePath = filePath;
    }

    public bool ContainsCompositeString(string compositeString) {
        return _uniqueLines.Any(singleLine => singleLine.Contains(compositeString));
        //return _uniqueString.Contains(compositeString);
    }
}

然后你可以按原样使用它:

    private static void Main(string[] args) {
        var filePaths = new List<string> { "c://temp.txt" };

        foreach (var filePath in filePaths) {
            FilesOnHdd.Add(new SearchableFile(filePath));
        }
        var csvFiles = new List<string> { "c://temp.csv" };
        foreach (var csvFile in csvFiles) {
            var lines = File.ReadAllLines(csvFile);
            foreach (var line in lines) {
                if (IsHeader(line)) {
                    continue;
                }
                var str = ComposeString(line);

                foreach (var singleFileOnHdd in FilesOnHdd) {
                    var result = singleFileOnHdd.ContainsCompositeString(str);
                    if (result) {
                        // do stuff with the line and bool
                    }
                }
            }
        }
    }

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM