[Java]互联网网站的反爬虫策略浅析

mikel阅读(700)

因为搜索引擎的流行,网络爬虫已经成了很普及网络技术,除了专门做搜索的Google,Yahoo,微软,百度以外,几乎每个大型门户网站都有自己的搜索 引擎,大大小小叫得出来名字得就几十种,还有各种不知名的几千几万种,对于一个内容型驱动的网站来说,受到网络爬虫的光顾是不可避免的。
一些智能的搜索引擎爬虫的爬取频率比较合理,对网站资源消耗比较少,但是很多糟糕的网络爬虫,对网页爬取能力很差,经常并发几十上百个请求循环重复抓取,这种爬虫对中小型网站往往是毁灭性打击,特别是一些缺乏爬虫编写经验的程序员写出来的爬虫破坏力极强。曾经有一次我在JavaEye的 日志里面发现一个User-Agent是Java的爬虫一天之内爬取了将近100万次动态请求。这是一个用JDK标准类库编写的简单爬取网页程序,由于 JavaEye网站内部链接构成了回环导致程序陷入了死循环。对于JavaEye这种百万PV级别的网站来说,这种爬虫造成的访问压力会非常大,会导致网 站访问速度缓慢,甚至无法访问。
此外,相当数量的的网页爬虫目的是盗取目标网站的内容。比方说JavaEye网站就曾经被两个竞争对手网站爬取论坛帖子,然后在自己的论坛里面用机器人发帖,因此这种爬虫不仅仅影响网站访问速度,而且侵犯了网站的版权。
对于一个原创内容丰富,URL结构合理易于爬取的网站来说,简直就是各种爬虫的盘中大餐,很多网站的访问流量构成当中,爬虫带来的流量要远远超过 真实用户访问流量,甚至爬虫流量要高出真实流量一个数量级。像JavaEye网站虽然设置了相当严格的反爬虫策略,但是网站处理的动态请求数量仍然是真实 用户访问流量的2倍。可以肯定的说,当今互联网的网络流量至少有2/3的流量爬虫带来的。因此反爬虫是一个值得网站长期探索和解决的问题。
一、手工识别和拒绝爬虫的访问
有相当多的爬虫对网站会造成非常高的负载,因此识别爬虫的来源IP是很容易的事情。最简单的办法就是用netstat检查80端口的连接:

C代码
  1. netstat -nt | grep youhostip:80 | awk '{print $5}' | awk -F":" '{print $1}'| sort | uniq -c | sort -r -n   

这行shell可以按照80端口连接数量对来源IP进行排序,这样可以直观的判断出来网页爬虫。一般来说爬虫的并发连接非常高。
如果使用lighttpd做Web Server,那么就更简单了。lighttpd的mod_status提供了非常直观的并发连接的信息,包括每个连接的来源IP,访问的URL,连接状 态和连接时间等信息,只要检查那些处于handle-request状态的高并发IP就可以很快确定爬虫的来源IP了。
拒绝爬虫请求既可以通过内核防火墙来拒绝,也可以在web server拒绝,比方说用iptables拒绝:

C代码
  1. iptables -A INPUT -i eth0 -j Drop -p tcp –dport 80 -s 84.80.46.0/24    

直接封锁爬虫所在的C网段地址。这是因为一般爬虫都是运行在托管机房里面,可能在一个C段里面的多台服务器上面都有爬虫,而这个C段不可能是用户宽带上网,封锁C段可以很大程度上解决问题。
有些人提出一种脑残的观点,说我要惩罚这些爬虫。我专门在网页里面设计动态循环链接页面,让爬虫掉进陷阱,死循环爬不出来,其实根本用不着设置陷 阱,弱智爬虫对正常网页自己就爬不出来,这样做多此一举不说,而且会让真正的搜索引擎降低你的网页排名。而且运行一个爬虫根本不消耗什么机器资源,相反, 真正宝贵的是你的服务器CPU资源和服务器带宽,简单的拒绝掉爬虫的请求是反爬虫最有效的策略。
二、通过识别爬虫的User-Agent信息来拒绝爬虫
有很多爬虫并不会以很高的并发连接爬取,一般不容易暴露自己;有些爬虫的来源IP分布很广,很难简单的通过封锁IP段地址来解决问题;另外还有很 多各种各样的小爬虫,它们在尝试Google以外创新的搜索方式,每个爬虫每天爬取几万的网页,几十个爬虫加起来每天就能消耗掉上百万动态请求的资源,由 于每个小爬虫单独的爬取量都很低,所以你很难把它从每天海量的访问IP地址当中把它准确的挖出来。
这种情况下我们可以通过爬虫的User-Agent信息来识别。每个爬虫在爬取网页的时候,会声明自己的User-Agent信息,因此我们就可 以通过记录和分析User-Agent信息来挖掘和封锁爬虫。我们需要记录每个请求的User-Agent信息,对于Rails来说我们可以简单的在 app/controllers/application.rb里面添加一个全局的before_filter,来记录每个请求的User-Agent信 息:

Ruby代码
  1. logger.info "HTTP_USER_AGENT #{request.env["HTTP_USER_AGENT"]}"    

然后统计每天的production.log,抽取User-Agent信息,找出访问量最大的那些User-Agent。要注意的是我们只关注 那些爬虫的User-Agent信息,而不是真正浏览器User-Agent,所以还要排除掉浏览器User-Agent,要做到这一点仅仅需要一行 shell:

Ruby代码
  1. grep HTTP_USER_AGENT production.log | grep -v -E 'MSIE|Firefox|Chrome|Opera|Safari|Gecko' | sort | uniq -c | sort -r -n | head -n 100 > bot.log    

统计结果类似这样:

C代码
  1. 57335 HTTP_USER_AGENT Baiduspider+(+http://www.baidu.com/search/spider.htm)  
  2. 56639 HTTP_USER_AGENT Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)  
  3. 42610 HTTP_USER_AGENT Mediapartners-Google  
  4. 19131 HTTP_USER_AGENT msnbot/2.0b (+http://search.msn.com/msnbot.htm)  

从日志就可以直观的看出每个爬虫的请求次数。要根据User-Agent信息来封锁爬虫是件很容易的事情,lighttpd配置如下:

C代码
  1. $HTTP["useragent"] =~ "qihoobot|^Java|Commons-HttpClient|Wget|^PHP|Ruby|Python" {  
  2.   url.rewrite = ( "^/(.*)" => "/crawler.html" )  
  3. }  

使用这种方式来封锁爬虫虽然简单但是非常有效,除了封锁特定的爬虫,还可以封锁常用的编程语言和HTTP类库的User-Agent信息,这样就可以避免很多无谓的程序员用来练手的爬虫程序对网站的骚扰。
还有一种比较常见的情况,就是某个搜索引擎的爬虫对网站爬取频率过高,但是搜索引擎给网站带来了很多流量,我们并不希望简单的封锁爬虫,仅仅是希望降低爬虫的请求频率,减轻爬虫对网站造成的负载,那么我们可以这样做:

C代码
  1. $HTTP["user-agent"] =~ "Baiduspider+" {  
  2.     connection.delay-seconds = 10  
  3. }  

对百度的爬虫请求延迟10秒钟再进行处理,这样就可以有效降低爬虫对网站的负载了。
三、通过网站流量统计系统和日志分析来识别爬虫
有些爬虫喜欢修改User-Agent信息来伪装自己,把自己伪装成一个真实浏览器的User-Agent信息,让你无法有效的识别。这种情况下我们可以通过网站流量系统记录的真实用户访问IP来进行识别。
主流的网站流量统计系统不外乎两种实现策略:一种策略是在网页里面嵌入一段js,这段js会向特定的统计服务器发送请求的方式记录访问量;另一种 策略是直接分析服务器日志,来统计网站访问量。在理想的情况下,嵌入js的方式统计的网站流量应该高于分析服务器日志,这是因为用户浏览器会有缓存,不一 定每次真实用户访问都会触发服务器的处理。但实际情况是,分析服务器日志得到的网站访问量远远高于嵌入js方式,极端情况下,甚至要高出10倍以上。
现在很多网站喜欢采用awstats来分析服务器日志,来计算网站的访问量,但是当他们一旦采用Google Analytics来统计网站流量的时候,却发现GA统计的流量远远低于awstats,为什么GA和awstats统计会有这么大差异呢?罪魁祸首就是 把自己伪装成浏览器的网络爬虫。这种情况下awstats无法有效的识别了,所以awstats的统计数据会虚高。
其实作为一个网站来说,如果希望了解自己的网站真实访问量,希望精确了解网站每个频道的访问量和访问用户,应该用页面里面嵌入js的方式来开发自 己的网站流量统计系统。自己做一个网站流量统计系统是件很简单的事情,写段服务器程序响应客户段js的请求,分析和识别请求然后写日志的同时做后台的异步 统计就搞定了。
通过流量统计系统得到的用户IP基本是真实的用户访问,因为一般情况下爬虫是无法执行网页里面的js代码片段的。所以我们可以拿流量统计系统记录 的IP和服务器程序日志记录的IP地址进行比较,如果服务器日志里面某个IP发起了大量的请求,在流量统计系统里面却根本找不到,或者即使找得到,可访问 量却只有寥寥几个,那么无疑就是一个网络爬虫。
分析服务器日志统计访问最多的IP地址段一行shell就可以了:

C代码
  1. grep Processing production.log | awk '{print $4}' | awk -F'.' '{print $1"."$2"."$3".0"}' | sort | uniq -c | sort -r -n | head -n 200 > stat_ip.log    

然后把统计结果和流量统计系统记录的IP地址进行对比,排除真实用户访问IP,再排除我们希望放行的网页爬虫,比方Google,百度,微软msn爬虫等等。最后的分析结果就就得到了爬虫的IP地址了。以下代码段是个简单的实现示意:

Ruby代码
  1. whitelist = []  
  2. IO.foreach("#{RAILS_ROOT}/lib/whitelist.txt") { |line| whitelist << line.split[0].strip if line }  
  3.   
  4. realiplist = []  
  5. IO.foreach("#{RAILS_ROOT}/log/visit_ip.log") { |line|  realiplist << line.strip if line }  
  6.   
  7. iplist = []  
  8. IO.foreach("#{RAILS_ROOT}/log/stat_ip.log"do |line|  
  9.   ip = line.split[1].strip  
  10.   iplist << ip if line.split[0].to_i > 3000 && !whitelist.include?(ip) && !realiplist.include?(ip)  
  11. end   
  12.   
  13. Report.deliver_crawler(iplist)  

分析服务器日志里面请求次数超过3000次的IP地址段,排除白名单地址和真实访问IP地址,最后得到的就是爬虫IP了,然后可以发送邮件通知管理员进行相应的处理。
四、网站的实时反爬虫防火墙实现策略
通过分析日志的方式来识别网页爬虫不是一个实时的反爬虫策略。如果一个爬虫非要针对你的网站进行处心积虑的爬取,那么他可能会采用分布式爬取策 略,比方说寻找几百上千个国外的代理服务器疯狂的爬取你的网站,从而导致网站无法访问,那么你再分析日志是不可能及时解决问题的。所以必须采取实时反爬虫 策略,要能够动态的实时识别和封锁爬虫的访问。
要自己编写一个这样的实时反爬虫系统其实也很简单。比方说我们可以用memcached来做访问计数器,记录每个IP的访问频度,在单位时间之 内,如果访问频率超过一个阀值,我们就认为这个IP很可能有问题,那么我们就可以返回一个验证码页面,要求用户填写验证码。如果是爬虫的话,当然不可能填 写验证码,所以就被拒掉了,这样很简单就解决了爬虫问题。
用memcache记录每个IP访问计数,单位时间内超过阀值就让用户填写验证码,用Rails编写的示例代码如下:

Ruby代码
  1. ip_counter = Rails.cache.increment(request.remote_ip)  
  2. if !ip_counter  
  3.   Rails.cache.write(request.remote_ip, 1, :expires_in => 30.minutes)  
  4. elsif ip_counter > 2000  
  5.   render :template => 'test':status => 401 and return false  
  6. end  

这段程序只是最简单的示例,实际的代码实现我们还会添加很多判断,比方说我们可能要排除白名单IP地址段,要允许特定的User-Agent通过,要针对登录用户和非登录用户,针对有无referer地址采取不同的阀值和计数加速器等等。
此外如果分布式爬虫爬取频率过高的话,过期就允许爬虫再次访问还是会对服务器造成很大的压力,因此我们可以添加一条策略:针对要求用户填写验证码 的IP地址,如果该IP地址短时间内继续不停的请求,则判断为爬虫,加入黑名单,后续请求全部拒绝掉。为此,示例代码可以改进一下:

Ruby代码
  1. before_filter :ip_firewall:except => :test  
  2. def ip_firewall  
  3.   render :file => "#{RAILS_ROOT}/public/403.html":status => 403 if BlackList.include?(ip_sec)  
  4. end  

我们可以定义一个全局的过滤器,对所有请求进行过滤,出现在黑名单的IP地址一律拒绝。对非黑名单的IP地址再进行计数和统计:

Ruby代码
  1. ip_counter = Rails.cache.increment(request.remote_ip)  
  2. if !ip_counter  
  3.   Rails.cache.write(request.remote_ip, 1, :expires_in => 30.minutes)  
  4. elsif ip_counter > 2000  
  5.   crawler_counter = Rails.cache.increment("crawler/#{request.remote_ip}")  
  6.   if !crawler_counter  
  7.     Rails.cache.write("crawler/#{request.remote_ip}", 1, :expires_in => 10.minutes)  
  8.   elsif crawler_counter > 50  
  9.     BlackList.add(ip_sec)  
  10.     render :file => "#{RAILS_ROOT}/public/403.html":status => 403 and return false  
  11.   end  
  12.   render :template => 'test':status => 401 and return false  
  13. end  

如果某个IP地址单位时间内访问频率超过阀值,再增加一个计数器,跟踪他会不会立刻填写验证码,如果他不填写验证码,在短时间内还是高频率访问, 就把这个IP地址段加入黑名单,除非用户填写验证码激活,否则所有请求全部拒绝。这样我们就可以通过在程序里面维护黑名单的方式来动态的跟踪爬虫的情况, 甚至我们可以自己写个后台来手工管理黑名单列表,了解网站爬虫的情况。
这个策略已经比较智能了,但是还不够好!我们还可以继续改进:
1、用网站流量统计系统来改进实时反爬虫系统
还记得吗?网站流量统计系统记录的IP地址是真实用户访问IP,所以我们在网站流量统计系统里面也去操作memcached,但是这次不是增加计 数值,而是减少计数值。在网站流量统计系统里面每接收到一个IP请求,就相应的cache.decrement(key)。所以对于真实用户的IP来说, 它的计数值总是加1然后就减1,不可能很高。这样我们就可以大大降低判断爬虫的阀值,可以更加快速准确的识别和拒绝掉爬虫。
2、用时间窗口来改进实时反爬虫系统
爬虫爬取网页的频率都是比较固定的,不像人去访问网页,中间的间隔时间比较无规则,所以我们可以给每个IP地址建立一个时间窗口,记录IP地址最 近12次访问时间,每记录一次就滑动一次窗口,比较最近访问时间和当前时间,如果间隔时间很长判断不是爬虫,清除时间窗口,如果间隔不长,就回溯计算指定 时间段的访问频率,如果访问频率超过阀值,就转向验证码页面让用户填写验证码。
最终这个实时反爬虫系统就相当完善了,它可以很快的识别并且自动封锁爬虫的访问,保护网站的正常访问。不过有些爬虫可能相当狡猾,它也许会通过大 量的爬虫测试来试探出来你的访问阀值,以低于阀值的爬取速度抓取你的网页,因此我们还需要辅助第3种办法,用日志来做后期的分析和识别,就算爬虫爬的再 慢,它累计一天的爬取量也会超过你的阀值被你日志分析程序识别出来。
总之我们综合运用上面的四种反爬虫策略,可以很大程度上缓解爬虫对网站造成的负面影响,保证网站的正常访问。

[C#]使用接口实现附带插件功能的程序

mikel阅读(679)

插件功能给软件的使用者可以扩充软件功能的机会。我们不可能让软件适用于所有人,也不是所有的人都会出资帮助你实现他们的需求。插件功能提供了一个软件的高度可扩充性,允许用户作为软件的二次开发者,继续完善软件的功能。

为了在软件中加入插件功能,我们需要下面几个特别的条件:

(1)      本软件(此后我们称之为‘宿主程序’)需要开放自己的成员,包括属性、方法、事件为插件程序提供服务。

(2)      宿主程序要很好的隐藏一些信息,阻止插件程序有意或无意的破坏本身的功能。

(3)      宿主程序提供插件服务,方便插件功能的升级。

(4)      定义一个统一的标志信息,保证宿主程序可以正常的识别并运行插件程序而不会出现类型安全问题。

为此我们模仿Visual Studio .Net本身提供的Addin的实现机制来实现我们的插件程序。

attachments/month_200701/8894356039.jpg

第一步,制作接口。它是建立在宿主程序和插件之间的桥梁。

首先我们建立一个“插件标识”接口,这个接口用来标识我们的插件类的特性。

Public Interface IPlugins

    Sub Connect(ByVal PluginsApp As IPluginsApplication)

End Interface

这个接口里面我们定义了一个方法Connect,用来启动我们的插件程序。也就是说,我们的宿主程序将会统一使用Connect方法启动插件程序。而对于插件程序,入口地址将是Connect方法。这个有点类似于普通应用程序的Main函数。Connect函数的参数IPluginsApplication表示我们宿主程序的实例,稍后将会进一步解释。

其次,我们建立一个“插件服务”接口,这个接口将宿主程序需要开放的属性、方法、实现定义出来,通过接口的方式提供给插件程序。

Public Interface IPluginsApplication

    Event Display(ByVal sender As Object, ByVal e As EventArgs)

    Property Caption() As String

    Sub DisplayInput(ByVal TextAs String)

End Interface

在例子中我们简单定义了一个事件、一个属性和一个方法。插件程序在开发的时候只要引用了我们的“插件服务”接口就可以调用里面定义的内容了。

 

第二步,建立宿主程序。

但是光有接口是不能执行里面的内容的,必须要有一个实现了这个接口的实例才可以。这里我们让宿主程序实现这个接口,并且实现这些接口里面的内容,让插件程序可以进行操作。

Public Class Form1

    Implements PluginsInterface.IPluginsApplication

    Public Event Display(ByVal sender As Object, ByVal e As System.EventArgs) Implements PluginsInterface.IPluginsApplication.Display

    Public Property Caption() As String Implements PluginsInterface.IPluginsApplication.Caption

        Get

            Return Me.Text

        End Get

        Set(ByVal value As String)

            Me.Text = value

        End Set

    End Property

    Public Sub DisplayInput(ByVal Text As String) Implements PluginsInterface.IPluginsApplication.DisplayInput

        MsgBox("输入内容:" & Text)

    End Sub

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

        RaiseEvent Display(Me, New EventArgs)

        Me.DisplayInput(Me.TextBox1.Text)

    End Sub

End Class

我们用一个标准的Windows窗体来实现“插件服务”接口的内容。通过Caption属性可以更改窗体的标题,通过DisplayInput方法可以显示字符串。

这样我们的宿主程序由于实现了“插件服务”接口,就可以通过“插件服务”接口来将实例传递进插件程序。我们回到“插件标识”接口的Connect方法,插件的入口函数参数将宿主程序通过“插件服务”接口的实例传递到插件程序内,使得插件程序可以调用宿主程序开放的内容。而且由于通过接口传递,这些操作都是类型安全的。

 

第三步,寻找并启动插件。

插件程序将会以动态链接库的形式提供,这就需要我们的宿主程序找到插件程序的文件,判断是不是合法的插件,实例化并且启动。

首先我们必须定义一个插件存放的路径,比如运行目录下面的Plugins目录。然后寻找这一目录下面所有的dll文件进行判断。这里我们固定好路径和文件名。

对于找到的文件,通过反射我们就可以得到定义于这个dll文件中的所有类定义信息。通过刚才我们说的“插件标识”接口逐个判断,将实现了“插件标识”接口的类作为我们判断合法的插件类。然后使用实例化方法进行实例化。(注意,我们的插件程序默认一个无参数的实例化方法。)通过强制类型转换,将这个Object的实例转化为我们的“插件标识”接口实例,也就是IPlugins。由于此前我们已经判断过了,这个类实现了“插件标识”接口(也就是IPlugins接口),所以这个转换是安全的。最后通过IPluginsConnect方法启动接口程序,将宿主程序,也就是我们的窗体实例通过参数传递。(由于我们的窗体已经实现了接口IPluginsApplication,所以这步操作也是安全的。)此后,程序将由插件接管,对于宿主程序,插件和宿主自己同时进行操作。

    Dim pobj As PluginsInterface.IPlugins

    Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click

        Dim ass As Reflection.Assembly = Reflection.Assembly.LoadFile("E:\Visual Sutdio Project 2005\PluginsApplication\Plugins\bin\Debug\Plugins.dll")

        Dim t As Type = Nothing

        For Each t In ass.GetTypes

            If t.IsClass AndAlso t.GetInterface(GetType(PluginsInterface.IPlugins).FullName, True) IsNot Nothing Then

                Exit For

            End If

        Next

        If t IsNot Nothing Then

            pobj = ass.CreateInstance(t.FullName, True)

            pobj.Connect(Me)

        End If

    End Sub

 

第四步,制作插件。

将一个类实现“插件标识”接口,用来表示这个类是宿主程序可识别的插件。同时必须实现Connect方法。通过Connect的参数,插件程序可以使用宿主程序通过“插件服务”接口提供的功能。

Public Class Plugins1

    Implements PluginsInterface.IPlugins

    Private WithEvents m_papp As PluginsInterface.IPluginsApplication

    Public Sub Connect(ByVal PluginsApp As PluginsInterface.IPluginsApplication) Implements PluginsInterface.IPlugins.Connect

        MsgBox("插件启动成功。")

        Me.m_papp = PluginsApp

        Me.m_papp.Caption = InputBox("请输入宿主程序的窗体标题")

        Me.m_papp.DisplayInput(InputBox("请输入字符串"))

    End Sub

    Private Sub m_papp_Display(ByVal sender As Object, ByVal e As System.EventArgs) Handles m_papp.Display

        Dim f As New Form1

        f.ShowDialog()

    End Sub

End Class

 

通过上述方法,我们就制作完成了一个简单的插件。

总结一下:

(1)      通过接口定义插件的标识,进行类型验证并启动插件程序。这样做的好处是统一了插件的类型并且可以安全的进行启动。

(2)      通过接口定义宿主程序希望公开的功能。这样做一方面保证了宿主程序不会被插件程序完全的控制,另一方面让插件程序可以安全的运行宿主提供的方法。缺点是宿主程序如果有多层嵌套的类关系需要开放的话,需要将所有的类都重新通过接口进行封装。

(3)      宿主程序、插件程序引用统一的接口程序,将插件的开发了宿主程序本身脱离,提高宿主的安全性,并且防止了循环引用的发生。

PS:本文所用到的程序代码下载文件pluginsapplication.zip

[Flash]flash cs3生成网页的flash插入方法研究

mikel阅读(674)

先来看看flash自动生成的网页是如何插入flash文件的:

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh_cn" lang="zh_cn">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
<title>test</title>
<scrīpt language="javascrīpt">AC_FL_RunContent = 0;</scrīpt>
<scrīpt src="AC_RunActiveContent.js" language="javascrīpt"></scrīpt>

<style type="text/css">
<!–
body {
 background-color: #999900;
}
–>
</style></head>
<body>
<!–影片中使用的 URL–>
<!–影片中使用的文本–>
<!–
eee
–>
<!– saved from url=(0013)about:internet –>
<scrīpt language="javascrīpt">
 if (AC_FL_RunContent == 0) {
  alert("此页需要 AC_RunActiveContent.js");
 } else {
  AC_FL_RunContent(
   'codebase', 'http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0',
   'width', '550',
   'height', '400',
   'src', 'test',
   'quality', 'high',
   'pluginspage', 'http://www.macromedia.com/go/getflashplayer',
   'align', 'middle',
   'play', 'true',
   'loop', 'true',
   'scale', 'showall',
   'wmode', 'transparent',
   'devicefont', 'false',
   'id', 'test',
   'bgcolor', '#666666',
   'name', 'test',
   'menu', 'true',
   'allowFullScreen', 'false',
   'allowscrīptAccess','sameDomain',
   'flashvars','txt=wwwww',
   'movie', 'test',
   'salign', ''
   ); //end AC code
 }
 function sendvar(){
  test.style.height=500;
  test.SetVariable("mv","kkkkkk")
  }
</scrīpt>
<noscrīpt>
 <object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0" width="550" height="400" id="test" align="middle">
 <param name="allowscrīptAccess" value="sameDomain" />
 <param name="allowFullScreen" value="false" />
 <param name="movie" value="test.swf" /><param name="quality" value="high" /><param name="bgcolor" value="#666666" /
><embed src="test.swf" quality="high" bgcolor="#666666" width="550" height="400" name="test" align="middle" allowscrīptAccess="sameDomain" allowFullScreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" />
 </object>
</noscrīpt>
 <br>
 <label>xxx
 <input type="submit" name="Submit" value="提交" ōnClick="sendvar()">
 </label>

</body>
</html>
这个网页插入flash共使用了3种方式,应对各种情况,尽可能使swf文件在各种情况、各种浏览器中都能够正常显示运行。

先来看看第一种情况:
最开始使用javascrīpt插入swf文件,这种方式兼容性最好,可以同时兼容IE内核的浏览器及FireFox 浏览器,而且这种插入方式可以避免IE中控件激活框的出现,非常实用。这段自动生成的代码包含的内容很丰富,你可以在其中任意添加IE或者其他浏览器使用 的参数,例如:
'name', 'test',
'id', 'test',
这个是javascrīpt引用swf文件的变量名,使javascrit可以直接对该swf文件进行操作,其中IE只使用id变量就可以了,name变量是针对embed插入方式FireFox使用的。

虽然javascrīpt的插入方式优点多多,但是一旦用户禁用了javascrīpt,就不行了。下面说说第二种方式:
删除所有的javascrīpt代码(同时删除<noscrīpt>和</noscrīpt>)。
<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0
" width="550" height="400" id="test" align="middle">
 <param name="allowscrīptAccess" value="sameDomain" />
 <param name="allowFullScreen" value="false" />
 <param name="movie" value="test.swf" /><param name="quality" value="high" /><param name="bgcolor" value="#666666" />
这是IE使用的flash文件插入方式,如果只使用了这段代码,IE可以正常显示,但是FireFox就不能显示了。

第三种,embed插入方式
<embed src="test.swf" quality="high" bgcolor="#666666" width="550" height="400" name="test" align="middle" allowscrīptAccess="sameDomain" allowFullScreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer
" />
这种方式IE和FireFox都可以正常显示

第二种和第三种的参数解释可以参考下面的文章
http://space.flash8.net/space/?246908/action_viewspace_itemid_408019.html

就算不使用javascrīpt,后面两种flash插入方式也可以通过htm页面向flash传递变量:

1、object插入方式:
增加参数:<param name="flashvars" value="mv=hello!"> 

2、embed插入方式:
在后面加入: flashvars="mv=hello!"

通过以上两种方式,flash都可以收到一个变量名为“mv”的变量,内容为“hello!" 。

[Flex]使用Flash Builder 4 beta进行以数据为中心的开发

mikel阅读(793)

作者 Tim Buntel 译者 曹如进 

Adobe Flash Builder 4 beta为Flex开发者们,不管是新手还是老手,提供了更多的机会来创建以数据为中心的富互联网应用。这样一个由专业工具,一个开源框架以及无处不在的 客户端所组成的Flash平台,让你能够发布出令人瞠目的表现内容和应用程序。

尽管如此,大多数的应用还依赖于平台之外的服务。也许你的应用程序为企业数据库中存储的信息提供了报表以及数据可视化的功能,抑或你的富电子商务应 用程序需要与现有的订单管理系统,或者第三方的支付服务进行集成。那么你只需要连接到相应的服务器或者服务,这种应用特性就可以使得上述一切像发送电子邮 件般的简单,例如通过使用云托管服务和第三方的API查询数据库等等。

在以往的版本中,开发者必须学会各种技巧手工编写连接服务器和服务的代码。例如,需要知道连接一个SOAP服务使用的MXML标签不同于连接 ColdFusion组件或者PHP类。此外,你通常还得编写一些对于web开发者来说很少见,很困惑的代码,如事件监听和故障处理。

在Flash Builder 4 beta中,Adobe改变了这一切,它采用了一种全新的方式创建以数据为中心的应用程序。新环境下的Flex开发者可以快速的连接到数据和服务,并将它 们绑定到富UI控件上。这些创建面向数据的高级应用的新方法使得经验丰富的开发者们受益匪浅。

使用Flash Builder 4 beta进行以数据为中心的开发主要包括三个步骤:

  • 定义一个数据/服务模型
  • 将服务操作绑定到Flex组件上
  • 实现高级的数据处理,例如分页和数据管理

在这篇文章中,你将会经历创建一个简单数据管理应用的主要步骤。在这个场景中,你有一张Oracle数据库中的表,并想要创建一个Flex应用来允许用户查看它的数据以及新增,更新和删除记录。

要求

为了更好的理解这篇文章,你需要使用下面的软件和文件:

Flash Builder 4 beta

基础知识

之前有过Flex Builder的使用经验会很有帮助,但这不是必要的。你需要熟悉一种服务端技术例如ColdFusion,Java或者PHP。

第一步:创建一个服务

 由于在Adobe Flash Player中运行的应用程序不能直接与Oracle数据库交互,因此你需要利用一个服务来完成该任务:它可以接受来自Flex发来的请求并传递到数据库 中;还可以将数据库中的数据用一种可理解的格式发回给Flex。这样的远程服务有着相当多的实现方式,在Flash Builder 4 beta中已经内嵌支持了使用ColdFusion,PHP和Java创建服务,而其他类型的服务可以像SOAP web服务或者HTTP服务一样使用。使用ColdFusion是个理想的选择,因为它可以和任何后端数据库交互,且它语法的简单性使得你仅仅需要少量的 标签即可完成创建一个数据接入服务。加之ColdFusion支持一个高性能的名为AMF的协议与Flex应用程序进行数据交换。另外PHP和Java也 同样支持AMF,因此你大可以随心所欲的使用自己最熟悉和认为最高效的服务端技术。

使用ColdFusion,你需要为Flex应用程序执行的每一个数据操作创建一个ColdFusion组件(CFC)函数,如:获取一张表的 所有记录,向表中增加一条新的记录,以及删除一条记录等等。CFC中的函数可以返回弱类型和强类型的数据(例如,如果你正在采用一种更加面向对象的方式进 行开发的话,可以使用getAllRecords函数返回一个ColdFusion的查询对象或者一个对象数组);Flex两种类型的数据都能处理。最后一定要确保cffunction标签的access属性标记为remote后再测试组件。好了,到此为止你已经可以在Flex应用程序中使用这个服务了。

第二步:在Flash Builder中创建模型

在Flash Builder 4 beta中,新的数据/服务面板位于中心位置,主要是用来管理和交互你的应用程序中使用到的所有服务器和服务。它采用一个树状视图来表示所有服务中可用的 数据和操作。视图中呈现的数据和服务可以来源于不同的地方。例如,其中一个可能是ColdFusion组件或者PHP类,另外一个可能是云托管的第三方 RESTful服务。尽管如此,你不必担心它们在服务端如何实现,因为现在将结果绑定到UI组件、编写代码来调用操作都能统一到一个方法中。

为了让服务得以使用,Flash Builder 4 beta会自动检查内部服务并创建树状视图。在Flash Builder 4 beta中选择数据->连接到ColdFusion(或 者你的服务使用到的技术)。对ColdFusion而言,你只需要简单地提供一个想使用的服务名称(例如,EmployeeSvc),并将它定向到文件系 统中的CFC即可。这一步骤会依服务使用技术的不同而略有变化(例如,你也许会为web服务指定WSDL),但是结果一定是一样的:Flash Builder 4 beta通过在内部检查服务来发现返回的操作和数据类型,继而在数据/服务面板上创建服务的树状视图。

如果有必要的话,你还可以继续向服务树状视图中加入其它服务,或者也可以马上就在应用程序中使用已有的服务。如果服务是弱数据类型,那么需要一个额外的步骤。因为一个弱类型的服务仅返回数据,而没有关于数据所代表含义的信息。

比方说,你的CFC函数返回了一个ColdFusion查询对象,而Flash Builder 4 beta看到的只是一堆记录,它并不知道这些记录代表的是产品集合还是员工集合或是销售订单的集合;这仅仅是一堆数据而已。为了关联操作结果的数据类 型,Flash Builder 4 beta允许你手工配置操作返回的数据类型。当然,如果你在服务端使用强类型的数据类型,这步是可以略去的。

想要设置弱类型服务的返回类型,你可以右键点击数据/服务面板(例如,getAllItems操作),然后选择配置返回类型。向导会帮助你建立服务端弱类型数据与Flex应用程序中的强类型的映射关系。它通过给出一个真实的操作样例数据来让你决定选择什么样的类型。过程中你需要为操作返回的自定义类型指定一个名称,例如可以把返回的每一条记录称为Employee或者SalesOrder,还可以指定数据类型中的字段和格式——如将name的类型设置为string,员工的id设置为数字(见图1)。

图1. 配置操作返回类型

第三步:将服务连接到UI控件

既然你已经定义好了服务中所有的操作以及返回的数据类型,那么现在需要做的就是在应用程序中 的某个地方显示那些操作的结果。Flex框架中包含了大量的控件用以数据绑定,包括数据网格(data grids),列表控件(list boxes),表单域(form fields)等等。这些组件可以显示数据并允许用户与你的服务进行交互。

一开始就在设计视图中对UI进行布局,以及绑定操作到组件上会很简单。只要切换编辑器从源代码视图到设计视图,你就可以从组件面板中拖动组件到应用程序的画布(canvas)上并进行精确定位。

选择DataGrid组件(在组件面板里数据控件组的下面),将它拖放到页面中。你会发现它没有绑定到任何数据;如果运行程序,会发现它仅仅是 一个三列的空网格。为了能够让网格显示从你的服务操作中获取的数据,你只需要简单地将数据/服务面板中的操作拖拽到网格上即可。结束之后你会发现,网格将 会显示从操作返回的列。这时,保存项目,运行,就得到了一个正在使用你的ColdFusion服务填充网格的应用程序。这一切都无需编写任何代码,无需事 先任何事件监听器,无需知道服务端是ColdFusion还是Java或者SOAP。你还可以用很多其他方法来快速创建基于数据类型和服务的应用程序 UI。如可以从一个数据类型生成表单并且创建主从表,可以将一个服务拖拽到按钮组件中,然后每当用户点击这个按钮,就会触发操作的执行(例如,调用保存操 作),还可以将操作拖拽到图表控件上等等。

数据和服务特性并不是仅仅在设计视图中有用。通过使用服务模型生成的子类,你可以获得关于所有操作和数据类型,甚至值对象的自定义行为的代码提示。

高级数据特性

Flash Builder 4 beta以数据为中心的新特色功能,可以极大地提高你在创建以数据为中心的应用程序时的生产力。虽然在Flex Builder 3中也可以创建同样的应用,但是要花费更多的精力。这种新的高级数据特性,已经超越了生产力;它们能够让你实现在以前看来极度困难或是不可能的功能。比 如,客户端数据管理特性可以让你将客户端的常见数据服务操作(选择,创建,更新和删除记录)与服务端相应的数据操作进行映射。这将使得你能够批量处理操 作,而撤销功能可以使用户重做一些改变等等。另外一个强大的特性是支持自动分页。如果你要显示大量的记录,那么在应用程序一次性读取和加载它们的时候,会 有性能问题。而分页会自动地每次按需取出一小部分的记录;你需要做的只是提供一个能够接受某行开始以及所需读取的记录数为参数的服务,而Flash Builder 4 beta负责实现客户端的所有逻辑。

下一步怎么做

不管你是一名经验丰富的Flex开发者还是刚刚接触这个技术的新手,Flash Builder 4 beta都能够让你充分利用已有的服务端数据和服务逻辑知识,轻松的创建富应用开发体验。下载好软件后,今天就可以开始让你的用户看到数据新的呈现方式。 同时也别忘了看看Adobe实验室的视频和教程哦.

关于作者

Tim Buntel是Flash Builder(以前叫做Flex Builder)的高级产品经理。在2007年加入Flex小组之前,他曾担任多年的Adobe ColdFusion高级产品经理。

阅读原文Data-centric development with Flash Builder 4 beta

[Flex]第三方强力工具TurboDieselSportInjection和Reducer

mikel阅读(768)

在描述这2个强力工具之前,先说一下这两个应用背后的框架,apparat,是一个基于Java构造的开源框架,用于优化SWC和SWF,我们可以从GoogleCode中发现这个不起眼的开源项目(主要是GoogleCode中,项目太多了的缘故):
http://code.google.com/p/apparat/
而基于此框架诞生的2个强力应用工具,第一是TurboDieselSportInjection(名字太长了,连原作者也说他并不善于起名字,简称TDSI)。
它是从整个apparat框架中派生出来的一个针对性工具,允许你链接封装的__bytecode(供AVM解释的机器码)并使用全新内存API的工具。这个工具内置对机器码和Alchemy操作的支持!
第二个工具也很惊奇,叫做Reducer。 这个工具也是从apparat框架中派生出来的,用于优化SWF/SWC文件,主要是让文件尺寸变的更小,但是不影响到组件功能,作者描述到,如果你在 SWF或者SWC中使用PNG图形,实际上对于图形元素,SWF/SWC并不会进行压缩处理。Reducer这个工具就是为了安全的让开发者优化 [Embed]标签并且以后也能正常使用压缩后的元件,它将压缩有所无损的高质量图片以降低文件尺寸。

[C#]基于.net技术的 Rss 订阅开发

mikel阅读(618)

        RSS(Really Simple Syndication,真正简单的连锁)是一种 Web 内容连锁格式。RSS 成为通过 Web 连锁新闻内容的标准格式。刚好我现在vs的环境也是.net,因为在.NET3.5下,MS集成了RSS对象。这样一改变,就很大的方便了创建和读取 RSS了。
       首先搞了个Rss.aspx页面,在Page_Load方法里面显示让它以标准的xml格式输出
      Response.Cache.SetNoStore();
      Response.ContentType = "application/xml";
 
      然后根据需要订阅的页面传过来的参数进行一番判断。把所有符合条件的资源都放在DataTable里面。
 
     接着用MemoryStream对象对xml进行操作,就不多说了,看了代码就会明白,同时也给自己做个备忘。如下:
    

        MemoryStream ms = new MemoryStream();
        XmlTextWriter xmlTW = new XmlTextWriter(ms, Encoding.UTF8);
        xmlTW.Formatting = Formatting.Indented;
        xmlTW.WriteStartDocument();
        xmlTW.WriteStartElement("rss");
        xmlTW.WriteAttributeString("version", "2.0");
        xmlTW.WriteStartElement("channel");
        if (WebID == 0)
        {
        }
        else
        {
            xmlTW.WriteElementString("title", "欢迎订阅"+WebDs.Tables[0].Rows[0] ["Web_Name"].ToString()+">>"+ColumnDs.Tables[0].Rows[0]["ColumnName"].ToString());
            xmlTW.WriteElementString("link", ColumnDs.Tables[0].Rows[0]["CoulumnUrl"].ToString());
            xmlTW.WriteElementString("description", "");
         
        }
     
        DataTable dt = ds.Tables[0];

        foreach (DataRow dr in dt.Rows)
        {
            xmlTW.WriteStartElement("item");
          
            xmlTW.WriteElementString("title", dr["Article_Title"].ToString());
            xmlTW.WriteElementString("link", GetNewsLink(dr));
            xmlTW.WriteElementString("pubDate",string.Format("{0:R}",dr["CreateTime"]));
            xmlTW.WriteElementString("author", dr["UserLogin_FullName"].ToString());
            xmlTW.WriteElementString("description", Pub_Config.nohtml(Pub_Config.Substrin(dr["Article_Body"], 400)));
          
           
            xmlTW.WriteEndElement();

        }

        xmlTW.WriteEndElement();
        xmlTW.WriteEndElement();
        xmlTW.WriteEndDocument();
        xmlTW.Flush();
        byte[] buffer = ms.ToArray();
        Response.Write(Encoding.UTF8.GetString(buffer));
        Response.End();
        xmlTW.Close();
        ms.Close();
        ms.Dispose();
  要注意的是:
   1.XML格式是大小写敏感的,这就意味着,XML元素的起始和终止标签必须匹配,拼写和大小写都必须一致。
  2.RSS2.0的根元素是< rss>元素,这个元素可以有一个版本号的属性,例如:

< rssversion="2.0">

< /rss>

< rss>元素只有一个子元素< channel>,用来描述聚合的内容。在< channel>元素里面有三个必需的子元素,用来描述Web站点的信息。这三个元素是:

title—定义聚合文件的名称,一般来说,还会包括Web站点的名称;
link—Web站点的URL;
description—Web站点的一段简短的描述。
除此之外,还有一些可选元素来描述站点信息。这些元素的更多信息请参见RSS2.0规范。

每一个新闻项目放在一个单独的< item>元素中。< channel>元素可以有任意数量的< item>元素。每个< item>元素可以有多种的子元素,唯一的要求是最少必须包含< title>元素和< description>元素其中一个作为子元素。以下列出了一些相关的< item>子元素:

 

title—新闻项目的标题;

link—新闻项目的URL;

description—新闻项目的大纲;

author—新闻项目的作者;

pubDate—新闻项目的发布日期

3.< item>子元素尤其要注意的是pubDate的格式,RSS要求日期必须按照RFC822日期和时间规范进行格式化,此格式要求:开头是一个可选的3字母星期缩写加一个逗号,

 

.

 

接着必须是日加上3字母缩写的月份和年份,最后是一个带时区名的时间。

我们可以用Stirng.foemat()来转化如期格式,就如我上面那个例子。
最终结果:
 

[SQL]sql server和oracle行转列的一种典型方法

mikel阅读(1049)

前言:网上有不少文章是讲行转列的,但是大部分都是直接贴代码,忽视了中间过程,本人自己思考了下为什么要这样实现,并且做了如下的笔记,对有些懂的人来说可能没有价值,希望对还不懂的人有一点借鉴意义。

对于有些业务来说,数据在表中的存储和其最终的Grid表现恰好相当于把源表倒转,那么这个时候我们就碰到了如何把行转化为列的问题,为了简化问题,我们且看如下查询出来的数据,您不必关心表的设计以及SQL语句:

image

假设用到的SQL语句为:

Select [姓名],[时代],[金钱]
  
FROM [test].[dbo].[people]  

 

这个表存储了两个人在不同时代(时代是固定的三个:年轻、中年和老年)拥有的金币,其中:

张三在年轻、中年和老年时期分别拥有1000、5000、800个金币;

李四在年轻、中年和老年时期分别拥有1200、6000、500个金币。

现在我们想把两人在不同阶段拥有的金币用类似如下的表格来展现:

姓名 年轻 中年 老年
张三 1000 5000 800
李四 1200 6000 500

 

我们现在考虑用最简单和直接的办法来实现,其实关键是如何创建那些需要增加的列,且如何设定其值,现在我们来创建“年轻”列,关键的问题是,这一列的值如何设定?合法的逻辑应该是这样:如果该行不是“年轻”时代,那么其“金钱”我们认为是0,那么sql语句如何写呢?

如果是用的sql server,那么肯定要用到case了:

 

case  [时代] when '年轻' then [金钱] else 0 end as 年轻

case when  [时代]= '年轻' then [金钱] else 0 end as 年轻

 

如果用的是oracle,那么要用到decode函数,decode(1+1,3,'错',2,'是',5,'错','都不满足下返回的值'),这 个函数将返回“是”,具体用法限于篇幅这里不再介绍,相信大家从这个式子可以大概了解到其意思,用decode创建“年轻”列的句子是:完整的sql语句如下所示:

decode(时代,'年轻',金钱,0)) 年轻

 


Select [姓名],[时代],[金钱]
case  [时代] when '年轻' then [金钱] else 0 end as 年轻,
case  [时代] when '中年' then [金钱] else 0 end as 中年,
case  [时代] when '老年' then [金钱] else 0 end as 老年 
  
FROM [test].[dbo].[people] 

 

现在我们来看看其执行结果:

image

相信看到这个结果,大家都知道下一步该做什么,那就是分组:按姓名分组,并且对三个时代的金钱进行求和:

select [姓名],sum([年轻]as 年轻,sum([中年]as 中年,sum([老年]as 老年 from
(
Select [姓名],[时代],[金钱]
case  [时代] when '年轻' then [金钱] else 0 end as 年轻,
case  [时代] when '中年' then [金钱] else 0 end as 中年,
case  [时代] when '老年' then [金钱] else 0 end as 老年 
  
FROM [test].[dbo].[people]) t
  
group by [姓名]

这里用到了子查询,是为了逻辑更清晰一点,其实可以不用子查询;至于oracle下的sql语句,除了要使用decode之外,其余几乎一致,本人正是在oracle中实现之后才研究了下sql server下的实现方式。

最后看看结果:

image

事实上,当列不固定的时候,比如除了“年轻”、“中年”、“老年”以外还有其他的未知的时代,实现思路其实基本一致,只是需要动态生成sql而已。

[MVC]ASP.NET MVC Action Filter - Caching and Compr

mikel阅读(826)

原文地址: ASP.NET MVC Action Filter – Caching and Compression

下载源码: Source.zip

关于Action Filter你可以参考我的另外一篇文章: ASP.NET MVC : Action过滤器(Filtering)

 

缓存在开发高扩充性WEB程序的时候扮演着很重要的角色.我们可以将HTTP请求在一个定义的时间内缓存在用户的浏览器中,如果用户在定义的时间内请求同一个URL,那么用户的请求将会从用户浏览器的缓存中加载,而不是从服务器.你可以在ASP.NET MVC应用程序中使用下面的Action Filter来实现同样的事情:

using System;
using System.Web;
using System.Web.Mvc;
public class CacheFilterAttribute : ActionFilterAttribute
{
/// <summary>
/// Gets or sets the cache duration in seconds. The default is 10 seconds.
/// </summary>
/// <value>The cache duration in seconds.</value>
public int Duration
{
get;
set;
}
public CacheFilterAttribute()
{
Duration = 10;
}
public override void OnActionExecuted(FilterExecutedContext filterContext)
{
if (Duration <= 0) return;
HttpCachePolicyBase cache = filterContext.HttpContext.Response.Cache;
TimeSpan cacheDuration = TimeSpan.FromSeconds(Duration);
cache.SetCacheability(HttpCacheability.Public);
cache.SetExpires(DateTime.Now.Add(cacheDuration));
cache.SetMaxAge(cacheDuration);
cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
}
}

你可以好像下面一样在你的Controller Action 方法中使用这个Filter :

 
[CacheFilter(Duration = 60)]
public void Category(string name, int? page)

下面是在firebug中当 缓存Filter 没有应用的时候的截图 :

NoCache

下面的截图是应用了 Cache Filter 时候的截图 :

Cache

 

另外一个很重要的事情就是压缩.现在的浏览器都可以接收压缩后的内容,这可以节省大量的带宽.你可以在你的ASP.NET MVC 程序中应用下面的Action Filter 来压缩你的Response :

 
using System.Web;
using System.Web.Mvc;
public class CompressFilter : ActionFilterAttribute
{
public override void OnActionExecuting(FilterExecutingContext filterContext)
{
HttpRequestBase request = filterContext.HttpContext.Request;
string acceptEncoding = request.Headers["Accept-Encoding"];
if (string.IsNullOrEmpty(acceptEncoding)) return;
acceptEncoding = acceptEncoding.ToUpperInvariant();
HttpResponseBase response = filterContext.HttpContext.Response;
if (acceptEncoding.Contains("GZIP"))
{
response.AppendHeader("Content-encoding", "gzip");
response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
}
else if (acceptEncoding.Contains("DEFLATE"))
{
response.AppendHeader("Content-encoding", "deflate");
response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
}
}
}

然后将这个Filter应用到你的Controller Action 中 :

[CompressFilter]
public void Category(string name, int? page)

下面是没有应用压缩的时候的截图 :

Uncompressed

下面的截图是应用了压缩Filter后的情形 :

Compressed

你当然也可以将这两个Filter都应用到同一个Action方法上,就好像下面所示 :

 
[CompressFilter(Order = 1)]
[CacheFilter(Duration = 60, order = 2)]
public void Category(string name, int? page)

下面是截图 :

Both

Enjoy!!!

下载源码: Source.zip

作者:QLeelulu
出处:http://QLeelulu.cnblogs.com/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利
 

[C#]List(T) 的实现实体对象操作

mikel阅读(719)

实体对象类:

新闻发布实体类
     [MapTo(
"us_News")]
    
public abstract class NewsBLL 
    {
        [MapTo(
"FId")]
        
public abstract string Id { get;}
        
/// <summary>
        
/// 标题
        
/// </summary>
        [MapTo("FCaption")]
        
public abstract string Caption {getset;}
        
/// <summary>
        
/// 新闻类别PK-ss_Item.ID
        
/// </summary>
        [MapTo("FType")]
        [ManyToOne(LocalKey 
= "FType", ForeignKey = "FId")]
        
public abstract DictItemBLL Type {getset;}
        
/// <summary>
        
/// 新闻内容
        
/// </summary>
        [MapTo("FContent")]
        
public abstract string Content {getset;}
        
/// <summary>
        
/// 发布人
        
/// </summary>
        [MapTo("FUserID")]
        
public abstract string UserID {getset;}        
     }
新闻类别类
    [MapTo(
"ss_Item")]
    
public abstract class DictItemBLL 
    {
        [MapTo(
"FId")]
        
public abstract int Id { get;}
        
/// <summary>
        
/// 分组编号
        
/// </summary>
        [MapTo("FClassID")]
        
public abstract int ClassID {getset;}
        
/// <summary>
        
/// 父节点ID
        
/// </summary>
        [MapTo("FParentID")]
        
public abstract int ParentID {getset;}
        
/// <summary>
        
/// 字典名称
        
/// </summary>
        [MapTo("FName")]
        
public abstract string Name {getset;}
        
#endregion
    }

1,使用Find搜索单个匹配值

 

    NewsBLL news = list.Find(delegate(NewsBLL x) 
           { 
               
return x.Type.Id == 10001//搜索新闻列表中类别(Type)子对象中的 Id=10001的单个对象
           });

2,使用FindAll搜索多个匹配值

    List<NewsBLL> newsList = list.Find(delegate(NewsBLL x) 
           { 
               
return x.Type.Id == 10001//搜索新闻列表中类别(Type)子对象中的 Id=10001的多个对象
           });

3,是用Contains检查满足条件的值是否存在

 

    bool isContains= list.Find(delegate(NewsBLL x) 
           { 
               
return x.Type.Id == 10001 && x.UserID=="ejiyuan"//搜索新闻列表中类别(Type)子对象中的 Id=10001,并且发布人等于ejiyuan的是否存在
           });

4,使用ForEach 对每个列表对象进行操作

 

    list.Find(delegate(NewsBLL x) 
           { 
               x.Caption 
= "10001";  //将列表中所有标题都修改为10001

           });

5,使用sort排序,按类别 id排序

            list.Sort(delegate(NewsBLL x,DocumentBLL y)
            {
                
if (x.Type.Id < y.Type.Id )
                {
                    
return 1;
                }
                
else if (x.Type.Id == y.Type.Id )
                {
                    
return 0;
                }
                
else return 1;
            });

 

[C#]从.NET中委托写法的演变谈开去(下):性能相关

mikel阅读(698)

上一篇文章中,我们详细讲述了C# 3.0中Lambda表达式(构造委托)的使用方式,它在语义上的优势及对编程的简化——这些内容已经属于委托的“扩展内容”。不如这次谈得更远一些,就来讨论一下上文中“编程方式”的性能相关话题。

循环分离及其性能

  在上文的第一个示例中, 我们演示了如何使用Lambda表达式配合.NET 3.5中定义的扩展方法来方便地处理集合中的元素(筛选,转化等等)。不过有朋友可能会提出,那个“普通写法”并非是性能最高的实现方法。方便起见,也为 了突出“性能”方面的问题,我们把原来的要求简化一下:将序列中的偶数平方输出为一个列表。按照那种“普通写法”可能就是:

static List<int> EvenSquare(IEnumerable<int> source)
{
var evenList = new List<int>();
foreach (var i in source)
{
if (i % 2 == 0) evenList.Add(i);
}
var squareList = new List<int>();
foreach (var i in evenList) squareList.Add(i * i);
return squareList;
}

  从理论上来说,这样的写法的确比以下的做法在性能要差一些:

static List<int> EvenSquareFast(IEnumerable<int> source)
{
List<int> result = new List<int>();
foreach (var i in source)
{
if (i % 2 == 0) result.Add(i * i);
}
return result;
}

  在第二种写法直接在一次遍历中进行筛选,并且直接转化。而第一种写法会则根据“功能描述”将做法分为两步,先筛选后转化,并使用一个临时列表进 行保存。在向临时列表中添加元素的时候,List<int>可能会在容量不够的时候加倍并复制元素,这便造成了性能损失。虽然我们通过“分 析”可以得出结论,不过实际结果还是使用CodeTimer来测试一番比较妥当:

List<int> source = new List<int>();
for (var i = 0; i < 10000; i++) source.Add(i);
// 预热
EvenSquare(source);
EvenSquareFast(source);
CodeTimer.Initialize();
CodeTimer.Time("Normal", 10000, () => EvenSquare(source));
CodeTimer.Time("Fast", 10000, () => EvenSquareFast(source));

  我们准备了一个长度为10000的列表,并使用EvenSquare和EvenSquareFast各执行一万次,结果如下:

Normal
Time Elapsed:   3,506ms
CPU Cycles:     6,713,448,335
Gen 0:          624
Gen 1:          1
Gen 2:          0
Fast
Time Elapsed:   2,283ms
CPU Cycles:     4,390,611,247
Gen 0:          312
Gen 1:          0
Gen 2:          0

  结果同我们料想中的一致,EvenSquareFast无论从性能还是GC上都领先于EvenSquare方法。不过,在实际情况下,我们该选择哪种做法呢?如果是我的话,我会倾向于选择EvenSquare,理由是“清晰”二字。

  EvenSquare虽然使用了额外的临时容器来保存中间结果(因此造成了性能和GC上的损失),但是它的逻辑和我们需要的功能较为匹配,我们 可以很容易地看清代码所表达的含义。至于其中造成的性能损失在实际项目中可以说是微乎其微的。因为实际上我们的大部分性能是消耗在每个步骤的功能上,例如 每次Int32.Parse所消耗的时间便是一个简单乘法的几十甚至几百倍。因此,虽然我们的测试体现了超过50%的性能差距,不过由于这只是“纯遍历” 所消耗的时间,因此如果算上每个步骤的耗时,性能差距可能就会变成10%,5%甚至更低。

  当然,如果是如上述代码那样简单的逻辑,则使用EvenSquareFast这样的实现方式也没有任何问题。事实上,我们也不必强求将所有步骤 完全合并(即仅仅使用1次循环)或完全分开。我们可以在可读性与性能之间寻求一种平衡,例如将5个步骤使用两次循环来完能是更合适的方式。

  说到“分解循环”,其实这类似于Martin Fowler在他的重构网站所上列出的重构方式之一:“Split Loop”。虽然Split Loop和我们的场景略有不同,但是它也是为了代码的可读性而避免将多种逻辑放在一个循环内。将循环拆开之后,还可以配合“Extract Method”或“Replace Temp with Query”等方式实现进一步的重构。自然,它也提到拆分后的性能影响:

You often see loops that are doing two different things at once, because they can do that with one pass through a loop. Indeed most programmers would feel very uncomfortable with this refactoring as it forces you to execute the loop twice – which is double the work.

But like so many optimizations, doing two different things in one loop is less clear than doing them separately. It also causes problems for further refactoring as it introduces temps that get in the way of further refactorings. So while refactoring, don't be afraid to get rid of the loop. When you optimize, if the loop is slow that will show up and it would be right to slam the loops back together at that point. You may be surprised at how often the loop isn't a bottleneck, or how the later refactorings open up another, more powerful, optimization.

  这段文字提到,当拆分之后,您可能会发现更好的优化方式。高德纳爷爷也认为“过早优化是万恶之源”。这些说法都在“鼓励”我们将程序写的更清晰而不是“看起来”更有效率

扩展方法的延迟特性

  对于上面的简化需求,使用Lambda表达式和.NET 3.5中内置的扩展方法便可以写成这样:

static List<int> EvenSquareLambda(IEnumerable<int> source)
{
return source.Where(i => i % 2 == 0).Select(i => i + 1).ToList();
}

  应该已经有许多朋友了解了.NET 3.5中处理集合时扩展方法具有“延迟”的效果,也就是说Where和Select中的委托(两个Lambda表达式)只有在调用ToList方法的时候 才会执行。这是优点也是陷阱,在使用这些方法的时候我们还是需要了解这些方法的效果如何。不过这些方法其实都没有任何任何“取巧”之处,换句话说,它们的 行为和我们正常思维的结果是一致的。如果您想得明白,能够自己写出类似的方法,或者能够“自圆其说”,十有八九也不会有什么偏差。但是如果您想不明白它们 是如何构造的,还是通过实验来确定一下吧。实验的方式其实很简单,只要像我们之前验证“重复计算”陷阱那种方法就可以了,也就是观察委托的执行时机和顺序进行判断。

  好,回到我们现在的问题。我们知道了“延迟”效果,我们知道了Where和Select会在ToList的时候才会进行处理。不过,它们的处理 方式是什么样的,是像我们的“普通方法”那样“创建临时容器(如List<T>),并填充返回”吗?对于这点我们不多作分析,还是通过“观察 委托执行的时机和顺序”来寻找答案。使用这种方式的关键,便是在委托执行时打印出一些信息。为此,我们需要这样一个Wrap方法(您自己做试验时也可以使 用这个方法):

static Func<T, TResult> Wrap<T, TResult>(
Func<T, TResult> func,
string messgaeFormat)
{
return i =>
{
var result = func(i);
Console.WriteLine(messgaeFormat, i, result);
return result;
};
}

  Wrap方法的目的是将一个Func<T, TResult>委托对象进行封装,并返回一个类型相同的委托对象。每次执行封装后的委托时,都会执行我们提供的委托对象,并根据我们传递的messageFormat格式化输出。例如:

var wrapper = Wrap<int, int>(i => i + 1, "{0} + 1 = {1}");
for (var i = 0; i < 3; i++) wrapper(i);

  则会输出:

0 + 1 = 1
1 + 1 = 2
2 + 1 = 3

  那么,我们下面这段代码会打印出什么内容呢?

List<int> source = new List<int>();
for (var i = 0; i < 10; i++) source.Add(i);
var finalSource = source
.Where(Wrap<int, bool>(i => i % 3 == 0, "{0} can be divided by 3? {1}"))
.Select(Wrap<int, int>(i => i * i, "The square of {0} equals {1}."))
.Where(Wrap<int, bool>(i => i % 2 == 0, "The result {0} can be devided by 2? {1}"));
Console.WriteLine("===== Start =====");
foreach (var item in finalSource)
{
Console.WriteLine("===== Print {0} =====", item);
}

  我们准备一个列表,其中包含0到9共十个元素,并将其进行Where…Select…Where的处理,您可以猜出经过foreach之后屏幕上的内容吗?

===== Start =====
0 can be divided by 3? True
The square of 0 equals 0.
The result 0 can be devided by 2? True
===== Print 0 =====
1 can be divided by 3? False
2 can be divided by 3? False
3 can be divided by 3? True
The square of 3 equals 9.
The result 9 can be devided by 2? False
4 can be divided by 3? False
5 can be divided by 3? False
6 can be divided by 3? True
The square of 6 equals 36.
The result 36 can be devided by 2? True
===== Print 36 =====
7 can be divided by 3? False
8 can be divided by 3? False
9 can be divided by 3? True
The square of 9 equals 81.
The result 81 can be devided by 2? False

  列表中元素的执行顺序是这样的:

  1. 第一个元素“0”经过Where…Select…Where,最后被Print出来。
  2. 第二个元素“1”经过Where,中止。
  3. 第三个元素“2”经过Where,中止。
  4. 第四个元素“4”经过Where…Select…Where,中止。
  5. ……

  这说明了,我们使用.NET框架自带的Where或Select方法,最终的效果和上一节中的“合并循环”类似。因为,如果创建了临时容器保存 元素的话,就会在第一个Where中把所有元素都交由第一个委托(i => i % 3 == 0)执行,然后再把过滤后的元素交给Select中的委托(i => i * i)执行。请注意,在这里“合并循环”的效果对外部是隐藏的,我们的代码似乎还是一步一步地处理集合。换句话说,我们使用“分解循环”的清晰方式,但获得 了“合并循环”的高效实现。这就是.NET框架这些扩展方法的神奇之处1

  在我们进行具体的性能测试之前,我们再来想一下,这里出现了那么多IEnumerable对象实现了哪个GoF 23中的模式呢?枚举器?看到IEnumerable就说枚举器也太老生常谈了。其实这里同样用到了“装饰器”模式。每次Where或Select之后其 实都是使用了一个新的IEnumerable对象来封装原有的对象,这样我们遍历新的枚举器时便会获得“装饰”后的效果。因此,以后如果有人问您 “.NET框架中有哪些的装饰器模式的体现”,除了人人都知道的Stream之外,您还可以回答说“.NET 3.5中System.Linq.Enumerable类里的一些扩展方法”,多酷。

扩展方法的性能测试

  经过上节的分析,我们知道了Where和Select等扩展方法统一了“分解循环”的外表和“合并循环”的内在,也就是兼顾了“可读性”和“性能”。我们现在就使用下面的代码来验证这一点:

List<int> source = new List<int>();
for (var i = 0; i < 10000; i++) source.Add(i);
EvenSquare(source);
EvenSquareFast(source);
EvenSquareLambda(source);
CodeTimer.Initialize();
CodeTimer.Time("Normal", 10000, () => EvenSquare(source));
CodeTimer.Time("Fast", 10000, () => EvenSquareFast(source));
CodeTimer.Time("Lambda", 10000, () => EvenSquareLambda(source));

  结果如下:

Normal
Time Elapsed:   3,127ms
CPU Cycles:     6,362,621,144
Gen 0:          624
Gen 1:          3
Gen 2:          0
Fast
Time Elapsed:   2,031ms
CPU Cycles:     4,070,470,778
Gen 0:          312
Gen 1:          0
Gen 2:          0
Lambda
Time Elapsed:   2,675ms
CPU Cycles:     5,672,592,948
Gen 0:          312
Gen 1:          156
Gen 2:          0

  从时间上看,“扩展方法”实现的性能介于“分解循环”和“合并循环”两者之间。而GC方面,“扩展方法”实现也优于“分解循环”(您同意这个看法吗?)。因此我们可以得出以下结论:

  性能 可读性
分解循环 No. 3 No. 2
合并循环 No. 1 No. 3
扩展方法 No. 2 No. 1

  至于选择哪种方式,就需要您自行判断了。

  值得注意的是,无论是“延迟”还是“分解循环”的效果,刚才我们都是针对于Where和Select来谈的。事实上,还有并不是所有的扩展方法都有类似的特性,例如:

  • 非延迟:ToArray、ToList、Any,All,Count……
  • 非分解循环:OrderBy,GroupBy,ToDictionary……

  不过别担心,正如上节所说,是否“延迟”,是否“分解循环”都是非常明显的。如果您可以写出类似的方法,或者能够“自圆其说”,一般您的判断也 不会有什么错误。例如,OrderBy为什么不是“分解循环”的呢?因为在交由下一步枚举之前,它必须将上一步中的所有元素都获取出来才能进行排序。如果 您无法“很自然”地想象出这些“原因”,那么就写一段程序来自行验证一番吧。

其他性能问题

  一般来说,这些扩展方法本身不太会出现性能问题,但是任何东西都可能被滥用,这才是程序中的性能杀手。例如:

IEnumerable<int> source = ...;
for (var i = 0; i < source.Count(); i++)
{
...
}

  这段代码的问题,在于每次循环时都需要不断地计算出source中元素的数量,这意味着不断地完整遍历、遍历。对于一些如Count或Any这 样“立即执行”的方法,我们在使用时脑子里一定要留有一些概念:它会不会出现性能问题。这些问题的确很容易识别,但是我的确看到过这样的错误。即使在出现 这些扩展方法之前,我也看到过某些朋友编写类似的代码,如在for循环中不断进行str.Split(',').Length。

  还是再强调一下“重复计算”这个问题吧,它可能更容易被人忽视。如果您“重复计算”的集合是内存中的列表,这对性能来说可能影响不大。但是,试想您每次重复计算时,都要重复去外部数据源(如数据库)获取原始数据,那么此时造成的性能问题便无法忽视了。

总结

  这个系列的文章就写到这里,似乎想谈的也谈的差不多了。这三篇文章是由《关于最近面试的一点感想》引起的,我写文章一开始的目的也是希望证明“委托也是个值得一提的内容”。了解委托的写法,了解这些变化并非仅仅是“茴有几种写法”。这里引用钧梓昊逑同学的评论,我认为说得很有道理:

我觉得问某件事物在不同的版本中有什么区别还是可以比较容易判断出他的掌握和理解程度的。新版本中每一个新特性的加入并不是随意加的,为什么要加, 加之前有什么不足,加了之后有什么好处,在什么时候什么场景下用这些新特性,用了有什么风险和弊端,等等。如果能非常流利得回答,就算不是很正确也至少说 明他去认真思考过了,因为这些东西可能是网上书上所没有的。

  所以,我在面试时也会提出“delegate写法演变”或类似的问题。甚至我认为这是一个非常好的入手点,因为在.NET中如此经典的演变并不 多见。如果一个人可以把这些内容都理清,我有理由相信他对待技术的态度是非常值得肯定的。反观原文后许多带有嘲讽的评论,在这背后我不知道他们是真正了解 了委托而认为这些内容不值一提,还是“自以为”理解了全部内容,却不知道在这看似简单的背后还隐藏着庞大的深层的思维和理念。

  我想,我们还是不要轻易地去“轻视”什么东西吧,例如在面试时轻视对方提出的问题,看重框架而轻视语言,看重所谓“底层”而轻视“应用”。最后,我打算引用韦恩卑鄙同学的话来结束全文:

一般说C语言比C#强大的,C语言也就写到Hello World而已。

相关文章

  • 从.NET中委托写法的演变谈开去(上):委托与匿名方法
  • 从.NET中委托写法的演变谈开去(中):Lambda表达式及其优势
  • 从.NET中委托写法的演变谈开去(下):性能相关
  •  

    注1:虽然Where和Select具有“延迟”效果,但是内部实现是“分解循环”还是“合并循环”则是另一种选择。您能否尝试在“延迟”的前提下,提供“分解循环”和“合并循环”两种Where或Select的实现呢?