玩转C科技.NET

从学会做人开始认识这个世界!http://volnet.github.io

导航

一个较完整的关键字过滤解决方案

MerryChristmas

圣诞节将至,虽然经济危机让这个寒冬雪上加霜,但我们仍应该积极地面对生活、朋友、亲人,把我们最快乐的一面传递给别人。大V在这里提前祝大家圣诞节快乐了!

看了暧昧的赵同学的《一个较完整的关键字过滤解决方案(上)》(http://www.cnblogs.com/JeffreyZhao/archive/2008/12/22/filter-forbidden-word-solution.html)的文章之后,突然手痒就决定也做一个类似功能的过滤器。因为我本人也是赵同学的粉丝,外加这标题也是盗版的,就自语山寨版好了。

一样的思路:HttpModule,就像拿着筛子筛麦子一样,黄色的、政治的、流氓的统统给他转成文明用语,就像cnBeta一样,一样地暴力一样地文明。

通过这篇文章您可以学会:

  • 创建添加一个HttpModule
  • 截获一段Http流
  • 获取全部的页面内容
  • 一段简陋的文本替换逻辑
  • 基于过滤器平台扩展自己的过滤器

需求概要

简单地说,因为是受赵同学文章的影响才写这个小玩意的,因此需求也是一样的,无非就是像cnBeta一样,将所有涉及敏感流氓政治词汇替换成文明用语。一样的不小心,在设计网站前,咱也不知道政府有“保护下一代”的计划,因此开放的平台让那些好事的主儿言论自由,后来发现现在的市民素质怎么逐年下降,主要表现为非主流增多以及非文明用语飙升,Google智能提示随便输入一词,关联的黄色信息直接爬上首位。这可不符合文明上网的宗旨。可是你丫的这年头办个网站容易么我,一不小心就新建了一百多个可以用户自行输入的页面,咱要挨个修改还不如关门算了。

赵同学非常有心,就创建了那个文明上网卫士,这下好了,把卫士往那里一部署,你再输入“中国”就都给你转成“天朝”,你一色性大发就给你转为“文明用语”,或者干脆都让你变成“***”,顺便给圣诞节增添雪花。不知道赵同学的(下)要出个什么内容,就觉着这统统给你转换了不是太好。比如是cnbeta,那些评论转瞬即逝,谁也无所谓,骂过了,留脚印了,就没人关心了,几乎没有人翻开去年的新闻细细品味那里的天朝人民和文明用语。但是如果是咱博客园,评论语也是有价值的,如果哪天改革开放了,文明用语的定义放宽了,这些内容还可以重新定义过。因此在提交的时候就过滤有的时候就显得不够合适了。

而我的方式则是在输出的时候篡改。而实际写入数据库的内容仍然是内容作者的原始版本,咱到时候才可以有法可依。

 

创建添加一个HttpModule

虽然添加一个HttpModule的文章很多也很专业,但这里还是有必要简要地说一下。(查看更多

HTTP Module

这幅图主要解释了HTTP Module在一次请求中的作用。首先任何向IIS发出的请求将调用相应的ISAPI进行处理,因此我们在IIS中去掉aspx的后缀匹配,将无法返回正确的请求,同样,像MSDN可以在IIS中添加mspx,同样指定由ASPNET_ISAPI进行处理,跟aspx没有任何的区别。但是ISAPI是一个本地非托管程序,通常由C++实现,因此增加了程序员的开发成本,ASP.NET平台允许大家通过ASP.NET Module的方式,使用托管代码来编写管道模式的插件来自由增加这些中间步骤。

实现和部署一个HTTP Module是非常简单的,只需要创建一个类,实现IHttpModule接口,就可以了。这个接口很简单,只有Init和Dispose两个方法。在Init中我们可以得到当前HttpApplication的实例,有了这个实例,自然就可以得到更多跟我们实例相关的信息了。HttpModule通常是通过订阅事件的方式,来做相应的处理。

public void Init(HttpApplication context) {
    context.BeginRequest += new EventHandler(context_BeginRequest);
    context.EndRequest += new EventHandler(context_EndRequest);
}

通过事件处理程序的sender就可以获得HttpApplication对象了,至于这个对象怎么处理就仁者见仁了。怎么样,简单吧?

编译这个程序集,你将得到一个单独的dll,它就是你的HttpModule。因为HttpModule被设计成可以自由发布并使用的结构,所以它是一个单独的可拷贝后部署的dll,哪怕你就是在你的WebSite项目中编写它们,它们也与你的程序没有丝毫的关系。因此你需要部署它们。

要使用HttpModule,需要你使用IIS而不是Visual Studio提供的轻量级的开发服务器。部署该程序集需要你修改web.config。针对IIS7以及IIS7以下版本,这个配置是不一样的(准确的说是IIS7的集成管道模式)。打开要应用该HttpModule的项目的web.config,找到httpModules和modules两个节,其中modules是针对集成管道模式的时候会用到的配置节,而httpModules则针对经典的配置所准备的。因此如果你使用的是Vista或者Server2008以下版本的操作系统,那么你肯定是要配置httpModules的,如果你使用了Vista或者Server2008及以上版本,但是没有使用集成管道模式,那你也还是要配置httpModules。

怎么配呢?这个大家可以通过模仿和实践就可以很容易学会,或者上Live搜一下就知道了。我这里仅列出本程序的配置。

<httpModules>
  <add name="ScriptModule" type="System.Web.Handlers.ScriptModu …… 
<add name="FilterModule" type="FilterModule.FilterModule" /> </httpModules>

<modules>
  <remove name="ScriptModule"/>
  <add name="ScriptModule" preCondition="managedHandler" type="S ……

<remove name="FilterModule"/>
<
add name="FilterModule" type="FilterModule.FilterModule" /> </modules>

 

截获一段HTTP流

要想得到HTTP流可不是想象的那样,你会发现通过HttpApplication无法直接得到返回客户端的HTML,你可能可以得到Headers,Post过来的数据,浏览器上的地址,查询字符串等诸多对象,但是却没有我们在控件或者页面通过HtmlTexter writer.Write出来的Html,或者你想要一堆按着层级的Control也是不行的。

还好HttpResponse.Filter属性的名字起得跟我的模块太像了,所以就把它拉出来溜溜,没想到就是它了。它的思想很简单,就是由你提供一个Stream的派生类的实例,那么每个经过管道的流就会调用相应的Write方法,而不是由你读一个属性,改完再放回去。

可是你可不是一次获得全部的文本,因为我们知道流的特性,也知道向客户端发送数据的方式,在一个最大值范围内,对应的流就会被发送到客户端,因此你得到的通常不是整个html源码,而是一段一段地读。不过不管怎么讲,总是把HTTP流截获出来了。

详见代码中:FilterStream.cs

public override void Write(byte[] buffer, int offset, int count) {
    if (HttpContext.Current.Response.ContentType == "text/html") {
        string charSet = HttpContext.Current.Response.Charset;
        System.Text.Encoding encoding = Encoding.GetEncoding(charSet);
        string currentHtml = encoding.GetString(buffer, offset, count);

        //other code
byte[] outputBuffer = encoding.GetBytes(currentHtml); _instance.Write(outputBuffer, 0, outputBuffer.Length); } else { _instance.Write(buffer, offset, count); } }

 

获取全部的页面内容

在上一部分提到了HTTP流截获的分段特性,我们要获取页面全部的内容,就应该有一个用于叠加的变量用于保存,这里我写了一个保存上下文的简单的类,提供了三组属性用于访问,一个是OriginalContent,一个是CurrentContent,另一个是OutputContent,OriginalContent在每次Write之后叠加原始文本,因此在EndRequest中获取它将得到原始的Html(全部)的内容。而OutputContent用于保存处理后的内容,而CurrentContent则是用于存储当前值,也就是上一部分提到的一段一段的内容。

在这个Filter模块中,你可以在Request中添加事件:

void context_BeginRequest(object sender, EventArgs e) {
    var app = (sender as HttpApplication);

    //other code
filterStream.Responsing += new EventHandler(filterStream_Responsing); app.Response.Filter = filterStream; } void filterStream_Responsing(object sender, EventArgs e) { IPageProcessor processor = sender as IPageProcessor; if (processor != null) { processor.Context.CurrentContent = processor.Context.CurrentContent.Replace("长得真帅", "长得真丑"); } }

增加了你的便携性,但事实上我并不推荐你这么做,但如果你自己愿意再扩展几个Filter出来的话,或许对你有帮助。但这里不可以对OutputContent进行赋值操作,因为你的赋值会无效,而且有可能让你迷惑,因此这里我对你的无心操作抛出了异常,这里唯一推荐的操作大概就是读取了。当然,最后一次你就可以通过OutputContent得到完整的代码了,但是你已经失去了对之前内容的修改权。

 

一段简陋的文本替换逻辑

不敢称什么算法,人家算法都有英文名,咱这东西太简陋,就不要跟那高科技的玩意儿沾上边,大家可以把那些高级的算法套进来用。在我的示例中,这种方式可能在你看来很低效或者没有借鉴意义,那你可以动手换成自己的。

我这里的方式比较土,我是这样想的,因为我们要替换的是html,而html就必然有标签,因此标签是不需要处理的,这是其一,因为标签是自己写的,因此不会有非文明用语,所以也可以不管。标签其实占了html的很大一部分,因此我们需要遍历整个html(我想遍历是必须的,不然怎么知道它有什么内容呢?)。这里我按字符存取,一个字符并用一个bool值标识当前所处理的文本块。(怎么都觉得像是某个简单的C语言入门题isAlpha()?)

简单地总结一下标签的形式:

<div style=”…”></div>

<input value=”…” />

其实另外一种我没有处理而且有很大必要的是<script></scirpt>以及<script language=”javascript”></script>,因为通常的网站都不会让用户自己写脚本(博客园除外),因此这里面不文明就算了……当然,现在的方式把它们也处理了,当然按照推荐,我们应该把代码放在单独的文件中,这样我这个“流氓扫描仪”就失效了。

言归正传,这里其实只需要处理<>之间的内容就可以了,因为通常我们都是用&lt;和&gt;来标识真正用语显示的尖括号,因此我们从流中读到的<>都可以被认为是标签。从<开始标记,到>开始取消标记,并为每一个字符标注它们是否在标签内。一次遍历就做完了。现在只需要遍历这个得到的集合,将不用关心标签内的值,把它们整理回string,再把非标签内的内容也整理成string,并把它们经过我们的“流氓扫描字典”的过滤(一个映射“中国”->“天朝”的字典集合),经过第一遍的扫描,剩下要replace的部分就少之又少了。至于针对字符的操作的一些处理可能您认为会损失大量的性能,但其中真正的文字拷贝却不多,因为引用拷贝几千次(html页面的字符数)事实上代价一点不大,倒是那个replace可能会消耗一些性能,但有更精简的方式吗?大部分针对这个的算法应该是针对文字辨别的,而不是针对文本替换的,比如在人眼看来“中 国”和“中国”是一样的,但是用replace就避免不了了。所以从这点讲我这里仍然不是一个算法,仅是一段逻辑。其实省去的标签部分事实上在为replace减负。

 

基于过滤器平台扩展自己的过滤器

其实你实现一个HttpResponse.Filter,就是一个过滤器了,这也是MS提供的,但是你无法再别的地方获取其中的文本,并且无法直接读取输出的文本(只能读出流),而且如果你需要处理的也只不过是塞选出来的非标签部分,那么你可能需要重新处理一遍,这不值得,因此你可以从我这里扩展,并且可以得到包括FilterContext在内的上下文支持。

public interface IPageProcessor {
    string Process(string beDisposed);
    FilterContext Context { get; set; }
}

通过实现这个接口,并将它添加进filters中(跟着ForbiddenWordFilter一样地放置就可以了),就可以得到跟它一样的扩展了。而这里你不需要处理FilterStream中Write这样的通过byte的字节数组来处流的流,同时也可以将它进行文本输出等其它操作。要实现这个类,ForbiddenWordFilter是一个很好的参考。

 

关于缓存

因为赵同学说缓存难做,事实上应该是这样讲,因为输入时候处理这个问题能够得到比较好的缓存支持,是因为这个情况的输出和平时并无二致,所以有很多的缓存机制可以用。而这里的输出时候写,不过是在输出的时候进行了另一些处理,最后我们要缓存的部分只不过延迟到了这个模块处理之后而已。如果愿意的话,稍微修改一下缓存的代码应该会好很多,或者如果是那种静态页的形式,直接在Begin的时候重定向过去也未尝不可。当然这个可能牵涉到缓存的实现方式,也许有的缓存方式这里就不能用了,这个或许就是代价吧。

 

一些修改

您如果决定使用这个模块用来应付检查,或者你的网站还没有高并发到一用这个模块就Down机,那么你可以选择该模块为你快速实现该功能。即便如此,你所下载的代码最好都经过修改后使用,比如流氓代码我是写在代码里的,这可能不能满足管理员的需要,你可以增加从文件读取流氓词语的功能,或者由用户举报词汇来过滤(也算人工智能哈,不过是手动的而已),而这些代码我就不给你加了,因为我自己不用这个东西,没有需求嘛!当然因为没有在大环境使用过,也没有经过压力测试,所以会不会Down你自己拿去试了,不好用的记得回来这里告诉我一声。

你还要改什么呢?事情永远是做不完的,现在你要实现一个deny列表或者是一个allow的列表,在BeginRequest的时候检测你所访问的页面是否在过滤范围内,如果你就是开黄色网站的主,或者您的个别页面是只适合成人的,那么你就需要对这些站点进行排除。当然了,你可以实现对目录的扫描,这样做有点像ASP.NET中对权限访问的过滤,但这也省去你的麻烦,当然如果你是一个第三方组件,并可以提供专人负责管理哪些不用过滤哪些需要过滤,或者您需要从数据库分析对该文的用户评价,并根据您指定的规则进行自动裁定,您也可以有自己的逻辑。

 

下载代码

这只是完成了大部分功能的示例,我不提供任何担保,您可以随意修改它并自用。

1、https://files.cnblogs.com/volnet/WebAppFilterForbiddenWord.zip

2、http://v-labs.googlecode.com/files/WebAppFilterForbiddenWord0.1.zip

 

阅读代码

1、http://code.google.com/p/v-labs/source/detail?r=18

posted on 2008-12-24 03:23  volnet(可以叫我大V)  阅读(4876)  评论(45编辑  收藏  举报

使用Live Messenger联系我
关闭