WPF資料爬取小工具-某寶推廣位批量生成,及訂單爬取 記:接單最痛一次的感悟
專案由來:上月閒來無事接到接到一個單子,自動登入X寶平臺,然後重定向到指定頁面批量生成推廣位資訊;與此同時自動定時同步訂單資料到需求提供方的Java服務。
當然期間遇到一個小小的問題就是介面樣式的問題,起初使用的winform開發,但是樣式,你懂的,所以後來索性直接使用wpf.
先宣告:這裡只做經驗分享,不提供其他支援,畢竟,,,不安全。
1.首先看下我們的專案介面
說明:三張圖分別是登入,登入後主頁面,和訂單頁面
(登入頁面)介面整體就劃分上中下尾四個部分,種下部分的灰色是一個webBrowser.可以很好地幫助我們解決重定向之後,通過重定向頁面獲取cookie,這個後面回說。
當然如果你覺得這個灰色很突兀,你可設定高寬為0,那麼介面將會很簡潔。我之所以顯示出來是因為初次訪問該網站的時候,會出現驗證的問題,需要手動點選以及拖拽拼圖。
(主頁面)依舊是頭部上部中部下部尾部,
(訂單頁面)很明瞭。
介面外掛:MetroWindow,請自行百度,謝謝。
2.主要邏輯
2.1.主頁面內容
首先我們分析下,一般情況下,我們在登入某平臺時候,如果使用第三方授權登陸之後,地址中會有一個redirectUrl,即授權成功之後從定向的頁面,那麼此時我們要獲取的cokkie肯定是從重定向之後的頁面獲取
所以,這裡也是一樣的,我們這裡的登入實現也是通過一個帶有redirectUrl的登陸地址模擬post。
首先,我們在窗體初始化的時候,在webBrowser中初始化我們的登入頁面,也就是 灰色部分。然後通過webbrowser獲取相關dom元素,賦值,模擬登陸按鈕的提交事件,程式碼如下
webBrowser程式碼:
在窗體的load事件中初始化,其中的 LoginUrl 就是我們的 帶有重定向地址的 登入地址;eg:https://login.xxbao.com/login?redirectURL=www.baidu.com
webBrowser.Navigate(LoginUrl);
當然如此還沒完,如果瞭解webBrowser的人肯定都知道,這個東西有一個常用的事件就是 LoadCompleted,每當頁面載入完成或者重定向完成之後,都會執行,所以,繼續在load中新增如下的程式碼,將 LoadCompleted 事件先設定了,
webBrowser.LoadCompleted += (wbSender, wbArgs) => { if (_loginViewModel != null && !string.IsNullOrWhiteSpace(_loginViewModel.LoginAccount)) { if (wbArgs.Uri.ToString().Contains("登入成功之後,跳轉到的回撥的網站的主頁面地址")) { Log4netHelper.WriteLog(Log4netHelper.LogType.Info, "正在獲取cookie..."); // TODO 獲取cookie操作 try { _loginViewModel.StrCookies = CookieHelper.GetCookies(_loginViewModel.WebBrowser.Source.ToString()); _loginViewModel._tb_token_ = CookieHelper.GetFiled(_loginViewModel.StrCookies, "_tb_token_"); Log4netHelper.WriteLog(Log4netHelper.LogType.Info, "獲取到TOKEN\n" + "\t" + _loginViewModel._tb_token_ + ""); } catch (Exception ex) { ........錯誤記錄省略 } this.WriteLog("cookie成功獲取,即將跳轉到主頁面..."); this.GoTuiGuangWei(); } } }; }
簡要說明下:其中紅色部分為登入成功之後重新定向的網站的主頁面,之所以在這裡判斷,上面有說了,loadCompleted會在webBrowser每次頁面載入完成的時候都會被執行,所以在這裡我們判斷當前載入的頁面是否是我們想要從中獲取
cookie的網站的頁面,如果是,那麼我們執行cookie的獲取操作
這裡涉及到一個 重點問題就是cookie獲取的問題,這裡需要注意,下面提供的方法可以正常獲取,其他方式 自行斟酌是否可行。
public class CookieHelper { [DllImport("wininet.dll", CharSet = CharSet.Auto, SetLastError = true)] static extern bool InternetGetCookieEx(string pchURL, string pchCookieName, StringBuilder pchCookieData, ref System.UInt32 pcchCookieData, int dwFlags, IntPtr lpReserved); [DllImport("wininet.dll", CharSet = CharSet.Auto, SetLastError = true)] static extern int InternetSetCookieEx(string lpszURL, string lpszCookieName, string lpszCookieData, int dwFlags, IntPtr dwReserved); private static string GetCookieString(string url) { // Determine the size of the cookie uint datasize = 256; StringBuilder cookieData = new StringBuilder((int)datasize); if (!InternetGetCookieEx(url, null, cookieData, ref datasize, 0x2000, IntPtr.Zero)) { if (datasize < 0) return null; // Allocate stringbuilder large enough to hold the cookie cookieData = new StringBuilder((int)datasize); if (!InternetGetCookieEx(url, null, cookieData, ref datasize, 0x00002000, IntPtr.Zero)) return null; } return cookieData.ToString(); } public static string GetCookies(string requestUrl) { return GetCookieString(requestUrl); } /// <summary> ///從cookie中獲取指定鍵名稱的對應的值 /// </summary> /// <param name="cookies"></param> /// <param name="fieldName"></param> /// <returns></returns> public static string GetFiled(string cookies, string fieldName) { var cookieArray = cookies.Split(';'); foreach (var cookieStr in cookieArray) { if (cookieStr.Contains(fieldName)) { return cookieStr.Split('=')[1]; } } return string.Empty; } }
涉及到的登入程式碼:
/// <summary> ///登入引數物件擴充套件方法 /// </summary> public static class WebBrowserExtensions { /// <summary> ///登陸擴充套件 /// </summary> /// <param name="loginViewModel"></param> public static void LoginEx(this LoginViewModel loginViewModel) { var webBrowser = loginViewModel.WebBrowser; #region 驗證操作(登陸一次之後就不存在了,但是這裡個 domID不確定是不是這個) IHTMLDocument2 doc = (IHTMLDocument2)webBrowser.Document; try { IHTMLElement jsLoginCheck = doc.all.item("J_SafeLoginCheck", 0);//id或者是name jsLoginCheck.click(); Thread.Sleep(1000); } catch (Exception) { } #endregion //賬號dom ID IHTMLElement elementAccount = doc.all.item("TPL_username_1", 0); //密碼dom ID IHTMLElement elementPassword = doc.all.item("TPL_password_1", 0); //賦值操作 elementAccount.setAttribute("value", loginViewModel.LoginAccount);//繫結值 elementPassword.setAttribute("value", loginViewModel.LoginPassword); Thread.Sleep(100); IHTMLElement buttonSubmit = doc.all.item("J_SubmitStatic", 0); buttonSubmit.click(); } }
這裡需要注意的是wpf的ewbBrowser和winform的稍有不同,獲取dom的方式,通過 IHTMLDocument2 doc = (IHTMLDocument2)webBrowser.Document;,需要引用名稱空間:using mshtml;
2.2.主頁面:
後臺程式碼中定義了兩個計時器 (System.Timers.Timer),這裡是 Timers下的timer不是Threading下的,注意下。其他細節不便提供出來,如果感興趣的我可以把原始碼改過後開放出來,
這裡只說下控制元件資料繫結的問題,當多個執行緒同時操作某個控制元件時,雖說wpf的控制元件和winform有很大不同,但是一樣的,存在子控制元件執行緒處理不當主執行緒(主介面)依舊會卡住的問題,
所以我們可像下面這樣解決:
比如我們有一個label控制元件,定時重新整理繫結 文字資訊
this.label.Invoke(new Action(()=>{
this.Lable.Context ="我是好人";
}));
如此,是沒有問題,但是看下面的寫法,猜猜是否有問題呢?
this.label.Invoke(new Action(()=>{
//其他的請求操作,假設返回結果為 reuslt
this.Lable.Context =result;
}));
不用懷疑,主執行緒會卡住,最明顯的示例就是使用wpf的 RitchTextBox,如下程式碼,會存在很嚴重的 問題:
private void WriteLog(string message){
this.RitchTextBox.Dispatcher.BeginInvoke(new Action(() =>
{
p = new Paragraph();
Run r = new Run(message.ToString() + "\n");
p.TextAlignment = TextAlignment.Left;
p.Inlines.Add(r);
RitchTextBox.Document.Blocks.Add(p);
RitchTextBox.ScrollToEnd();
}));
}
假如反覆的執行該方法,去給 RitchTextBox 追加內容,介面會卡到你想把自己的蛋蛋捏碎,然而,成敗就在細節之間,下面的方式 是毫無問題的,
private void WriteLog(string message)
{
p = new Paragraph();
Run r = new Run(message.ToString() + "\n");
p.TextAlignment = TextAlignment.Left;
p.Inlines.Add(r);
this.RitchTextBox.Dispatcher.BeginInvoke(new Action(() =>
{
RitchTextBox.Document.Blocks.Add(p);
RitchTextBox.ScrollToEnd();
}));
}
當然更進一步的優化一下可以像下面的方式:
void WriteLog(object message, bool isError = false) { this.Dispatcher.BeginInvoke(new Action(() => { Paragraph p; if (isError) { SolidColorBrush solidColorBrush = new SolidColorBrush(Color.FromRgb(255, 0, 0)); p = new Paragraph() { Foreground = solidColorBrush }; Run r = new Run(message.ToString() + "\n"); p.TextAlignment = TextAlignment.Left; p.Inlines.Add(r); } else { p = new Paragraph(); Run r = new Run(message.ToString() + "\n"); p.TextAlignment = TextAlignment.Left; p.Inlines.Add(r); } this.rtbLog.Dispatcher.BeginInvoke(new Action(() => { rtbLog.Document.Blocks.Add(p); rtbLog.ScrollToEnd(); })); })); }
其他:專案還使用了一個ini的檔案的作為配置檔案,相關實現類如下:
/// <summary> /// ini檔案操作類 /// </summary> public class IniHelper { #region 動態連結庫呼叫 /// <summary> /// 呼叫動態連結庫讀取值 /// </summary> /// <param name="lpAppName">ini節名</param> /// <param name="lpKeyName">ini鍵名</param> /// <param name="lpDefault">預設值:當無對應鍵值,則返回該值。</param> /// <param name="lpReturnedString">結果緩衝區</param> /// <param name="nSize">結果緩衝區大小</param> /// <param name="lpFileName">ini檔案位置</param> /// <returns></returns> [DllImport("kernel32")] private static extern int GetPrivateProfileString( string lpAppName, string lpKeyName, string lpDefault, StringBuilder lpReturnedString, int nSize, string lpFileName); /// <summary> /// 呼叫動態連結庫寫入值 /// </summary> /// <param name="mpAppName">ini節名</param> /// <param name="mpKeyName">ini鍵名</param> /// <param name="mpDefault">寫入值</param> /// <param name="mpFileName">檔案位置</param> /// <returns>0:寫入失敗 1:寫入成功</returns> [DllImport("kernel32")] private static extern long WritePrivateProfileString( string mpAppName, string mpKeyName, string mpDefault, string mpFileName); #endregion /// <summary> /// 讀ini檔案 /// </summary> /// <param name="section">節</param> /// <param name="key">鍵</param> /// <returns>返回讀取值</returns> public static string Ini_Read(string section, string key, string path) { StringBuilder stringBuilder = new StringBuilder(1024);//定義一個最大長度為1024的可變字串 GetPrivateProfileString(section, key, "", stringBuilder, 1024, path);//讀取INI檔案 return stringBuilder.ToString();//返回INI檔案的內容 } /// <summary> /// 寫ini檔案 /// </summary> /// <param name="section">節</param> /// <param name="key">鍵</param> /// <param name="iValue">待寫入值</param> public static void Ini_Write(string section, string key, string iValue, string path) { WritePrivateProfileString(section, key, iValue, path);//寫入 } /// <summary> /// 根據檔名建立檔案 /// </summary> /// <param name="path">檔名稱以及路徑</param> public static void ini_creat(string path) { if (!File.Exists(path))//判斷是否存在相關檔案 { FileStream _fs = File.Create(path);//不存在則建立ini檔案 _fs.Close();//關閉檔案,解除佔用 } } /// <summary> /// 刪除ini檔案中鍵 /// </summary> /// <param name="section">節名稱</param> /// <param name="key">鍵名稱</param> /// <param name="path">ini檔案路徑</param> public static void Ini_Del_Key(string section, string key, string path) { WritePrivateProfileString(section, key, null, path);//寫入 } /// <summary> /// 刪除ini檔案中節 /// </summary> /// <param name="section">節名</param> /// <param name="path">ini檔案路徑</param> public static void Ini_Del_Section(string section, string path) { WritePrivateProfileString(section, null, null, path);//寫入 } /// <summary> ///指定的ini檔案是否存在,不存在就建立 /// </summary> /// <param name="iniFileName">檔名(非路徑,只是名稱)</param> /// <returns></returns> public static void Ini_Init(string iniFileName = "app.ini") { var filePath = IniFilePath(iniFileName); if (!File.Exists(filePath)) { //初始化基礎資訊 IniHelper.ini_creat(filePath); IniHelper.Ini_Write("INFO", "Preffix", "GWJ-", filePath); IniHelper.Ini_Write("INFO", "DaoGouID", "", filePath); IniHelper.Ini_Write("INFO", "Count", "0", filePath); IniHelper.Ini_Write("INFO", "TAG", "29", filePath); IniHelper.Ini_Write("INFO", "TimeRange", "8", filePath); //初始化 同步到Java 介面時候 爬取的分頁,每爬取一次 更新一次 IniHelper.Ini_Write("DELIVERY", "TO_PAGE", "1", filePath); IniHelper.Ini_Write("DELIVERY", "PER_PAGE_SIZE", "10", filePath); //IniHelper.Ini_Write("DELIVERY", "TIMES", "0", filePath);//剩餘請求次數 //初始化訂單引數, IniHelper.Ini_Write("ORDER", "SIZE", "2000", filePath); //初始化 伺服器地址配置 var tempUrl = "http://xxxxx"; var tgwAddress = IniHelper.Ini_Read("SERVER", "TGWAddress", filePath); if (tgwAddress != tempUrl) { if (string.IsNullOrWhiteSpace(tgwAddress)) IniHelper.Ini_Write("SERVER", "TGWAddress", tempUrl, filePath);//推廣位 else IniHelper.Ini_Write("SERVER", "TGWAddress", tgwAddress, filePath);//推廣位 } var qq = "21321321"; var initQQ = IniHelper.Ini_Read("QQ", "QQ", filePath); if (qq != initQQ) { if (string.IsNullOrWhiteSpace(initQQ)) IniHelper.Ini_Write("QQ", "QQ", qq, filePath);//客服QQ else IniHelper.Ini_Write("QQ", "QQ", initQQ, filePath);//客服QQ } } } public static string IniFilePath(string iniFileName = "app.ini") { var filePath = Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, iniFileName); return filePath; } }
http幫助類:(context-type哪裡可以合成一個方法
public class HttpRequestHelper { /// <summary> /// 預設的頭 /// </summary> public static string defaultHeaders = @"Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Encoding:gzip, deflate, sdch Accept-Language:zh-CN,zh;q=0.8 Cache-Control:no-cache Connection:keep-alive Pragma:no-cache Upgrade-Insecure-Requests:1 User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36"; public static string DoRequest(string alimamaUrl, string method, string postDataStr, Encoding encoding, string cookiesStr) { var html = string.Empty; HttpWebRequest request = (HttpWebRequest)WebRequest.Create(alimamaUrl); request.Method = method; request.AllowAutoRedirect = true; request.ContentType = "application/x-www-form-urlencoded"; request.KeepAlive = true; //request.CookieContainer.Add(cc); request.Headers[HttpRequestHeader.Cookie] = cookiesStr; if (method.ToUpper() == "POST") { byte[] data = Encoding.UTF8.GetBytes(postDataStr); request.ContentLength = data.Length; Stream requestStream = request.GetRequestStream(); requestStream.Write(data, 0, data.Length); requestStream.Close(); } try { HttpWebResponse httpResponse = (HttpWebResponse)request.GetResponse(); using (System.IO.Stream dataStream = httpResponse.GetResponseStream()) { using (System.IO.StreamReader sr = new System.IO.StreamReader(dataStream, Encoding.GetEncoding("utf-8"))) { html = sr.ReadToEnd(); sr.Close(); } } httpResponse.Close(); } catch (System.Net.WebException ex) { html = ex.Message; } return html; } public static string DoJsonRequest(string alimamaUrl, string method, string postDataStr, Encoding encoding, string strCookies = "") { var html = string.Empty; HttpWebRequest request = (HttpWebRequest)WebRequest.Create(alimamaUrl); request.Method = method; request.AllowAutoRedirect = true; request.ContentType = "application/json"; request.KeepAlive = true; //request.CookieContainer.Add(cc); if (!string.IsNullOrWhiteSpace(strCookies)) request.Headers[HttpRequestHeader.Cookie] = strCookies; if (method.ToUpper() == "POST") { byte[] data = Encoding.UTF8.GetBytes(postDataStr); request.ContentLength = data.Length; Stream requestStream = request.GetRequestStream(); requestStream.Write(data, 0, data.Length); requestStream.Close(); } try { HttpWebResponse httpResponse = (HttpWebResponse)request.GetResponse(); using (System.IO.Stream dataStream = httpResponse.GetResponseStream()) { using (System.IO.StreamReader sr = new System.IO.StreamReader(dataStream, Encoding.GetEncoding("utf-8"))) { html = sr.ReadToEnd(); sr.Close(); } } httpResponse.Close(); } catch (System.Net.WebException ex) { html = ex.Message; } return html; } }
3.注意點
1.wpf窗體間傳值問題;
2.cookie(cookieCollection)的傳遞問題,在上面獲取到cookie(是一個字串),不用做任何處理,在後面每次的請求(使用http的幫助類),都需要帶上,不需要轉換成cookiCollection,直接拼接到header中即可(http幫助類中有做判斷),
3.控制元件繫結繫結值問題,避免子執行緒阻塞主執行緒(介面)
原始碼稍後提供到git
4.感悟:多麼痛的領悟。
自從業以來,打過工、創過業(儘管失敗了),接過單,但是這次接的這破東西是最坑的一次,按以往的經驗和習慣,就這樣類似的東西,僅僅四五個功能的,基本3-4個小時不出意外的話,毫無疑問的就可以完成,其中會有1-2小時的測試和資料分析。
然而,就這三個介面的東西,前前後後花了我三四天時間,每天都要花在上面3小時左右,平心而論,兒了這千把塊的東西珍惜不值得,第二天時候我已經和需求方提出了,你們重新找人吧,需求一再的變動,前前後後不下4次,價格卻一成不變,咱拜拜吧。但是一再的求我,
“兄弟,幫個忙吧,好處會有的,,,,”,完全是一堆廢話,後又趕上訂親等一些事宜是真的很忙,這人竟然和我火了,大發雷霆,發簡訊打電話威脅我,我說你這麼囂張威脅我?回:對,我就是威脅你。。。。如此之囂張,無奈之舉,,找了幾個兄弟,
同時報了警,做兩手準備,錢可以不要,尊嚴一定要有,不能讓技術顯得這麼廉價。,,,然而我想多了,事後第二天這人又打我電話表態與我和好,說自己是個傻子,讓我把它當成個傻子,求我一定要把他這個東西做好,,,,
拋開其他不說,格局已經定了,這就是格局,小的很,雖然這人和其他兩人創業的,但是格局決定了這個人以後的道路。
最後當然是把東西做完了,也就是上面的截圖,交給了他,這人硬塞給了我1k,,,,1k,,,1k啊,儘管我早說了不要這錢了。
我就是想說明下,不論單子大小,不論生人熟人(我接這活的人是一個老鄉),一定先見到錢,其他的都是扯淡,生意上,有錢老子跟你混,沒錢少跟老子扯淡。更不要跟我談什麼親戚朋友或者是狗屁老鄉,我跟錢才是老鄉。
其次是需求文件,無文件不開發,每次業務變更需求變動,白紙黑字,明明白白的寫清楚,咱該加價加價,該加工期加工期。