为不需要的字符串</string>过滤IEnumerable <string>

时间:2013-02-22 22:34:29

标签: c# linq optimization

编辑:我收到了一些非常好的建议,我会尝试通过它们并在某些时候接受答案

我有一个很大的字符串列表(800k),我想尽快过滤掉不需要的单词列表(最终亵渎,但可能是任何东西)。

我最终希望看到的结果是

等列表
Hello,World,My,Name,Is,Yakyb,Shell

会变成

World,My,Name,Is,Yakyb
经过

检查后

Hell,Heaven.

到目前为止,我的代码是

 var words = items
            .Distinct()
            .AsParallel()
            .Where(x => !WordContains(x, WordsUnwanted));

public static bool WordContains(string word, List<string> words)
    {
        for (int i = 0; i < words.Count(); i++)
        {
            if (word.Contains(words[i]))
            {
                return true;
            }
        }
        return false;
    }

这目前需要大约2.3秒(9.5 w / o并行)来处理800k字,这对于一次性来说并不是什么大不了的事。但是,学习过程是否有更快的处理方式?

不需要的单词列表长100个字
没有一个词包含标点符号或空格

  1. 采取措施删除所有列表中的重复项
  2. 步骤查看是否使用数组更快(不是)有趣地将参数单词更改为字符串[]使其慢25%
  3. 添加AsParallel()的步骤将时间缩短到~2.3秒

5 个答案:

答案 0 :(得分:1)

几件事

改动1(漂亮又简单): 通过在Distinct方法上使用HashSet,我能够加速运行(分数)。

var words = new HashSet<string>(items) //this uses HashCodes
        .AsParallel()...

改变2(跟我一起;)): 关于@Tim的评论,包含可能无法为您提供足够的搜索黑名单词。例如 Takeshita 是街道名称。

你已经确定你想要这个词的有限状态(又名Stemmed)。例如,对于苹果,我们会将其视为Apple。为此,我们可以使用诸如Porter Stemmer等词干算法。

如果我们要干一个字然后我们可能不需要做包含(x),我们可以使用等于(x)或甚至更好地比较HashCodes(最快的方式)。

var filter = new HashSet<string>(
    new[] {"hello", "of", "this", "and", "for", "is", 
        "bye", "the", "see", "in", "an", 
        "top", "v", "t", "e", "a" }); 

var list = new HashSet<string> (items)
            .AsParallel()
            .Where(x => !filter.Contains(new PorterStemmer().Stem(x)))
            .ToList();

这将比较其哈希码上的字词 int == int

使用stemmer并没有减慢速度,因为我们使用HashSet对其进行补充(对于过滤后的列表,bigO为1)。这返回了更大的结果列表。

我正在使用位于Lucene.Net代码中的Porter Stemmer,这不是线程安全因此我们每次新建一个

Alteration 2的问题,Alteration 2a:与大多数自然语言处理一样,并不简单。

时会发生什么
  1. 这个词是被禁词的组合&#34; GrrArgh&#34; (Grr和Argh被禁止的地方)
  2. 这个词拼写错误&#34; Frack&#34;,但仍然与禁止的词有相同的含义(对论坛ppl抱歉)
  3. 这个单词拼写为空格&#34; G r r&#34;。
  4. 你这个乐队的单词不是一个单词,而是一个短语,可怜的例子:&#34;一个桶的儿子&#34;
  5. 通过论坛,他们利用人类来弥补这些差距。

    或者引入了白名单(假设您提到了bigO,我们可以说它的性能将达到2n ^ 2,因为我们为每个项目做了2个列表,不要忘记删除领先的人,如果我没记错的话你会留下n ^ 2,但我的bigO上有点生锈)

答案 1 :(得分:1)

更改您的WordContains方法以使用单个Aho-Corasick搜索而不是~100包含调用(当然只需初始化Aho-Corasick搜索树一次)。

您可以在http://www.codeproject.com/script/Articles/ViewDownloads.aspx?aid=12383找到一个开源实施。

StringSearch课程开始后,您将为每个800k字符串调用方法public bool ContainsAny(string text)

无论你的不受欢迎的词汇列表有多长,一次通话都会花费O(字符串的长度)时间。

答案 2 :(得分:0)

尝试使用名为Except的方法。

http://msdn.microsoft.com/en-AU/library/system.linq.enumerable.except.aspx

var words = new List<string>() {"Hello","Hey","Cat"};
var filter = new List<string>() {"Cat"};

var filtered = words.Except(filter);

还怎么样:

var words = new List<string>() {"Hello","Hey","cat"};
var filter = new List<string>() {"Cat"};
// Perhaps a Except() here to match exact strings without substrings first?
var filtered = words.Where(i=> !ContainsAny(i,filter)).AsParallel();    
// You could experiment with AsParallel() and see 
// if running the query parallel yields faster results on larger string[]
// AsParallel probably not worth the cost unless list is large
public bool ContainsAny(string str, IEnumerable<string> values)
{
   if (!string.IsNullOrEmpty(str) || values.Any())
   {
       foreach (string value in values)
       {
             // Ignore case comparison from @TimSchmelter
             if (str.IndexOf(value, StringComparison.OrdinalIgnoreCase) != -1) return true;

             //if(str.ToLowerInvariant().Contains(value.ToLowerInvariant()))
             // return true;
       }
   }

   return false;
}

答案 3 :(得分:0)

啊,根据“坏”列表中的匹配过滤单词。这是一个克隆问题,已经测试了许多程序员的完整性。来自斯肯索普的朋友写了一篇关于它的论文。

你真正想要避免的是一个在O(lm)中测试单词的解决方案,其中l是要测试的单词的长度,m是坏单词的数量。为了做到这一点,你需要一个解决方案,而不是循环坏词。我曾经认为正则表达式可以解决这个问题,但我忘了典型的实现有一个内部数据结构,每次交替都会增加。正如其他解决方案之一所说,Aho-Corasick就是这样做的算法。标准实现找到所有匹配项,因为你可以在第一场比赛中拯救,所以你的比赛会更有效率。我认为这提供了一个理论上最优的解决方案。

答案 4 :(得分:0)

我有兴趣看看我是否能想出一个更快的方法 - 但我只管理了一点优化。那就是检查在另一个字符串中出现的字符串的索引,因为它首先看起来比'contains'略快,然后让你指定不区分大小写(如果这对你有用)。

下面是我写的测试类 - 我使用了&gt; 100万字,并且在所有情况下使用区分大小写的测试。它测试你的方法,也是我试图在运行中建立的正则表达式。你可以亲自尝试一下,看看时间;正则表达式的工作速度不如您提供的方法快,但我可能会错误地构建它。我之前使用(?i)(word1 | word2 ...)来指定正则表达式中的不区分大小写(我很想知道如何优化它 - 它可能会遇到经典的回溯问题!)。

随着更多“不需要的”词语被添加,搜索方法(无论是正则表达式还是提供的原始方法)似乎进展缓慢。

无论如何 - 希望这个简单的测试可以帮助你:

    class Program
{


    static void Main(string[] args)
    {
        //Load your string here - I got war and peace from project guttenburg (http://www.gutenberg.org/ebooks/2600.txt.utf-8) and loaded twice to give 1.2 Million words
        List<string> loaded = File.ReadAllText(@"D:\Temp\2600.txt").Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries).ToList();

        List<string> items = new List<string>();
        items.AddRange(loaded);
        items.AddRange(loaded);

        Console.WriteLine("Loaded {0} words", items.Count);

        Stopwatch sw = new Stopwatch();

        List<string> WordsUnwanted = new List<string> { "Hell", "Heaven", "and", "or", "big", "the", "when", "ur", "cat" };
        StringBuilder regexBuilder = new StringBuilder("(?i)(");

        foreach (string s in WordsUnwanted)
        {
            regexBuilder.Append(s);
            regexBuilder.Append("|");
        }
        regexBuilder.Replace("|", ")", regexBuilder.Length - 1, 1);
        string regularExpression = regexBuilder.ToString();
        Console.WriteLine(regularExpression);

        List<string> words = null;

        bool loop = true;

        while (loop)
        {
            Console.WriteLine("Enter test type - 1, 2, 3, 4 or Q to quit");
            ConsoleKeyInfo testType = Console.ReadKey();

            switch (testType.Key)
            {
                case ConsoleKey.D1:
                    sw.Reset();
                    sw.Start();
                    words = items
                        .Distinct()
                        .AsParallel()
                        .Where(x => !WordContains(x, WordsUnwanted)).ToList();

                    sw.Stop();
                    Console.WriteLine("Parallel (original) process took {0}ms and found {1} matching words", sw.ElapsedMilliseconds, words.Count);
                    words = null;
                    break;

                case ConsoleKey.D2:
                    sw.Reset();
                    sw.Start();
                    words = items
                        .Distinct()
                        .Where(x => !WordContains(x, WordsUnwanted)).ToList();

                    sw.Stop();
                    Console.WriteLine("Non-Parallel (original) process took {0}ms and found {1} matching words", sw.ElapsedMilliseconds, words.Count);
                    words = null;
                    break;

                case ConsoleKey.D3:
                    sw.Reset();
                    sw.Start();
                    words = items
                        .Distinct()
                        .AsParallel()
                        .Where(x => !Regex.IsMatch(x, regularExpression)).ToList();

                    sw.Stop();
                    Console.WriteLine("Non-Compiled regex (parallel) Process took {0}ms and found {1} matching words", sw.ElapsedMilliseconds, words.Count);
                    words = null;
                    break;

                case ConsoleKey.D4:
                    sw.Reset();
                    sw.Start();
                    words = items
                        .Distinct()
                        .Where(x => !Regex.IsMatch(x, regularExpression)).ToList();

                    sw.Stop();
                    Console.WriteLine("Non-Compiled regex (non-parallel) Process took {0}ms and found {1} matching words", sw.ElapsedMilliseconds, words.Count);
                    words = null;
                    break;

                case ConsoleKey.Q:
                    loop = false;
                    break;

                default:
                    continue;
            }
        }
    }

    public static bool WordContains(string word, List<string> words)
    {
        for (int i = 0; i < words.Count(); i++)
        {
            //Found that this was a bit fater and also lets you check the casing...!
            //if (word.Contains(words[i]))
            if (word.IndexOf(words[i], StringComparison.InvariantCultureIgnoreCase) >= 0)
                return true;
        }
        return false;
    }
}