以前看过某个同窗的一篇有关与使用JSoup解析学校图书馆的文章,仔细一看,发现居然是同校!!既然对方用的是java,那么我也就来个C#好了,虽然个人入门语言是java。javascript
C#没有JSoup这样方便的东西,我也没有仔细去找,由于只要利用正则表达式,一样能够很好的解析网页内容而不须要其余帮助。如今作前端的程序员,若是正则表达式不熟悉,反而去依赖第三方的话,感受很惋惜!html
这是咱们学校图书馆的登陆界面的body:前端
<body onload="bodyload()"> <form name="aspnetForm" method="post" action="login.aspx?ReturnUrl=%2fuser%2fuserinfo.aspx" onsubmit="javascript:return WebForm_OnSubmit();" id="aspnetForm"> <div> <input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="" /> <input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="" /> <input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwULLTE0MjY3MDAxNzcPZBYCZg9kFgoCAQ8PFgIeCEltYWdlVXJsBRt+XGltYWdlc1xoZWFkZXJvcGFjNGdpZi5naWZkZAICDw8WAh4EVGV4dAUt5bm/5Lic5bel5Lia5aSn5a2m5Zu+5Lmm6aaG5Lmm55uu5qOA57Si57O757ufZGQCAw8PFgIfAQUcMjAxM+W5tDA35pyIMjHml6UgIOaYn+acn+aXpWRkAgQPZBYEZg9kFgQCAQ8WAh4LXyFJdGVtQ291bnQCCBYSAgEPZBYCZg8VAwtzZWFyY2guYXNweAAM55uu5b2V5qOA57SiZAICD2QWAmYPFQMTcGVyaV9uYXZfY2xhc3MuYXNweAAM5YiG57G75a+86IiqZAIDD2QWAmYPFQMOYm9va19yYW5rLmFzcHgADOivu+S5puaMh+W8lWQCBA9kFgJmDxUDCXhzdGIuYXNweAAM5paw5Lmm6YCa5oqlZAIFD2QWAmYPFQMUcmVhZGVycmVjb21tZW5kLmFzcHgADOivu+iAheiNkOi0rWQCBg9kFgJmDxUDE292ZXJkdWVib29rc19mLmFzcHgADOaPkOmGkuacjeWKoWQCBw9kFgJmDxUDEnVzZXIvdXNlcmluZm8uYXNweAAP5oiR55qE5Zu+5Lmm6aaGZAIID2QWAmYPFQMbaHR0cDovL2xpYnJhcnkuZ2R1dC5lZHUuY24vAA/lm77kuabppobpppbpobVkAgkPZBYCAgEPFgIeB1Zpc2libGVoZAIDDxYCHwJmZAIBD2QWBAIDD2QWBAIBDw9kFgIeDGF1dG9jb21wbGV0ZQUDb2ZmZAIHDw8WAh8BZWRkAgUPZBYGAgEPEGRkFgFmZAIDDxBkZBYBZmQCBQ8PZBYCHwQFA29mZmQCBQ8PFgIfAQWlAUNvcHlyaWdodCAmY29weTsyMDA4LTIwMDkuIFNVTENNSVMgT1BBQyA0LjAxIG9mIFNoZW56aGVuIFVuaXZlcnNpdHkgTGlicmFyeS4gIEFsbCByaWdodHMgcmVzZXJ2ZWQuPGJyIC8+54mI5p2D5omA5pyJ77ya5rex5Zyz5aSn5a2m5Zu+5Lmm6aaGIEUtbWFpbDpzenVsaWJAc3p1LmVkdS5jbmRkZLjlteIKM9K+qxtjyYb5tuBVJpjN" /> </div> <script type="text/javascript"> //<![CDATA[ var theForm = document.forms['aspnetForm']; if (!theForm) { theForm = document.aspnetForm; } function __doPostBack(eventTarget, eventArgument) { if (!theForm.onsubmit || (theForm.onsubmit() != false)) { theForm.__EVENTTARGET.value = eventTarget; theForm.__EVENTARGUMENT.value = eventArgument; theForm.submit(); } } //]]> </script> <script src="/WebResource.axd?d=aUGQsxtqBlPCYSLCotjFAgPO7LVRMgZpwdLaRZWG0_6ihSU9GEuAV24Gz4casq4yN9Ey0mGcWUzl2dmajXQUJps-v9o1&t=635090739058261250" type="text/javascript"></script> <script src="/WebResource.axd?d=ID2SHi1EXLOLcv8QZV5z65ofzpfIKQP67HbOJyDtBOZGRBT6-d--Al86w9CE4E-H3dCnvuE2ZcqgPXnod-92Tv-ZeIo1&t=635090739058261250" type="text/javascript"></script> <script type="text/javascript"> //<![CDATA[ function WebForm_OnSubmit() { if (typeof(ValidatorOnSubmit) == "function" && ValidatorOnSubmit() == false) return false; return true; } //]]> </script> <div> <input type="hidden" name="__EVENTVALIDATION" id="__EVENTVALIDATION" value="/wEWBQLuwfDDAwKOmK5RApX9wcYGAsP9wL8JAqW86pcI915B4NnPkyRLP7m6znmysZd+180=" /> </div> <input name="ctl00$ContentPlaceHolder1$txtlogintype" type="hidden" id="ctl00_ContentPlaceHolder1_txtlogintype" value="0" /> <div id="Login" class="clearFix"> <div class="LoginTitle"> 登陆个人图书馆 </div> <div class="LeftLogin"> <div class="LoginDiv"> <div class="loginContent"> <div class="loginInfo"> <span class="leftInfo">图书证号:</span> <span class="rightInfo"> <input name="ctl00$ContentPlaceHolder1$txtUsername_Lib" type="text" id="ctl00_ContentPlaceHolder1_txtUsername_Lib" class="txtInput" autocomplete="off" /><span id="ctl00_ContentPlaceHolder1_rfv_UserName_Lib" style="color:Red;display:none;">请输入证号</span> </span> </div> <div class="loginInfo"> <span class="leftInfo">密 码:</span> <span class="rightInfo"> <input name="ctl00$ContentPlaceHolder1$txtPas_Lib" type="password" id="ctl00_ContentPlaceHolder1_txtPas_Lib" class="txtInput" /><span id="ctl00_ContentPlaceHolder1_rfv_Password_Lib" style="color:Red;display:none;">请输入密码</span> </span> </div> <div> <span id="ctl00_ContentPlaceHolder1_lblErr_Lib"></span> </div> <div class="loginInfo"> <input type="submit" name="ctl00$ContentPlaceHolder1$btnLogin_Lib" value="登陆" onclick="javascript:WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions("ctl00$ContentPlaceHolder1$btnLogin_Lib", "", true, "", "", false, false))" id="ctl00_ContentPlaceHolder1_btnLogin_Lib" class="btn" /> <input type="button" value="清空" onclick="rset()" class="btn"/> </div> </div> </div> </div> <div class="RightDescription"> <img src="images/pin.gif" /> <br/> 1. 若是您使用的是公共电脑,请在使用完毕后,务必退出登陆,以保安全。<br /> 2. 首次登陆,请先<a href="changepas.aspx">修改初始密码</a>。 </div> </div> <script type="text/javascript"> //<![CDATA[ var Page_Validators = new Array(document.getElementById("ctl00_ContentPlaceHolder1_rfv_UserName_Lib"), document.getElementById("ctl00_ContentPlaceHolder1_rfv_Password_Lib")); //]]> </script> <script type="text/javascript"> //<![CDATA[ var ctl00_ContentPlaceHolder1_rfv_UserName_Lib = document.all ? document.all["ctl00_ContentPlaceHolder1_rfv_UserName_Lib"] : document.getElementById("ctl00_ContentPlaceHolder1_rfv_UserName_Lib"); ctl00_ContentPlaceHolder1_rfv_UserName_Lib.controltovalidate = "ctl00_ContentPlaceHolder1_txtUsername_Lib"; ctl00_ContentPlaceHolder1_rfv_UserName_Lib.focusOnError = "t"; ctl00_ContentPlaceHolder1_rfv_UserName_Lib.errormessage = "请输入证号"; ctl00_ContentPlaceHolder1_rfv_UserName_Lib.display = "Dynamic"; ctl00_ContentPlaceHolder1_rfv_UserName_Lib.evaluationfunction = "RequiredFieldValidatorEvaluateIsValid"; ctl00_ContentPlaceHolder1_rfv_UserName_Lib.initialvalue = ""; var ctl00_ContentPlaceHolder1_rfv_Password_Lib = document.all ? document.all["ctl00_ContentPlaceHolder1_rfv_Password_Lib"] : document.getElementById("ctl00_ContentPlaceHolder1_rfv_Password_Lib"); ctl00_ContentPlaceHolder1_rfv_Password_Lib.controltovalidate = "ctl00_ContentPlaceHolder1_txtPas_Lib"; ctl00_ContentPlaceHolder1_rfv_Password_Lib.focusOnError = "t"; ctl00_ContentPlaceHolder1_rfv_Password_Lib.errormessage = "请输入密码"; ctl00_ContentPlaceHolder1_rfv_Password_Lib.display = "Dynamic"; ctl00_ContentPlaceHolder1_rfv_Password_Lib.evaluationfunction = "RequiredFieldValidatorEvaluateIsValid"; ctl00_ContentPlaceHolder1_rfv_Password_Lib.initialvalue = ""; //]]> </script> <script type="text/javascript"> //<![CDATA[ var Page_ValidationActive = false; if (typeof(ValidatorOnLoad) == "function") { ValidatorOnLoad(); } function ValidatorOnSubmit() { if (Page_ValidationActive) { return ValidatorCommonOnSubmit(); } else { return true; } } //]]> </script> </form> </body>
最主要的是<form></form>里面的内容,也就是咱们要提交的表单。要实现登陆,就要将表单提交上去,固然,在这以前,咱们先要填充该表单。java
首先,咱们须要提取出须要填充的内容。直接看源代码,咱们已经知道须要填充什么数据了,但注意到咱们要提交的表单须要填充__VIEWSTATE和__EVENTVALIDATION,而它们的表单值每次都不同,因此咱们须要提取出来,而不是本身填充。程序员
ViewState是ASP.NET用来保存控件状态信息的。在ASP时代,一个HTML控件的值,好比input控件值会在咱们把表单提交到服务器,页面再刷新回来的时候自动清空掉,这是由于web的无状态致使的,因此服务端每次把HTML输出到客户端后就再也不与客户端有任何联系。web
可是ASP.NET解决了这个问题:当咱们在写一个ASP.NET表单的时候,若是标明"form runat = server",那么ASP.NET就会自动在输出时给页面添加一个隐藏域:正则表达式
<input type = "hidden" name = "_VIEWSTATE" value = "">
有了这个隐藏域,页面里其余全部控件的状态,包括页面自己的一些状态都会保存到这个控件值里,这样每次页面提交时就会一块儿提交到后台,而后由ASP.NET对其中的值进行解码,最后输出时再根据这个值来恢复各个控件的状态。
在上面获得的网页源码中,我么能够看到,_VIEWSTATE的值很是复杂,根本没法阅读,就像是加密后的值同样,但实际上并无任何加密,仅仅是由于各个控件和页面的状态都存入适当的对象里面,而后把该对象序列化并进行一次Base64编码,最后直接赋值给viewstate控件。所谓的Base64就是一种基于64个可打印字符来表示二进制数据的表示方法,一般用于处理像是MIME的Email或者XML中存储的一些复杂数据,在HTTP环境中专门用来传递较长的标识信息,用做HTTP表单和HTTP GET URL中的参数。浏览器
固然,咱们会想到:为何不用Session来保存这些数据呢?由于Session值是保存在服务器内存上的,若是大量使用Session将致使服务器负担加剧,而ViewState只是将数据存入到页面隐藏控件里,不会占用服务器资源。因此,咱们能够将一些须要服务器保存的变量和对象保存到ViewState里,而Session只应该用在须要跨页面而且与每一个访问用户相关的变量和对象存储上。最重要的一点,Session默认状况下20分钟就过时了,但ViewState永远不会过时。安全
ViewState并不能存储全部的.NET数据类型,它仅仅支持String , Integer, Boolean, Array, ArrayList, HashTable,以及一些自定义的类型。服务器
虽然使用ViewState对服务器内存是友好的,但使用ViewState会增长页面HTML的输出量,像是上面那么一大串的字符串,这样带宽就会被占用得更多。并且ViewState是不安全的,由于全部的ViewState都存储在同一个地方,咱们彻底能够经过查看源码来找到这个值,只要通过转换就能够获取其中的对象和变量值。
为了解决这个问题,ASP.NET提供了两种方案:防篡改和加密。这些措施是题外话了,你们有兴趣的能够本身研究下。
__EVENTVALIDATION是ASP.NET2.0新增长的特性(很遗憾,咱们学校图书馆使用的就是ASP.NET 2.0,已经断代了好久了,但这种公共管理网站是不会更新的,除非哪天它出现了严重问题),它用于对PostBack的值进行验证,确保其是合法的值。他的工做原理是这样的:在页面render的时候,ASP.NET会对控件全部可能的值以及控件的UniqueID进行Hash计算,获得一个值,全部须要Render的控件都会有一个这样的计算值,而且组成一个列表,而后放在隐藏字段__EVENTVALIDATION中,页面Render后,就会对该字段的额内容进行解包,从新计算并对比Hash值是否一致。这样是为了防范一些模拟的post攻击,但问题也就来了:若是页面很是大或者网速加载较慢,用户在没有加载到该字段的时候就提交,会发数据不完整,那么就会出错。
经过上面的内容,咱们已经明白:要使表单提交成功,咱们就要获得这两个值。
private List<string> GetElementContent(string url, string elementName, string subElement) { HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); HttpWebResponse response = (HttpWebResponse)request.GetResponse(); Stream responseStream = response.GetResponseStream(); StreamReader reader = new StreamReader(responseStream, Encoding.UTF8); string content = reader.ReadToEnd(); reader.Close(); responseStream.Close(); StringBuilder regexStr = new StringBuilder("(?is)<"); regexStr.Append(elementName).Append("[^>]*?").Append(subElement).Append(@"=(['""\s]?)([^'""\s]+)\1[^>]*?>"); Regex regex = new Regex(regexStr.ToString()); MatchCollection match = regex.Matches(content); List<string> values = new List<string>(); foreach (Match m in match) { values.Add(m.Groups[2].Value); } return values; }
该方法就是利用正则表达式对HTML页面中的节点内容进行提取。首先,咱们得先获得该页面,因而须要经过HttpWebRequest发出请求,而后由HttpWebResponse获得响应该请求后返回的页面,接着就是利用正则表达式提取了。
若是有心想搞Web的同窗,正则表达式必定要懂得写。若是对正则表达式的使用驾轻就熟的话,在实际编码中就会很是方便。
咱们来看看这个方法是如何使用的:
List<string> values = GetElementContent(url, "input", "value"); List<string> names = GetElementContent(url, "input", "name");
这样咱们就获得了表单中全部要提交的元素的name和value了。
接着就是自动填充表单了:
Dictionary<string, string> postPair = new Dictionary<string, string>(); for (int i = 0, len = names.Count(); i < len; i++) { postPair.Add(names.ElementAtOrDefault(i), values.ElementAtOrDefault(i)); }
利用一个字典,将对应的name和value放进去。固然,要想登陆,咱们仍是得须要用户名和密码:
SetParams("ctl00$ContentPlaceHolder1$txtUsername_Lib", "***", postPair); SetParams("ctl00$ContentPlaceHolder1$txtPas_Lib", "***", postPair);
SetParams()方法的实现以下:
private void SetParams(string element, string content, Dictionary<string, string> dictionary) { for (int i = 0, len = dictionary.Keys.Count(); i < len; i++) { string key = dictionary.Keys.ElementAtOrDefault(i); if (key == element) { dictionary[key] = content; } } }
这样的值咱们是不能提交上去的,还须要进行URL编码:
private void UrlEncodeParams(Dictionary<string, string> formParams) { for (int i = 0, len = formParams.Keys.Count(); i < len; i++) { string key = formParams.Keys.ElementAtOrDefault(i); formParams[key] = HttpUtility.UrlEncode(formParams[key], Encoding.GetEncoding("GBK")); } }
而后就是Post该表单了:
private string PostForm(string url, Dictionary<string, string> form) { CookieContainer cookies = new CookieContainer(); string postStr = ""; foreach (string key in form.Keys) { postStr += key + "=" + form[key] + "&"; } byte[] postData = Encoding.ASCII.GetBytes(postStr.Substring(0, postStr.Length - 1)); HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); request.Method = "POST"; request.AllowAutoRedirect = false; request.ContentType = "application/x-www-form-urlencoded;charset=gbk"; request.CookieContainer = new CookieContainer(); request.UserAgent = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.95 Safari/537.11"; request.ContentLength = postData.Length; Stream requestStream = request.GetRequestStream(); requestStream.Write(postData, 0, postData.Length); requestStream.Close(); HttpWebResponse response = (HttpWebResponse)request.GetResponse(); Stream responseStream = response.GetResponseStream(); StreamReader reader = new StreamReader(responseStream); string cookie = response.Headers.Get("Set-Cookie"); string resultPage = reader.ReadToEnd(); string html = getHtml(GetCookieName(cookie), GetCookieValue(cookie)); reader.Close(); responseStream.Close(); return html; }
该方法才是咱们的重点!
咱们要先定义一个CookieContainer用来存储获得的Cookie。提交的表单字符串的形式应该是这样:name1=value1&name2=value2&...。一样,提交上去的时候,咱们也是须要对该字符串进行编码。
重点在于接下来的HttpWebRequest和HttpWebResponse的使用。
首先,咱们的HttpRequest应该是post方式,能够经过request.Method进行设置。request.AllowAutoRedirect=false是为了防止重定向,不少网站都是登陆后就立刻进行重定向,这样咱们就没法获得登陆时的Cookie,因而须要禁止该重定向。而后就是request.ContentType = "application/x-www-form-urlencoded;charset=gbk"。Content-Type就是所谓的内容类型,它是HTTP请求的头部信息,用于指定消息的类型,默认是text/plain,即纯文本,这里是application/x-www-form-urlencode,即指定是使用HTTP的POST方式提交表单。而后咱们再指定请求的CookieContainer为刚才定义的CookieContainer。而后就是指定代理:request.UserAgent = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.95 Safari/537.11"。用户代理字符串用于标识请求的浏览器类型和版本,操做系统,使用语言等各类信息,主要就是用于抓包。采用POST方式提交,都须要指定内容长度:request.ContentLength = postData.Length。
接着咱们再向请求流中写入数据:
Stream requestStream = request.GetRequestStream(); requestStream.Write(postData, 0, postData.Length); requestStream.Close();
获得响应后,咱们就能够获得响应中的Cookie:
string cookie = response.Headers.Get("Set-Cookie");
一般在响应的头信息里就包含了Cookie,它就是Set-Cookie的值。若是想要知道HTTP的头信息有哪些,能够在谷歌浏览器中按F12进行查看,但记得,要想捕捉这些信息,必须在未登陆前就按F12,不然谷歌浏览器是不会跟踪这些信息的。
而后咱们就能够利用该Cookie登陆网站而且获得登陆后的网站内容:
string html = getHtml(GetCookieName(cookie), GetCookieValue(cookie)); private string GetCookieValue(string cookie) { Regex regex = new Regex("=.*?;"); Match value = regex.Match(cookie); string cookieValue = value.Groups[0].Value; return cookieValue.Substring(1, cookieValue.Length - 2); } private string GetCookieName(string cookie) { Regex regex = new Regex("sulcmiswebpac.*?"); Match value = regex.Match(cookie); return value.Groups[0].Value; } private string getHtml(string name, string value) { CookieCollection cookies = new CookieCollection(); cookies.Add(new Cookie(name, value)); HttpWebRequest request = (HttpWebRequest)WebRequest.Create("http://222.200.98.171:81/user/bookborrowed.aspx"); request.Method = "GET"; request.Headers.Add("Cookie", name + "=" + value); HttpWebResponse response = (HttpWebResponse)request.GetResponse(); Stream stream = response.GetResponseStream(); StreamReader reader = new StreamReader(stream, Encoding.UTF8); return reader.ReadToEnd(); }
咱们以前获得的cookie字符串中其实就是一个键值对字符串,咱们仍是须要根据正则表达式提取出cookie的name和value,而后咱们定义一个CookieContainer,往里面添加咱们捕捉到的Cookie,接着就是须要Cookie的网页发送一个带有该Cookie的HTTP请求。
方法很简单,只要合理的使用正则表达式,咱们就能够方便的对网页进行处理,而不须要什么第三方的库。
每一个程序员都须要学会本身造轮子而不是一味的追求轮子,就算是其余语言的轮子,咱们依然能够用本身熟悉的语言实现出来,毕竟全部的语言背后的实现思想都是同样的,尤为是面向对象语言,它们都是相互借鉴的,交叉处实在是太多了,C#更是在参考java的基础上创造出来的,有什么理由是java能够C#不能够呢?(有是有,但咱们能够模拟)