xingd.net

.net related techonology
posts - 54, comments - 601, trackbacks - 4, articles - 2

再度提升!.NET脏字过滤算法

Posted on 2008-02-01 20:40 xingd 阅读(2152) 评论(26)  编辑 收藏 所属分类: Framework
再度改进,在脏字可能存在的情况下,例如出现了多个脏字前Length-1部分时,性能相比http://www.cnblogs.com/xingd/archive/2008/01/31/1060425.html中描述的又提升了300%~400%。

直接贴出全部代码了,通过新增的一个byte[char.MaxValue]和BitArray(char.MaxValue),减少了大量的Substring和GetHashCode的调用。耗的内存也不算多,除HashSet外,仅需要144k内存。

引用此文或者使用此代码请说明出处,谢谢,以便于我将来的更新。

2008-02-02修订:if (index > 0 || (fastCheck[text[index]] & 1) == 0) 应去掉index > 0的判断,这个优化考虑的不够成熟。感谢sumtec和灵感之源指出错误。避免最短匹配时,可以在 if (hash.Contains(sub)) 之后,可以加入判断 if ((fastLength[begin] >> Math.Min(j,7)) == 0),然后再return true。

2008-02-03修订:for循环内部的if ((fastCheck[current] & 1== 0)应为if ((fastCheck[current] & 1== 0 && count == j)。修正bug并加入大小写敏感后,效率降低1倍。

public class BadWordsFilter
{
    
private HashSet<string> hash = new HashSet<string>();
    
private byte[] fastCheck = new byte[char.MaxValue];
    
private byte[] fastLength = new byte[char.MaxValue];
    
private BitArray charCheck = new BitArray(char.MaxValue);
    
private BitArray endCheck = new BitArray(char.MaxValue);
    
private int maxWordLength = 0;
    
private int minWordLength = int.MaxValue;

    
public BadWordsFilter()
    {

    }

    
public void Init(string[] badwords)
    {
        
foreach (string word in badwords)
        {
            maxWordLength 
= Math.Max(maxWordLength, word.Length);
            minWordLength 
= Math.Min(minWordLength, word.Length);

            
for (int i = 0; i < 7 && i < word.Length; i++)
            {
                fastCheck[word[i]] 
|= (byte)(1 << i);
            }

            
for (int i = 7; i < word.Length; i++)
            {
                fastCheck[word[i]] 
|= 0x80;
            }

            
if (word.Length == 1)
            {
                charCheck[word[
0]] = true;
            }
            
else
            {
                fastLength[word[
0]] |= (byte)(1 << (Math.Min(7, word.Length - 2)));
                endCheck[word[word.Length 
- 1]] = true;

                hash.Add(word);
            }
        }
    }

    
public string Filter(string text, string mask)
    {
        
throw new NotImplementedException();
    }

    
public bool HasBadWord(string text)
    {
        
int index = 0;

        
while (index < text.Length)
        {
            
int count = 1;

            
if (index > 0 || (fastCheck[text[index]] & 1== 0)
            {
                
while (index < text.Length - 1 && (fastCheck[text[++index]] & 1== 0) ;
            }

            
char begin = text[index];

            
if (minWordLength == 1 && charCheck[begin])
            {
                
return true;
            }

            
for (int j = 1; j <= Math.Min(maxWordLength, text.Length - index - 1); j++)
            {
                
char current = text[index + j];

                
if ((fastCheck[current] & 1== 0)
                {
                    
++count;
                }

                
if ((fastCheck[current] & (1 << Math.Min(j, 7))) == 0)
                {
                    
break;
                }

                
if (j + 1 >= minWordLength)
                {
                    
if ((fastLength[begin] & (1 << Math.Min(j - 17))) > 0 && endCheck[current])
                    {
                        
string sub = text.Substring(index, j + 1);

                        
if (hash.Contains(sub))
                        {
                            
return true;
                        }
                    }
                }
            }

            index 
+= count;
        }

        
return false;
    }
}

Feedback

#1楼    回复  引用    

2008-02-01 22:55 by aysun168 [未注册用户]
谢谢!和上面的一篇一起收藏!

#2楼    回复  引用    

2008-02-02 00:58 by 支持楼主 [未注册用户]
支持楼主!精神可贵!向你学习!

#3楼    回复  引用  查看    

2008-02-02 01:27 by 钢钢      
要是能加上一些必要的注释,我相信将更加精彩......

#4楼    回复  引用    

2008-02-02 02:12 by 网游天下 [未注册用户]
半夜2点来顶你

#5楼    回复  引用  查看    

2008-02-02 09:23 by Wisdom-zh      
这个算法不错.
不过模式匹配, 还是 DFA 速度快啊, 想来脏字也不会太多, 自己构造 DFA 要快不少, 内存耗费也不见得非常多.

#6楼    回复  引用  查看    

2008-02-02 09:51 by Sumtec      
@楼主:
算法不稳定啊……
同一篇280K左右的文章,我匹配出380个,你的算法只匹配了298个,相差太多了,上一次出错是什么原因找到了没有?这样的情况下所获得的性能提升,很可能是以没有匹配出应有的东西为代价的。如果都匹配上的话,说不定要补充一些语句,是否会消耗大量性能就不好说了。有句话叫做20%占用80%的资源,所以如果我们两个算法之间的匹配数量差异超过10个,那么性能上的比较就没有意义了。至于你算法里面到底什么地方导致某些单字没有被检索出来,我一眼没有看出来,我想还是你查一下比我快。我这次不打算给你发报告了,因为我发现你的算法目前连脏字表文件都没有全检出来,你可以试一下通过这个来看看算法到底为什么丢了一些匹配。

同一篇文章,目前你的这个算法用5ms匹配298个条目,我需要用19ms匹配出380个条目。不知道等你的算法稳定后,是否还是这个数值。

另外,目前你的改进基本上主要还是动用“空间换时间”的思路,算法上可以说没有性质上的改进。因此,我没有太大的动力来改进自己的算法效率,因为你通过空间换时间,我也一样可以空间换时间。

#7楼 [楼主]   回复  引用  查看    

2008-02-02 12:06 by xingd      
@Sumtec
原来的算法错误已经在原文中加入修订了。

原来的脏字库中会有长的脏字包含短的脏字的情况,我的算法是发现最短的就返回了,或许这就是380 vs 298的原因。存在bug也很正常,在我把算法合并到网站上投入使用的时候会进行进一步的测试,如果有修改也会及时更新我的博客。

空间换时间是很多算法的根本,但是空间如何组织,记录哪些特征信息,是算法改进的关键。

算法的稳定性我是一定会解决的,我走的是工业路线,从实用出发,在使用中提升。

阁下这次的回复显得有点失风度,呵呵,得罪了。我只不过是有了新的想法,看到一个好的结果,很兴奋的发出来。发现其中的问题持续改进,也是一种乐趣,但是阁下似乎对我的算法表现出来到的不稳定颇为不屑。我只是要跟自己不断进行比较,从来就没有讲过任何一个跟你的算法效率的对比。就算我的代码不稳定,不可否认的是每个版本都比前一个版本要好。

#8楼    回复  引用  查看    

2008-02-02 13:01 by Sumtec      
@楼主:
呵呵,有失风度的地方多多包涵。不过380 vs 298 的原因我看了差异了,确实是没有检索出来。比如“功”这个单字就没有照出来,另外用您的算法查脏字表本身,确实是有条目没有被查出来。

如果我有表现出来不屑,首先道歉,其次解释一下,确实没有这个意思。这么说吧,我个人认为,如果说有两种算法,空间复杂度近似,比如说O(c*m)和O(1*m),这种就可以认为是类似。那么在类似的前提下,效率不同,那就是算法的功劳。否则,比如说我们的脏字检索,如果通过开通更大的映射表来解决效率问题,确实有可能提高,但是这样的提高就不能体现这个算法的优势了。原因很简单,两个不同的算法,通常都有可能通过增加内存消耗,来提高效率。

换个比方吧:比如我们的机器,我想如果比较1个AMD的CPU和1个Intel的CPU有什么差异,这个比较值得研究。但是如果讨论变成了:我这个装AMD CPU的服务器,原来只有1个CPU,现在我加到8个了,所以变得很快……这个讨论起来感觉就有点那个了,因为你肯定也会像,那我加几个Intel CPU,不也一样能够更快吗?Money换Time嘛。

另外,你原来那个版本的算法确实是很好啊,我没有想要非争个你高我低,面红耳赤的。我之所以要跟你的算法作比较,无非也是想要找一个更优秀的算法。如果说你的比我的更快,我会这么处理:
1、思考为什么我的思路会更慢;
2、采用你的方案。

前提是,你的算法确实正确。目前问题就在这里了:得到的结果确实是不正确的。

PS:等你的算法能够检出脏字表文件里面的所有条目,我会再测一次,到时候可能你的代码会不一样,到时候我们再分析一下思路上的差异导致了那些结果上的差异。现在我们争论太多也不会出一个结果的,对吧?

#9楼    回复  引用    

2008-02-02 13:23 by kcaz [未注册用户]
由于返回的是最短匹配,所以数目上有差距是可能的。这个问题我在第一篇文章的回复里也提出过的,但是就脏字过滤的使用效果来说,这点基本没什么影响。

若要完全识别出脏字词组,那么除了构建语义pattern或是建立相应词典,我也想不到很直接的方法了。

#10楼 [楼主]   回复  引用  查看    

2008-02-02 14:20 by xingd      
看了一下,我发给你的脏字表中并没有“功”这个单字,只有一个"x x 功”,文件中的脏字字典应该只用'|'分隔,不要用空格。从原理上来看,是不可能有单字符的脏字漏过charCheck的判断。

避免最短匹配也是很容易做到的,在 if (hash.Contains(sub)) 之后,可以加入判断 if ((fastLength[begin] >> Math.Min(j,7)) == 0),然后再return true,也不会对效率有很大的影响。

从我在北航计算机系读完大一后就退学开始,我就选择了一条应用开发的道路,我对算法复杂性上的关注比较少,更多的是从实用的角度,追求pragmatic programming。通过144k的固定空间换取20多倍的效率提升,从实际运营的角度是,是完全值得的。

#11楼    回复  引用  查看    

2008-02-02 15:07 by Sumtec      
这个……确实不好意思,因为我们对断字的方法不一样,我这边会根据空格作为分隔符,所以会有“法”这个词。
抛开这个不说吧,因为这个也许会有点争议。但至少目前打开脏字表文件本身来检索,其繁体字的台X,X湾什么的,有好几个是没有被检查出来的。

#12楼    回复  引用  查看    

2008-02-02 17:40 by 灵感之源      
@xingd

如果要获取所有匹配的脏字,是否应该改动如下:

HasBadWord(string text, ref List FoundWords)
{
//去掉所有return替换为found=true标记,然后FoundWords.Add(脏字)

//最后,return found;
}

#13楼    回复  引用    

2008-02-02 20:18 by 时尚品牌 [未注册用户]
--引用--------------------------------------------------
灵感之源: 算法确实有问题,以下情况无法匹配:

目标内容包含中英混合的内容,如脏字为英文,或者中英文混合,如:

a.目标内容:“abcboxundefg”,关键字是“boxun”;

或者

b.目标内容:“abcB样defg”,关键字是“B样”;

或者

c.目标内容:“abc屄defg”,关键字是“屄”;



--------------------------------------------------------
这个有点难度

#14楼 [楼主]   回复  引用  查看    

2008-02-03 00:13 by xingd      
问题已经发现了一个,正文里给出修订了。

昨天晚上去机房忙了一个通宵,下午回家睡觉了。

#15楼    回复  引用  查看    

2008-02-03 00:52 by 怪怪      
路过, 文无第一, 文无第一....

#16楼    回复  引用  查看    

2008-02-03 11:35 by Sumtec      
@楼主:
根据我的测试,下面这段话是有Bug的,能否说一下他的含义:

if ((fastCheck[current] & 1) == 0)
{
++count;
}

我是没有明白,为什么如果在检索的时候发现某个字符不在脏字表中的起始符,就可以将“跳跃字符数”加1?
比如说脏字表:
ABCDE
JCKED
BC
CE
待检文本:
ABCEDXX
当扫描到A的时候,就会进入扫描过程。接下来会扫描
AB
ABC
ABCE
ABCED
但是由于E和D都不在脏字表的第一个字符里面,因此会导致++count执行两次,最终count==3
那么下一个扫描的字符就变成了第4个字符E,这样的话BC和CE都没有机会被扫描到。这是我和你的扫描结果不一样的另外一个重要原因。我把这段话去掉之后,部分的解决了问题,但是仍然有无法正常匹配的情况。我会给你寄一个简单的样本,你如果有兴趣,可以测试一下。

另外我今天才发现,我和你之间的速度比较是有点不公平的,因为你的算法是大小写敏感的,而我的算法是大小写敏感的。大小写敏感加上去之后,速度会变慢很多的,你可以试试。

#17楼 [楼主]   回复  引用  查看    

2008-02-03 12:59 by xingd      
@Sumtec
考虑不够成熟的一个优化,需要加个条件,应该是
if ((fastCheck[current] & 1) == 0 && count == j)
如果发现连续的不为第一字符的,就跳过。

大小写我测试过对测试文本tolower,当然是放到循环内的。

修复发现的两个优化过度的bug,同时加上tolower,最长匹配,的确性能有一倍多的降低。

我觉得检索方面的优化已经没有太直接的方法了,使用更多空间也不一定会有明显的效率改进。到目前为止,我的算法还算是简单易理解的,如果对效率还不满,就需要在substring, gethashcode和忽略大小写方面改进了。

#18楼    回复  引用  查看    

2008-02-03 13:12 by Sumtec      
@楼主:
那我绝对建议你对ToLower做一个优化。这么说吧,同样是对一个大约280K个字符的文本进行检索:
原来没有执行ToLower的时候,大致消耗10ms左右的时间检索完毕。
但是算上ToLower的时间后,消耗的时间就变成了4807 ms了。

可以说光是做ToLower的操作,就够你算法核心的本身处理将近500遍了。与其在fastCheck上面空间换时间,还不如做这一个优化呢。由于你现在并没有作HashCode优化,所以我建议你不必做HashCode优化了,撑死了你也只能优化出10ms的时间来。

#19楼 [楼主]   回复  引用  查看    

2008-02-03 13:31 by xingd      
@Sumtec
这个当然跟文本长度有关系了,因为tolower涉及到内存分配,我测试用的文本只有68k,所以加上tolower影响不大。前面的blog提到我是在大众点评网工作的,我们的会员点评长度都在2k以内的,所以影响不大。

长文本的时候肯定不能简单地tolower了,等待大小写敏感的代码稳定后,我会加上忽略大小写的功能。

#20楼    回复  引用  查看    

2008-02-03 14:24 by 灵感之源      
加入判断 if ((fastLength[begin] >> Math.Min(j,7)) == 0),会造成无法匹配的情况,譬如

目标内容:“fdfl;[xxx]gkfg”,关键字“xxx”是“法xx”的拼音;

#21楼 [楼主]   回复  引用  查看    

2008-02-03 14:41 by xingd      
if ((fastLength[begin] >> Math.Min(j,7)) == 0)是为了最长匹配的,仅用于需要找出所有可能的脏字的情况。

如果脏字表里有xxx开头的其他脏字,加入if会尝试更长的匹配。因为只有一个return,所以会出现你说的无法匹配的情况。如果通过日志或者Console报告匹配情况,就要加上if ((fastLength[begin] >> Math.Min(j,7)) == 0),仅判断是否包含的时候,不必要加。

#22楼    回复  引用  查看    

2008-02-03 15:20 by 灵感之源      
@xingd

如果要获取所有匹配的脏字,是否应该改动如下:

HasBadWord(string text, ref List<string> FoundWords)
{
//去掉所有return替换为found=true标记,然后FoundWords.Add(脏字)

//最后,return found;
}

#23楼 [楼主]   回复  引用  查看    

2008-02-03 15:35 by xingd      
@灵感之源
对,就是这个意思。如果需要匹配所有情况,就要加上if ((fastLength[begin] >> Math.Min(j,7)) == 0,同时对匹配到的作记录。如果只需要判断是否包含,就不需要。

#24楼    回复  引用    

2008-03-13 10:49 by 在线代理 [未注册用户]
居然一句注释都没有,难道高手都是这样的吗。嫌弃代码短了,不需要注释

#25楼    回复  引用  查看    

2008-03-25 21:28 by 农夫三拳      
多模式匹配吗? DSFA, FS,WM

#26楼    回复  引用    

2008-06-18 17:51 by zzzzz [未注册用户]
不用移位行不行,看着有点晕

标题  
姓名  
主页
Email (只有博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2008-02-03 13:01 编辑过


相关链接:

历史上的今天:
2005-02-01 BinaryFormatter序列化实例(二)