2010-03-30 81 views
6

我有大约100k个Outlook邮件项目,每个正文约500-600个字符。我有一个必须搜索每个主体的580个关键字列表,然后在底部添加单词。提高正则表达效率

我相信我已经增加了大多数功能的效率,但它仍然需要花费大量的时间。即使是100封电子邮件也需要4秒左右的时间。

我跑为每个关键字列表两种功能(290个关键字每个列表)。

 public List<string> Keyword_Search(HtmlNode nSearch) 
    { 
     var wordFound = new List<string>(); 
     foreach (string currWord in _keywordList) 
     { 
      bool isMatch = Regex.IsMatch(nSearch.InnerHtml, "\\b" + @currWord + "\\b", 
                RegexOptions.IgnoreCase); 
      if (isMatch) 
      { 
       wordFound.Add(currWord); 
      } 
     } 
     return wordFound; 
    } 

是否有反正我可以提高此功能的效率?

可能放缓下来的另一件事是,我使用的HTML敏捷性包通过一些节点导航,并拉出体(nSearch.InnerHtml)。 _keywordList是一个List项目,而不是一个数组。

+10

别猜了,言归正传吧 – Paolo 2010-03-30 13:23:58

+0

探查我dotTrace,但它不能在Outlook中的加载项工作。 – cam 2010-03-30 13:26:54

+0

从我的经验调用到COM API通常是一个瓶颈(在你的情况下检索100K的项目),你唯一可以做的事情就是尽量减少这些调用的次数。但作为已经由Paolo说这是最好的得到它探查或使用'StopWatch'类,如果你的分析器不支持插件。 – 2010-03-30 13:30:30

回答

7

我假设COM调用nSearch.InnerHtml是相当缓慢的,你重复调用您所检查每一个字。您可以简单地缓存通话结果:

public List<string> Keyword_Search(HtmlNode nSearch) 
{ 
    var wordFound = new List<string>(); 

    // cache inner HTML 
    string innerHtml = nSearch.InnerHtml; 

    foreach (string currWord in _keywordList) 
    { 
     bool isMatch = Regex.IsMatch(innerHtml, "\\b" + @currWord + "\\b", 
               RegexOptions.IgnoreCase); 
     if (isMatch) 
     { 
      wordFound.Add(currWord); 
     } 
    } 
    return wordFound; 
} 

另一个优化是由Jeff Yates提出的。例如。通过使用单一模式:

string pattern = @"(\b(?:" + string.Join("|", _keywordList) + @")\b)"; 
+0

简化模式:'@“(\ b(?:”+ string.Join(“|”,_keywordList)+ @“)\ b)”' – 2010-03-30 14:09:38

+0

@Konrad Rudolph:哦,是的,当然,这样更好。 – 2010-03-30 16:15:28

0

正则表达式可以优化了不少,当你只是要匹配一组固定的常量字符串的。而不是几场比赛,例如对于“冬天”,“胜利”或“袋熊”,您可以匹配"w(in(ter)?|ombat)",例如(杰弗里弗里德尔的书可以给你很多这样的想法)。这种优化也被嵌入到一些程序中,特别是emacs('regexp-opt')。我对.NET不太熟悉,但我认为某人已编程类似的功能 - 谷歌的“正则表达式优化”。

+0

为什么这个表达式比'winter | wombat | win'更有效率?它们都应该被编译成大致相同的自动机(减去实际使你的表达更复杂的捕获组)。我不相信,但不幸的是,我无法找到有关技术细节的良好信息。你有什么好的来源? – 2010-03-30 14:01:02

+0

这并不比'winter | wombat | win'更好,它可以让你不必信任正则表达式编译器就可以做得很好。它*比运行N个单独的比赛要好,这是OP所拥有的。 – 2010-03-30 14:41:02

2

我不认为这是对正则表达式的工作。您最好逐字搜索每个消息,并检查每个单词是否与您的单词列表相对应。通过你的方法,你可以搜索每条消息n次,其中n是你想要查找的单词的数量 - 这也就不足为奇了。

+0

是的,这就是我想要做的。我认为这是最高效/准确的方式。 – cam 2010-03-30 13:42:31

1

一件事,你可以很容易做的就是比赛安剑铮,卓杰在一个全力以赴的话通过构建一个表达式,如:

\ B(:字词1 |单词2 | WORD3 | ....)\ b

然后,您可以预编译模式并重新使用它来查找每封电子邮件的所有发生(不知道如何使用.Net API执行此操作,但必须有一种方法)。

的另一件事是,而不是使用忽略大小写的标志,如果你将所有的都为小写,可能会给你一个小的提升速度(需要个人资料,因为它的实现依赖)。不要忘记在你的个人资料上预热CLR。

+0

如果将单词合并为一个正则表达式,他们需要为每个单词使用组,否则他们将无法跟踪匹配的内容。 – 2010-03-30 13:44:19

+0

@Jeff - 这就是我的想法。但我只是意识到,当使用单词边界时,匹配将始终是单词本身。所以实际上你可以简单地将每个匹配的值添加到wordFound列表中。看到我的答案我的实施。 – 2010-03-30 21:10:12

+0

@Steve:好点。现在看来很明显你指出了。干杯。 – 2010-03-31 02:02:38

2

大部分的时间来失败,所以要尽量减少故障形式的比赛。

如果搜索关键字不频繁,您可以同时测试所有的关键字(使用正则表达式\b(aaa|bbb|ccc|....)\b),然后排除没有匹配的电子邮件。至少有一场比赛的球员会进行彻底搜索。

0

如果正则表达式确实是瓶颈,甚至对其进行优化(通过连接检索词一个表达)没有帮助,考虑使用多模式搜索算法,比如Wu-Manber。

I’ve posted a very simple implementation这里堆栈溢出。它是用C++编写的,但由于代码简单明了,应该很容易将其转换为C#。

注意,这将找到的话随时随地,不只是在单词边界。然而,这可以很容易地测试后您检查文本是否包含任何话;由前后各命中后检查字符或手动 - 无论是再次使用正则表达式(更快现在你只测试单个电子邮件)。

0

如果您的关键字搜索是直接的文字,即不包含进一步的正则表达式模式匹配,则其他方法可能更合适。下面的代码演示了一个这样的方法,该代码只能通过每个电子邮件去一次,你的代码通过每个电子邮件290时(两次)

 public List<string> FindKeywords(string emailbody, List<string> keywordList) 
     { 
      // may want to clean up the input a bit, such as replacing '.' and ',' with a space 
      // and remove double spaces 

      string emailBodyAsUppercase = emailbody.ToUpper(); 

      List<string> emailBodyAsList = new List<string>(emailBodyAsUppercase.Split(' ')); 

      List<string> foundKeywords = new List<string>(emailBodyAsList.Intersect(keywordList)); 


      return foundKeywords; 
     } 
0

去如果你能使用的.Net 3.5+和LINQ,你可以这样做这个。

public static class HtmlNodeTools 
{ 
    public static IEnumerable<string> MatchedKeywords(
     this HtmlNode nSearch, 
      IEnumerable<string> keywordList) 
    { 
     //// as regex 
     //var innerHtml = nSearch.InnerHtml; 
     //return keywordList.Where(kw => 
     //  Regex.IsMatch(innerHtml, 
     //      @"\b" + kw + @"\b", 
     //      RegexOptions.IgnoreCase) 
     //  ); 

     //would be faster if you don't need the pattern matching 
     var innerHtml = ' ' + nSearch.InnerHtml + ' '; 
     return keywordList.Where(kw => innerHtml.Contains(kw)); 
    } 
} 
class Program 
{ 
    static void Main(string[] args) 
    { 
     var keyworkList = new string[] { "hello", "world", "nomatch" }; 
     var h = new HtmlNode() 
     { 
      InnerHtml = "hi there hello other world" 
     }; 

     var matched = h.MatchedKeywords(keyworkList).ToList(); 
     //hello, world 
    } 
} 

...重复使用正则表达式的例子...

public static class HtmlNodeTools 
{ 
    public static IEnumerable<string> MatchedKeywords(
     this HtmlNode nSearch, 
      IEnumerable<KeyValuePair<string, Regex>> keywordList) 
    { 
     // as regex 
     var innerHtml = nSearch.InnerHtml; 
     return from kvp in keywordList 
       where kvp.Value.IsMatch(innerHtml) 
       select kvp.Key; 
    } 
} 
class Program 
{ 
    static void Main(string[] args) 
    { 
     var keyworkList = new string[] { "hello", "world", "nomatch" }; 
     var h = new HtmlNode() 
     { 
      InnerHtml = "hi there hello other world" 
     }; 

     var keyworkSet = keyworkList.Select(kw => 
      new KeyValuePair<string, Regex>(kw, 
              new Regex(
               @"\b" + kw + @"\b", 
               RegexOptions.IgnoreCase) 
               ) 
              ).ToArray(); 

     var matched = h.MatchedKeywords(keyworkSet).ToList(); 
     //hello, world 
    } 
} 
+0

这样好的想法是,如果你真的只想要在keyworkList中包含一个值的所有消息,你可以用'.Any()'替换'.ToList()',并且它会在第一次匹配后返回。 – 2010-03-30 16:11:56

+0

您也可以考虑将IEnumerable 传递给扩展方法,并将关键字转换为正则表达式。然后,您将不会重新生成您扫描的每封电子邮件的值。 – 2010-03-30 16:13:55

1

可能更快。你可以利用这样的正则表达式组:

public List<string> Keyword_Search(HtmlNode nSearch) 
    { 
     var wordFound = new List<string>(); 

     // cache inner HTML 
     string innerHtml = nSearch.InnerHtml; 
     string pattern = "(\\b" + string.Join("\\b)|(\\b", _keywordList) + "\\b)"; 
     Regex myRegex = new Regex(pattern, RegexOptions.IgnoreCase); 
     MatchCollection myMatches = myRegex.Matches(innerHtml); 

     foreach (Match myMatch in myMatches) 
     { 
      // Group 0 represents the entire match so we skip that one 
      for (int i = 1; i < myMatch.Groups.Count; i++) 
      { 
       if (myMatch.Groups[i].Success) 
        wordFound.Add(_keywordList[i-1]); 
      } 
     } 

     return wordFound; 
    }  

这样你只能使用一个正则表达式。而小组的索引应通过1偏移与_keywordList相关,因此行wordFound.Add(_keywordList[i-1]);

UPDATE:

看着我的代码后再次我只是意识到,把比赛变成组实际上是不必要的。正则表达式组有一些开销。相反,您可以从模式中删除括号,然后将匹配自己添加到wordFound列表中。这会产生相同的效果,但速度会更快。

它会是这样的:

public List<string> Keyword_Search(HtmlNode nSearch) 
{ 
    var wordFound = new List<string>(); 

    // cache inner HTML 
    string innerHtml = nSearch.InnerHtml; 
    string pattern = "\\b(?:" + string.Join("|", _keywordList) + ")\\b"; 
    Regex myRegex = new Regex(pattern, RegexOptions.IgnoreCase); 
    MatchCollection myMatches = myRegex.Matches(innerHtml); 

    foreach (Match myMatch in myMatches) 
    { 
     wordFound.Add(myMatch.Value); 
    } 

    return wordFound; 
}  
+0

谢谢史蒂夫,那就是我的意思。如果您不介意,您是否也可以让我们知道在使用ignorecase标志和将正则表达式和文本转换为小写字母并匹配而不忽略大小写之间是否存在性能差异(当您旋转测量循环时,您应该取出正则表达式的创建,但保留innerHtml.toLowerCase()里面)。 – ddimitrov 2010-04-01 14:35:47