2010-03-30 78 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