快速文本预处理

时间:2010-07-29 17:29:09

标签: c# regex text-processing

在我的项目中,我一般都使用文本。我发现预处理可能非常慢。所以我想问你是否知道如何优化我的代码。流程是这样的:

获取HTML页面 - > (对于纯文本 - >词干 - >删除停用词) - >进一步的文字处理

括号中有预处理步骤。该应用程序运行在大约10.265秒,但预处理需要9.18秒!这是预处理50个HTML页面的时间(不包括下载)。

我使用HtmlAgilityPack库将HTML转换为纯文本。这很快。转换1个文档需要2.5ms,所以它相对正常。

问题出现了。阻止一个文档需要120毫秒。不幸的是,那些HTML页面是波兰语。用C#编写的波兰语不存在词干。我知道只有2个免费使用Java编写:stempel和morfologic。我借助IKVM软件将stempel.jar预编译为stempel.dll。所以没有其他事可做了。

消除停用词也需要很多时间(1个文档约70毫秒)。它是这样完成的:


result = Regex.Replace(text.ToLower(), @"(([-]|[.]|[-.]|[0-9])?[0-9]*([.]|[,])*[0-9]+)|(\b\w{1,2}\b)|([^\w])", " ");
while (stopwords.MoveNext())
{
   string stopword = stopwords.Current.ToString();                
   result = Regex.Replace(result, "(\\b"+stopword+"\\b)", " ");                               
}
return result;

首先我删除所有数字,特殊字符,单字和双字母单词。然后在循环中删除停用词。大约有270个停用词。

是否可以加快速度?

修改

我想要做的是删除所有不超过2个字母的单词。所以我想把所有特殊的字符(包括'。',',','?','!'等)数字,停止字样。我只需要用于数据挖掘的纯文字。

6 个答案:

答案 0 :(得分:15)

迭代替换单词将成为您实现的最大瓶颈。在每次迭代中,您必须扫描整个字符串以获取停用词,然后替换操作必须分配一个新的字符串并且用替换后的文本填充它。这不会很快。

一种更有效的方法是对字符串进行标记化并以流式方式执行替换。将输入划分为由适当的空格或分隔符分隔的单个单词。您可以逐步执行此操作,因此您无需分配任何额外的内存来执行此操作。对于每个单词(标记),您现在可以在停用词的哈希集中执行查找 - 如果找到匹配项,则在将最终文本流式传输到单独的StringBuilder时将替换它。如果令牌不是停用词,只需将其流出StringBuilder未修改。此方法应具有O(n)性能,因为它只扫描字符串一次并使用HashSet执行停用词查找。

以下是我希望表现更好的一种方法。虽然它不是完全流式传输(它使用分配了一系列附加字符串的String.Split()),但它在一次传递中完成所有处理。改进代码以避免分配额外的字符串可能不会带来很大的改进,因为你仍然需要提取子字符串来执行与你的停用词的比较。

下面的代码会返回排除所有停用词的单词列表,并且会在结果中包含两个字母或更短的单词。它也对停用词使用不区分大小写的比较。

public IEnumerable<string> SplitIntoWords( string input,
                                           IEnumerable<string> stopwords )
{
    // use case-insensitive comparison when matching stopwords
    var comparer = StringComparer.InvariantCultureIgnoreCase;
    var stopwordsSet = new HashSet<string>( stopwords, comparer );
    var splitOn = new char[] { ' ', '\t', '\r' ,'\n' };

    // if your splitting is more complicated, you could use RegEx instead...
    // if this becomes a bottleneck, you could use loop over the string using
    // string.IndexOf() - but you would still need to allocate an extra string
    // to perform comparison, so it's unclear if that would be better or not
    var words = input.Split( splitOn, StringSplitOptions.RemoveEmptyEntries );

    // return all words longer than 2 letters that are not stopwords...
    return words.Where( w => !stopwordsSet.Contains( w ) && w.Length > 2 );
}

答案 1 :(得分:3)

好的,我知道SO不是一个纯粹的论坛,也许我不应回答我自己的问题,但我想与我的结果分享。

最后,感谢你们,我设法更好地优化了我的文本预处理。首先,我从我的问题中得到了更长的表达(在Josh Kelley的回答之后):

[0-9]|[^\w]|(\b\w{1,2}\b)

它与第一个相同,但非常简单。然后再按照Josh Kelley的建议,我把这个正则表达式组装起来。将表达式编译到程序集中的一个很好的例子我发现here。我这样做了,因为这个正则表达式使用了很多次。关于编译的正则表达式的几篇文章的讲座后,这是我的决定。我在删除了停用词之后删除了最后一个表达式(没有真正的意义)。

因此12KiB文本文件的执行时间约为15ms。这仅用于上述表达。

最后一步是停用词。我决定对3个不同的选项进行测试(执行时间是针对相同的12KiB文本文件)。

一个大的正则表达式

用所有停用词并编译成汇编(mquander的建议)。这里没什么好说的。

  • 执行时间:~215ms

与string.replace()

人们说这可能比正则表达更快。因此,对于每个停用词,我使用了string.Replace()方法。结果需要很多循环:

  • 执行时间:~65ms

LINQ

LBushkin提出的方法。没什么好说的。

  • 执行时间:~2.5ms

我只能说哇。只需比较第一个和最后一个的执行时间!非常感谢LBushkin!

答案 2 :(得分:2)

为什么不动态构造匹配正则表达式匹配任何一个停用词的怪物,而不是在循环中替换正则表达式,然后运行一个替换,替换为什么?如果您的停用词是“what”,“ok”和“yes”,那就像"\b(what|ok|yeah)\b"。这似乎可能更有效率。

答案 3 :(得分:1)

加速你的正则表达

你的正则表达式可以使用一些工作。

例如,这一行:

result = Regex.Replace(result, "(\\b"+stopword+"\\b)", " ");

使用括号捕获停用词供以后使用,然后它永远不会使用它。也许.NET正则表达式引擎足够聪明,可以在这种情况下跳过捕获,也许不是。

这个正则表达式太复杂了:

"(([-]|[.]|[-.]|[0-9])?[0-9]*([.]|[,])*[0-9]+)|(\b\w{1,2}\b)|([^\w])"
  • "([-]|[.]|[-.]|[0-9])?""([-.0-9])?"相同。 (除非你试图将“ - 。”作为你的可能性之一吗?我现在不会假设。)如果你不需要捕捉(你的例子中没有),那么它与{相同} {1}}。
  • "[-.0-9]?""[-.0-9]?"之前有点多余。您可以进一步将其简化为"[0-9]*"
  • 同样,如果您不需要捕获,则"[-.]?[0-9]*""([.]|[,])*"相同。

最后,测试compiling your regexes是否可以带来更好的效果。

减少正则表达式和字符串操作

构造一堆字符串,组成一堆Regex对象,然后丢弃它们,就像你在这个循环中所做的那样,可能不是很快:

"[,.]*"

尝试将停用词预处理为Regex对象数组或创建一个预编译的怪物Regex(正如其他人建议的那样)。

重构您的算法

看起来你只对处理词干,非禁言,文字,而不是标点符号,数字等感兴趣。

为此,您的算法采用以下方法:

  • 干掉所有文字(包括停用词?)。
  • 使用正则表达式(不一定是最快的方法)来替换(需要不断重新排列字符串的主体)非空白字。
  • 使用正则表达式(再次,不一定是最快的方法)来替换(再次)带有空格的停用词,一次一个停用词。

我开始在这里写另一种方法,但是LBushkin打败了我。做他说的话。请记住,作为一般规则,更改算法通常会提供比微优化更大的改进,例如提高正则表达式的使用。

答案 4 :(得分:0)

可能会进入Schlemiel the Painter problem。在C#(和其他语言)中,当您追加或连接字符串时,实际上是在创建一个全新的字符串。在循环中执行此操作通常会导致大量内存分配,否则可以避免。

答案 5 :(得分:0)

我同意mquander,这里有更多信息。 每次使用正则表达式时,C#都会创建一个与文本匹配的表。如果你只调用正则表达式函数几次,那就很好了,但你在这里做的是创建大约270个新表并为每个html文档删除它们。

我要尝试的只是创建一个Regex对象,并使用|运算符匹配所有不同的停用词和第一个过滤器。之后,您应该使用正则表达式编译进行汇编,以便让JIT生成机器代码。

http://en.csharp-online.net/CSharp_Regular_Expression_Recipes%E2%80%94Compiling_Regular_Expressions

你应该看到这个

的戏剧性加速