(转)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("btnLogin", "", true, "", "", 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"> </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 + "\n"); } }
最后附上程序源码,有兴趣的可以重构一下程序,完成未实现的功能部分。