问题(检查编辑说明)
我有一个大约1500个字符串的列表,对于每个字符串,我必须检查目录(和子目录)中的任何文件是否包含该字符串(大约有4000个文件)。
代码
我现在拥有的是这两种变体:
原始
foreach(var str in stringList)
{
allFiles.Any(f => File.ReadAllText(f).Contains(str));
}
第二个变体(使用ReadLines而不是ReadAllText,正如Vlad this question中建议的那样)
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
}
答案 0 :(得分:3)
文件是否包含任何字符串?
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 :(得分:3)
我认为最快的方法是:
Aho-Corasick的实施可用here。
我编写了一个简单的程序,使用来自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));
将每个文件完全加载到内存中并将枚举每个文件中的字符(如上所述)进行比较是值得的,看是否有任何差异。对于非常大的文件,避免将其全部内容加载到内存中可能很重要。
答案 2 :(得分:2)
您正在阅读每个字符串的所有文件。
如何以相反的方式做到这一点?循环遍历所有文件:
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();
答案 3 :(得分:1)
这在很大程度上取决于您的确切用例。如果您试图匹配整个单词,可以轻松区分,您可以构建某种散列索引(例如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
s。对于Dictionary<TKey, TValue>
进行哈希处理,访问它的速度非常快。您可以使用这些类来构建Dictionary<string, ReferenceIndex>
来跟踪文件中的所有字符串并对这些字符串进行文件引用
Dictionary<string, ReferenceIndex> stringIndex = BuildIndex(fileName);
foreach(var s in searchStrings)
{
if(stringIndex.ContainsKey(s))
{
// do something
}
}
答案 4 :(得分:0)
我最近遇到了类似的问题。将每个可搜索文件表示为:
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
}
}
}
}
}