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啊,盡管我早說了不要這錢了。

  我就是想說明下,不論單子大小,不論生人熟人(我接這活的人是一個老鄉),一定先見到錢,其他的都是扯淡,生意上,有錢老子跟你混,沒錢少跟老子扯淡。更不要跟我談什么親戚朋友或者是狗屁老鄉,我跟錢才是老鄉。

  其次是需求文檔,無文檔不開發,每次業務變更需求變動,白紙黑字,明明白白的寫清楚,咱該加價加價,該加工期加工期。

 

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM