(转)C# HTML解析示例---星星引发的血案

原文地址:http://www.cnblogs.com/wurang/archive/2013/06/14/3119023.html

【前言】  

    从CSDN转投cnBlog也有一段时间了,发现cnBlog中也有类似CSDN的迷你博客的功能,就是闪存。闪存使用了幸运星的机制也引发一大批人没事就来刷星星……虽然不知道有什么用,但无聊中也试过几次。由于幸运星随机分发,那么就有一个想法,不停的发消息,不是星星的就删掉以免有刷屏嫌疑。手动操作起来当然怪麻烦的,于是干脆用代码,这就产生了一个需求:获取html,解析,自动提交登陆,自动发布,判断是否是星星,删除等等。

【方案】Webbrowser

      因为只是想随手玩下,没考虑复杂性和完善程度,我最先想到的是用webbrowser,然后获取html,纯手动解析。

      Step1:表单填充

      首先当然是放置一个Webbrowser控件,为了方便,直接设置了url为http://passport.cnblogs.com/login.aspx

      然后登陆http://passport.cnblogs.com/login.aspx,查看源代码获取登陆框的id。

复制代码
<input name="tbUserName" type="text" id="tbUserName" class="Textbox" />

<input name="tbPassword" type="password" id="tbPassword" class="Textbox" />

<input type="submit" name="btnLogin" value="登  录" onclick="javascript:WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions("btnLogin", "", true, "", "", false, false))" id="btnLogin" class="Button" style="margin-top: 8px" />
复制代码

    程序开始运行后Webbrowser会自动打开http://passport.cnblogs.com/login.aspx,我们就要在Webbrowser加载页面结束后来做表单填充,那么如何得知页面已经被加载完成了呢?这里可以使用Webbrowser的DocumentCompleted事件,当Webbrowser加载页面结束后,会触发这个事件,我们只需要在这个事件中做表单填充就可以了。表单填充和提交的方法如下:

复制代码
            HtmlDocument doc = wbBlog.Document;
            foreach (HtmlElement em in doc.All)
            {
                string str = em.Name;

                switch (str)
                {
                    case "tbUserName":
                        em.SetAttribute("value", user);
                        break;
                    case "tbPassword":
                        em.SetAttribute("value", pwd);
                        break;
                    case "btnLogin":
                        isLogIn = true;
                        em.InvokeMember("click");
                        break;
                }
            }
复制代码

      如果登陆成功,页面应该跳转至主页,程序也需要导航到闪存的网址http://home.cnblogs.com/ing,如果未成功则还是停留在该页面,所以通过判断webbrowser的当前url就可以知道是否登陆成功了,这是一个取巧的方法。当然,判断当前url也需要在DocumentCompleted事件中,因为我们需要等待页面刷新结束后才能做判断。

复制代码
            isLogIn = false;
            if (wbBlog.Url.ToString() == "http://passport.cnblogs.com/login.aspx")
            {
                System.Windows.MessageBox.Show("用户名或密码错误!");
                return;
            }
            else
            {
                isSetForm = false;
                mylogin.Close();
                wbBlog.Navigate("http://home.cnblogs.com/ing/");
                this.Show();
            }
复制代码

      这时候可能会发现DocumentCompleted事件中需要做的事有点多了,会不会有冲突或者重复执行?所以我们需要一些标记来控制。在上面的代码中可以看到isLogIn这个变量,就是用于控制在DocumentCompleted到底要执行判断还是执行表格填充。

       Step2:发布闪存

       登陆成功后,webbrowser跳转到闪存页面,这时候需要程序自动发布闪存,原理也是表单填充和提交。可以看下页面的源码。

<textarea class="ing_text" onblur="IngIsEmpty();" onfocus="HideTip()" onkeydown="return PublicIngEnterNew(event)" id="txt_ing">你在做什么?你在想什么?</textarea>

 <input type="submit" name="btnLogin" value="登  录" onclick="javascript:WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions(&quot;btnLogin&quot;, &quot;&quot;, true, &quot;&quot;, &quot;&quot;, false, false))" id="btnLogin" class="Button" style="margin-top: 8px" />    

        然后程序需要做的就是不停的做填充和提交,然后判断是否有星星,如果有就退出循环。

复制代码
            HtmlDocument doc = wbBlog.Document;
            foreach (HtmlElement em in doc.All)
            {
                string str = em.Id;

                switch (str)
                {
                    case "txt_ing":
                        content = em;
                        em.SetAttribute("value", txtContent.Text);
                        break;
                    case "btn_ing_publish":
                        isPublish = true;
                        submit = em;
                        em.InvokeMember("click");
                        break;
                }
            }
复制代码

       提交表单后,页面会刷新,所以判断是否有星星也是要在DocumentCompleted事件中,同时需要isPulish这个标记来表示是否需要执行判断方法。

       Step3:判断是否有星

       分析闪存页面的源码可以看到每一条闪存的html都是下面这样:

复制代码
<div class="feed_body" id="feed_content_414865"><a href="/u/516258/" class="ing-author" target="_blank">作者</a><span class="ing_body" id="ing_body_414865">内容</span><img src="http://static.cnblogs.com/images/ing_lucky.png" class="ing_icon_lucky" alt="" title="这是幸运闪"/> <a class="ing_time" href="/ing/414865/" title="发布于 6-5 10:15:43,点击进入详细页面" target="_blank">31分钟前</a> <a href="#" id="a_414865" onclick="showCommentBox(414865,516258);return false;" class="ing_reply" title="点击进行回应">回应</a><div class="ing_comments"><div class='feed_ing_comment_block'><ul id="comment_block_414865"><li style="display:none">&nbsp;</li></ul><div class='ing_cm_box' id='panel_414865'></div></div></div></div><div class="clear"></div></div></li><li class="entry_a"><div class="ing-item"><div class="feed_avatar"><a href="/u/liujinyao/" target="_blank"><img width="36" height="36" src="http://pic.cnitblog.com/face/502329/20130312132011.png" alt=""/></a></div>
复制代码

        所以首先要获取id格式是feed_content_***的所有div,然后判断这个htmlelement中是否包含了自己发布的信息,如果是就锁定这个element,然后判断是否包含

<img src="http://static.cnblogs.com/images/ing_lucky.png" class="ing_icon_lucky" alt="" title="这是幸运闪"/>

      如果有,则提示发布成功,如果没有则删除这条闪存并继续发布。需要注意的是,发布一条新的闪存后并没有删除选项,

                                                                               

      需要刷新一下页面才会看到,包括查看是否有星星也是要刷新后才能判断。

复制代码
             HtmlDocument doc = wbBlog.Document;
             foreach (HtmlElement em in doc.All)
             {
                 string str = em.Id; 
                 if (str != null && str.Contains("feed_content") && em.OuterHtml.Contains(txtContent.Text))
                 {
                     if (em.OuterHtml.Contains("http://static.cnblogs.com/images/ing_lucky.png"))
                     {
                         lstInfo.Items.Add("获得幸运闪:" + txtContent.Text);
                     }
                     else
                     {
                         //删除
                     }
                 }
             }
复制代码

       Step4:删除闪存

      程序写到这里我遇到了麻烦,由于我是获取id是feed_content_***的div,现在要取得div中的删除链接,发现这个a链接没有id,那该如何获取?貌似需要用正则表达式了。但这里偷了个懒,获取页面所有的a连接,然后判断title属性是不是为“删除这个闪存”,从而获取这个a连接元素。

<a class='recycle' onclick='return DelIng(415025)' href='javascript:void(0);' title='删除这个闪存' >

        得到a连接的元素之后就可以操作它的Click事件了,但又有一个新问题,点击删除之后,这货居然弹出一个Confirm对话框,继而引出一个老问题,如何干掉网页弹出的Confirm和Alert对话框。这里使用一个原始方法,让页面所有的function confirm()都自动返回ture。首先需要引用Microsoft.mshtml和Interop.SHDocVw,具体操作代码如下:

复制代码
                                HtmlElementCollection hrefs = em.GetElementsByTagName("a");
                                foreach (HtmlElement h in hrefs)
                                {
                                    if (h.GetAttribute("title") == "删除这个闪存")
                                    {
                                        IHTMLDocument2 doc1 = (wbBlog.ActiveXInstance as SHDocVw.WebBrowser).Document as IHTMLDocument2;
                                        doc1.parentWindow.execScript("function confirm(){return true;}", "javascript");
                                        h.InvokeMember("click");
                                        //等待
                                        return;
                                    }
                                }
复制代码

         做到这一步,基本功能已经实现,现在需要做的就是在发布,判断和删除这几个操作中做循环,需要注意的是网页页面上的刷新和删除闪存是通过ajax刷新部分div,所以webbrowser不会触发DocumentCompleted事件,这里可以仿照winform写一个DoEvent,还需要Sleep一段时间。然后才能读取刷新后的页面信息。

复制代码
        public void DoEvent()
        {
            DispatcherFrame frame = new DispatcherFrame();
            Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(ExitFrame), frame);
            Dispatcher.PushFrame(frame);
        }
        public object ExitFrame(object f)
        {
            ((DispatcherFrame)f).Continue = false;
            return null;
        }
复制代码

【结束】

      到这里程序就写完了,运行一下,发现程序在不停的控制发布和删除,但还是有问题,刷不到星星。cnblog的星星虽说是随机分配,但是相同的内容,或者相隔时间太短都会被排除,闪存还有两个机制,同一页面只允许一个用户发布五条信息,用户每天发布闪存的数量是有上限的,具体多少没有统计,是用程序刷了几百条后给出的提示。所以程序虽然写完了,但却没有达到最初的效果,这不禁让人失望。不过换一种思路,一次发布五条不同的闪存(需前后有数秒间隔),然后依次判断是否有星星,删除没有星星的,保留有星星的,这样应该就符合规则了。当然,本来是随手写写的东西发现还是挺复杂的,这部分就没有再实现了。其实这篇文章的主要目的是HTML解析不是么?

                   

      回过头来想想,如果真的要做这样一个工具,用webbrowser做html解析会导致程序的可维护性和执行效率很低,如果是解析html,推荐使用Html Agility Pack,而提交删除等操作则可以用网页开发工具抓个包分析然后用ajax直接发送请求。举个栗子,在之前的代码中,我们要获取闪存的div以及闪存的内容并判断是否有星星等操作是比较复杂的,如果使用Html Agility Pack,Xpath将轻松搞定一切。

复制代码
            string pageUrl = "http://home.cnblogs.com/ing/";
            WebClient wc = new WebClient();
            byte[] pageSourceBytes = wc.DownloadData(new Uri(pageUrl));
            string pageSource = Encoding.GetEncoding("utf-8").GetString(pageSourceBytes);

            HtmlDocument doc = new HtmlDocument();
            doc.LoadHtml(pageSource);

            string xpath = @"//div[@class='feed_body']";
            HtmlNodeCollection keyNodes = doc.DocumentNode.SelectNodes(xpath);
            foreach (HtmlNode node in keyNodes)
            {
                HtmlNode img = node.SelectSingleNode("./img[@class='ing_icon_lucky']");
                
                if (img != null)
                {
                    Debug.WriteLine(node.InnerText);
                   // Debug.WriteLine("luck: " + keyNode.SelectSingleNode("//span[@class='ing_body']").InnerText + "
");
                } 
            }
复制代码

      最后附上程序源码,有兴趣的可以重构一下程序,完成未实现的功能部分。

源码下载

原文地址:https://www.cnblogs.com/fcsh820/p/3143659.html