Hello World
Spiga

按月统计博客园单个用户的发文数量

2010-01-11 00:07 by 老赵, 7482 visits

这几天在家闲着,便试着写一些小程序。之前有朋友问到“F#能不能写Web”,于是我也就打算这么一试。虽然我能肯定,用F#写Web应用程序不会是问题,不过倒真还没有做过这方面的尝试。我想,如果用F#写Web应用程序,那么它很重要的一点,应该是利用其在异步编程方面的强大特性。最后我决定,使用F#编写一个按月统计博客园单个用户发文数量的简单服务。尝试的结果是——还有些问题没有解决。不管怎么样,我先把其主体逻辑描述一下吧。

按月统计博客园单个用户的发文数量,这个并不困难,只要利用博客园的“按月汇总”页面就行了——前几天我也利用这个页面来捕获我所有文章ID,这次还是使用这个方法。由于这是一个打算公开的服务,为了性能着想我打算利用起博客园的gzip压缩,因此我们这里为WebClient类扩展一个异步获取数据流的GetDataAsync函数:

type WebClient with 

    member c.GetDataAsync(url) =
        async {
            do c.DownloadDataAsync(new Uri(url))
            let! args = Async.AwaitEvent(c.DownloadDataCompleted)
            return args.Result
        } 

当得到了页面的数据流之后,我们便可以使用GZipStream将其解压缩了。如此,我们便可以写一个函数,生成一个异步工作流,其效果是返回某个月指定用户所有文章的URL:

let getPostUrlsAsync alias (beginMonth: DateTime) (endMonth: DateTime) = 

    if (beginMonth > endMonth) then
        failwith "beginMonth must smaller then or equals to endMonth"

    let getPostUrlsAsync' alias (m: DateTime) = 
        async {
            let webClient = new WebClient();
            webClient.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip")

            let url = sprintf "http://%s.cnblogs.com/archive/%i/%i.html" alias m.Year m.Month
            let! data = webClient.GetDataAsync(url)
            
            let rawStream = new MemoryStream(data)
            let gzipStream = new GZipStream(rawStream, CompressionMode.Decompress)
            let reader = new StreamReader(gzipStream)
            let html = reader.ReadToEnd()

            let regex = @"..."
            return [ for m in Regex.Matches(html, regex) -> m.Groups.Item(1).Value ]
        }

    ...

在getPostUrlsAsync'函数中,我们首先拼接出该用户月份汇总页的URL——使用子域名的方式。博客园有两种方式可以访问某个博客,一是子域名,二是普通的URL形式。在这里我使用子域名的访问方式是避免.NET类库中对单个域名2个连接的限制。现在这个服务可以同时统计不同用户的信息而不会冲突。不过对于单个用户,还是只能同时出现2个连接——嗯,这是个Feature,避免在虚拟主机上消耗太多资源的Feature。

上面还省略了一个正则表达式,其实它是这样的:

<a\s[^>]*href=["|'](http://www.cnblogs.com/\w+/archive/\d{4}/\d{2}/\d{2}/[^.]*\.html)["|'][^>]*>\s*阅读全文\s*</a>

这个正则表达式实在不容易找。博客园的不同皮肤的HTML可能截然相反,我原本以为都会有EditPosts.aspx?postid=12345这样的链接,但事实证明……有些模板中这样的链接是使用JavaScript生成的,于是“此路不通”。后来我又想通过“评论”链接来捕获URL,但发现有的皮肤使用#FeedBack,有的却使用#Comments。总之,很难统一起来便是了。

最后,我决定通过使用每篇文章中的“阅读全文”链接来识别一篇文章——便是这样,您从这个正则表达式中也可以发现,这是一个较为宽泛的Pattern,甚至href属性还要分单引号和双引号两种情况。这个方法有缺陷,因为对于一些非常短的文章,博客园是不会为其生成“阅读全文”链接的——不过,这也算是个Feature吧,太短的文章咱就不算了。:P

接下来便是从beginMonth和endMonth参数中收集任务,并进行“汇总”了:

let getPostUrlsAsync alias (beginMonth: DateTime) (endMonth: DateTime) = 

    ...

    let executeAsync tasks =
        let rec executeAsync' (tasks: (_ * Async<_>) list) acc = 
            async {
                match tasks with
                | [] -> return acc |> List.rev
                | (pre, task) :: ts ->
                    let! result = task
                    return! executeAsync' ts ((pre, result) :: acc)
            }
        
        executeAsync' tasks List.empty

    Seq.initInfinite (fun i -> endMonth.AddMonths(-i))
    |> Seq.takeWhile (fun m -> m >= beginMonth)
    |> Seq.map (fun m -> (m, getPostUrlsAsync' alias m))
    |> Seq.toList
    |> executeAsync

F#与Haskell不同,它不是延迟的语言,因此它的List必须是有限的,即时生成的。不过Seq便不同了,F#中的Seq可以认作是IEnumerable的对应物,可以是无限的,而Seq.initInfinite便是初始化这样一个无限的序列。不过Seq.takeWhile却只是取到这个序列大于等于beginMonth的那些元素——然后我们再将其映射成月份与“获取单月文章URL”这个异步工作流的“元组”。最后,再使用executeAsync将DateTime * Async<string list>汇总成Async<(DateTime * string list) list>类型。由于我打算把它部署在虚拟主机上,为了节省资源没有把它们并行处理——因此在最后执行时耗时会有些长,不过最后感觉下来,速度基本还可以接受。

最后,我们与上次相同,写个异步Handler用于处理请求:

#light

namespace CnBlogsMonitoring

open System
open System.Web

type PostsOfMonthsHandler() =
    let mutable m_context = null
    let mutable m_endWork = null

    interface IHttpAsyncHandler with
        member h.IsReusable = false
        member h.ProcessRequest(context) = failwith "not supported"

        member h.BeginProcessRequest(c, cb, state) =
            m_context <- c

            let alias = c.Request.QueryString.Item("alias").Trim()
            let bm = DateTime.ParseExact(c.Request.QueryString.Item("begin"), "yyyy/MM", null)
            let em = DateTime.ParseExact(c.Request.QueryString.Item("end"), "yyyy/MM", null)

            let monthDiff = (em.Month - bm.Month) + (em.Year - bm.Year) * 12
            if monthDiff > 12 then failwith "Please pick a range no larger than 12 months."

            let title = sprintf "%s: %i/%i ~ %i/%i" alias bm.Year bm.Month em.Year em.Month
            m_context.Items.Item("Title") <- title

            let work = PostMonitor.getPostUrlsAsync alias bm em
            let beginWork, endWork, _ = Async.AsBeginEnd work
            m_endWork <- new Func<_, _>(endWork)

            beginWork (cb, state)

        member h.EndProcessRequest(ar) =
            m_context.Items.Item("PostsOfMonths") <- m_endWork.Invoke ar
            m_context.Server.Transfer("PostsOfMonths.aspx")

在EndProcessRequest方法中,我们得到了统计结果。在这里我的处理方式是将请求Transfer到PostsOfMonths.aspx这个页面去显示HTML——传递数据的方式是利用HttpContext.Items集合。

在PostsOfMonths.aspx中,显示HTML的方式与普通页面毫无二致:

public partial class PostsOfMonths : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        this.Title = this.Context.Items["Title"].ToString();

        this.rptPostsOfMonths.DataSource = this.Context.Items["PostsOfMonths"];
        this.rptPostsOfMonths.DataBind();
    }
}
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <h1><%= this.Title %></h1>
    
    <asp:Repeater runat="server" ID="rptPostsOfMonths">
        <ItemTemplate>
            <h2><%# Eval("Item1", "{0:yyyy/MM}") %> - <%# Eval("Item2.Length") %> post(s)</h2>
            
            <asp:Repeater runat="server" DataSource='<%# Eval("Item2") %>'>
                <HeaderTemplate><ul></HeaderTemplate>
                <ItemTemplate>
                    <li>
                        <a href="<%# Container.DataItem %>"><%# Container.DataItem %></a>
                    </li>
                </ItemTemplate>
                <FooterTemplate></ul></FooterTemplate>
            </asp:Repeater>
        </ItemTemplate>
    </asp:Repeater>
</body>
</html>

从F#代码中我们知道,HttpContext.Items["PostsOfMonths"]的类型是(DateTime * string list) list,也就是说,绑定至rptPostsOfMonths中的每一项都是个DateTime * string list对象——写成C#的形式便是Tuple<DateTime, FSharpList<string>>,其中包含Item1和Item2两个属性,分别是DateTime和FSharpList<string>类型,而后者又会绑定至内层的Repeater中,最终生成整页的HTML。

那么我为什么不写个异步的WebForm页面呢?因为经过我的简单尝试,在WebClient.GetDataAsync扩展里的Async.AwaitEvent操作中会抛出InvalidOperationException异常:

Asynchronous operations are not allowed in this context. Page starting an asynchronous operation has to have the Async attribute set to true and an asynchronous operation can only be started on a page prior to PreRenderComplete event.

经过了多番检查和比对,我始终没有发现出了什么问题,因此最后还是使用了异步Handler的方式编写这个服务。按理说,异步Hander可以正常工作,异步页面也应该没有什么问题,具体原因我会继续探究一下。不过,如果只是编写一个同步页面的话,不会出现任何问题。

最后,我把这个简单服务部署到了免费的虚拟主机上(以前用的Hosting由于众所周知的原因,在国内已经无法使用了——现在这个只能用到1月底),您可以在文末访问到该服务的入口页:这是一个简单的静态页面,填好信息后点击Submit便会提交至那个异步Handler进行处理——稍等片刻,便会得到结果。F#的方便之处,在于它编译后的结果便是标准的.NET程序集,使用时也只是复制一些dll便可——无需额外支持。这样看来,有一个统一的虚拟机平台的确方便,例如GAE在支持Java平台之后便相当于直接支持Scala语言了。同理,在.NET平台上使用Rails(Ruby)、Django(Python)等框架似乎都不是个梦想。

谁说.NET封闭?我感觉没有比.NET更海纳百川的平台了,呵呵——这也是我喜欢.NET平台的最大原因。

服务入口:http://user868.netfx4lab.discountasp.net/PostsOfMonths.html

本文代码:http://gist.github.com/273556

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

24 条回复

  1. xuefly
    *.*.*.*
    链接

    xuefly 2010-01-11 00:15:00

    暂时看不懂 沙发先

  2. 永不言败
    *.*.*.*
    链接

    永不言败 2010-01-11 00:52:00

    太牛了。一辈子都追不上。老赵有空研究下MonoRail呀。。

  3. 老赵
    admin
    链接

    老赵 2010-01-11 02:08:00

    @永不言败
    为什么要研究这个?什么时候的东西了啊……

  4. winter-cn
    *.*.*.*
    链接

    winter-cn 2010-01-11 02:44:00

    永不言败:太牛了。一辈子都追不上。


    xuefly:暂时看不懂 沙发先


    估计老赵最不喜欢看到的就是这种评论了吧

  5. 紫色永恒
    *.*.*.*
    链接

    紫色永恒 2010-01-11 08:26:00

    初看f#搞的头昏眼花,最近有点眉目了
    老赵学习f#参考的是哪里的资料

  6. xuefly
    *.*.*.*
    链接

    xuefly 2010-01-11 08:29:00

    书名:Beginning F#
    作者:Robert Pickering
    出版日期:December 22, 2009
    出版社:Apress
    页数:448 pages
    ISBN:1430223898

    书名:Programming F#
    作者:Chris Smith
    副书名:A comprehensive guide for writing simple code to solve complex problems
    出版日期:2009 10
    出版社:O'Reilly
    页数:408
    ISBN:978-0-596-15364-9

    书名:Expert F#
    作者:Don Syme, Adam Granicz, Antonio Cisternino
    出版日期:December 2007
    出版社:Apress
    页数:609
    ISBN:1590598504

    书名:Foundations of F#
    作者:Robert Pickering
    出版日期:May 2007
    出版社:Apress
    页数:360
    ISBN:1590597575

    书名:Functional Programming for the Real World: With Examples in F# and C#
    作者:Tomas Petricek, Jon Skeet
    出版日期:October 28, 2009
    出版社:Manning Publications
    页数:500
    ISBN:ISBN-10: 1933988924 ISBN-13: 978-1933988924

    书名:F# for Scientists
    作者:Jon Harrop
    出版日期:August 4, 2008
    出版社:Wiley Publishing
    页数:368
    ISBN:ISBN-10: 0470242116 ISBN-13: 978-0470242117
    http://www.ppurl.com/?s=F%23

  7. Leon Weng
    *.*.*.*
    链接

    Leon Weng 2010-01-11 08:32:00

    老赵,真的很慢哪。不知道是不是因为我的网速。。

  8. 李永京
    *.*.*.*
    链接

    李永京 2010-01-11 09:22:00

    Leon Weng:老赵,真的很慢哪。不知道是不是因为我的网速。。


    慢慢爬博客园的东西能不慢么,而且也不是异步的~~~

  9. Ivony...
    *.*.*.*
    链接

    Ivony... 2010-01-11 10:07:00

    Asynchronous operations are not allowed in this context. Page starting an asynchronous operation has to have the Async attribute set to true and an asynchronous operation can only be started on a page prior to PreRenderComplete event.

    在此上下文不支持异步操作,页面开始异步操作必须确保Async attribute设置为true并且异步操作只能在页面的PreRenderComplete事件之前开始?

  10. 微软MVP
    *.*.*.*
    链接

    微软MVP 2010-01-11 10:50:00

    管它什么新技术,能赚钱的技术就是好技术

  11. RayChueng
    *.*.*.*
    链接

    RayChueng 2010-01-11 10:55:00

    老赵真是牛啊……向老赵学习。

  12. Harold Shen
    *.*.*.*
    链接

    Harold Shen 2010-01-11 11:33:00

    微软又发布了一个新的语言Axum,是为了并行计算,F#是函数式编程,我觉得微软应该将二者结合起来推出一个新的语言,这样才好,不然我为了并行使用Axum,为了函数式使用F#,我觉得这样不行,最有前途的技术不是完全改变现在编程的模式,而是在此基础上发展出来新的特性。譬如 从二进制 -〉汇编 -〉C语言 -> C++ -> C#,java -> 下一个语言
    下一个语言应该市在C#,java基础上的,适应新的特性而不是像Axum,F#,各兼顾一个方面,这样走不长久的,两条腿走路比较稳。

  13. Harold Shen
    *.*.*.*
    链接

    Harold Shen 2010-01-11 11:35:00

    不过我还是计划学习F#。

  14. ZC29
    *.*.*.*
    链接

    ZC29 2010-01-11 12:27:00

    老赵博客园排名到榜首了啊,恭喜啊

  15. canbeing
    *.*.*.*
    链接

    canbeing 2010-01-11 12:45:00

    昨天在twitter就看到了你的demo,哈哈

  16. 老赵
    admin
    链接

    老赵 2010-01-11 15:20:00

    李永京:

    Leon Weng:老赵,真的很慢哪。不知道是不是因为我的网速。。


    慢慢爬博客园的东西能不慢么,而且也不是异步的~~~


    概念啊概念……谁说不是异步的,只不过不是并行爬而已。

  17. 老赵
    admin
    链接

    老赵 2010-01-11 15:21:00

    Ivony...:
    在此上下文不支持异步操作,页面开始异步操作必须确保Async attribute设置为true并且异步操作只能在页面的PreRenderComplete事件之前开始?


    但是检查了半天没有看出问题来,用C#是可以的,我打算做更多试验缩小问题范围。

  18. 老赵
    admin
    链接

    老赵 2010-01-11 15:21:00

    Leon Weng:老赵,真的很慢哪。不知道是不是因为我的网速。。


    不一定是你的网速,可能是博客园的网速,可能是虚拟主机的网速。

  19. 老赵
    admin
    链接

    老赵 2010-01-11 15:25:00

    Harold Shen:
    微软又发布了一个新的语言Axum,是为了并行计算,F#是函数式编程,我觉得微软应该将二者结合起来推出一个新的语言,这样才好,不然我为了并行使用Axum,为了函数式使用F#,我觉得这样不行,最有前途的技术不是完全改变现在编程的模式,而是在此基础上发展出来新的特性。譬如 从二进制 -〉汇编 -〉C语言 -> C++ -> C#,java -> 下一个语言
    下一个语言应该市在C#,java基础上的,适应新的特性而不是像Axum,F#,各兼顾一个方面,这样走不长久的,两条腿走路比较稳。


    我不同意你这个看法啊,这里提几个问题:
    1、Axum是并行计算,是Message Passing模式,这种语言的成功案例有Erlang,它的优势可以参考我的或别人的Actor相关文章。
    2、函数式编程也是通用编程语言,它的一个重要应用方向就是并行计算,函数式编程和并行编程不是分割的,它们是不同层面的。
    3、函数式编程不是在改变现在编程模式,它的历史比C这种命令式编程还要悠久……
    4、我觉得,只是你不了解函数式编程,所以觉得命令式编程比较“稳”,其实函数式编程语言也是如C -> C++这样逐渐发展的……

  20. 老赵
    admin
    链接

    老赵 2010-01-11 15:27:00

    微软MVP:管它什么新技术,能赚钱的技术就是好技术


    如果新技术赚钱快,就是好技术,哈哈。

  21. NullReference
    *.*.*.*
    链接

    NullReference 2010-01-11 16:08:00

    新手前来拜访大师。

  22. Harold Shen
    *.*.*.*
    链接

    Harold Shen 2010-01-12 09:01:00

    恩,决定去学F#了,慢慢看看。

  23. 极品拖拉机
    *.*.*.*
    链接

    极品拖拉机 2010-01-13 09:52:00

    F# 和Erlang比较下
    那个优势大点。
    瞅瞅
    Erlang语法真他妈扯蛋,本来眼睛就不好

  24. 老赵
    admin
    链接

    老赵 2010-01-13 12:15:00

    @极品拖拉机
    习惯问题吧

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我