之前看過某個同學的一篇有關與使用JSoup解析學校圖書館的文章,仔細一看,發現竟然是同校!!既然對方用的是java,那么我也就來個C#好了,雖然我的入門語言是java。
C#沒有JSoup這樣方便的東西,我也沒有仔細去找,因為只要利用正則表達式,同樣可以很好的解析網頁內容而不需要其他幫助。現在做前端的程序員,如果正則表達式不熟悉,反而去依賴第三方的話,感覺很可惜!
這是我們學校圖書館的登錄界面的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>里面的內容,也就是我們要提交的表單。要實現登錄,就要將表單提交上去,當然,在這之前,我們先要填充該表單。
首先,我們需要提取出需要填充的內容。直接看源代碼,我們已經知道需要填充什么數據了,但注意到我們要提交的表單需要填充__VIEWSTATE和__EVENTVALIDATION,而它們的表單值每次都不一樣,所以我們需要提取出來,而不是自己填充。
ViewState是ASP.NET用來保存控件狀態信息的。在ASP時代,一個HTML控件的值,比如input控件值會在我們把表單提交到服務器,頁面再刷新回來的時候自動清空掉,這是因為web的無狀態導致的,所以服務端每次把HTML輸出到客戶端后就不再與客戶端有任何聯系。
但是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#不可以呢?(有是有,但我們可以模擬)