基於搜狗搜索的微信公眾號爬蟲實現(C#版本)


Author: Hoyho Luo

Email: luohaihao@gmail.com

Source Url:https://here2say.tw/11/

轉載請保留此出處

 

   本文介紹基於搜狗的微信公眾號定向爬蟲,使用C#實現,故取名WeGouSharp。本文中的項目托管在Github上,你可以戳WeGouSharp獲取源碼,歡迎點星。關於微信公共號爬蟲的項目網上已經不少,然而基本大多數的都是使用Python實現 鑒於鄙人是名.NET開發人員,於是又為廣大微軟系同胞創建了這個輪子,使用C#實現的微信爬蟲 藍本為Chyroc/WechatSogou, 在此還請各位大佬指教。

 

目錄
1.項目結構
2.數據結構
3.xpath介紹 
4.使用HtmlAgilityPack解析網頁內容
5.驗證碼處理以及文件緩存
 
 
 
 

一、 項目結構

如下圖

API類:

所有直接的操作封裝好在API類中,直接使用里面的方法

Basic類:
主要處理邏輯
 
FileCache:
主要出現驗證碼的時候需要使用Ccokie驗證身份,此類可以加密后序列化保存UIN,BIZ,COOKIE等內容以供后續使用
 
HttpHelper類:
網絡請求,包括圖片
 
Tools類:
圖片處理,cookie加載等

 

依賴包可以直接使用package文件夾的版本
也可以自行在NuGet添加 如(visual studio-->tools-->Nuget Package Manager-->Package Manager Console):
Install-Package HtmlAgilityPack

  

 

二、 數據結構

本項目根據微信公賬號以及搜狗搜索定義了多個結構,可以查看模型類,主要包括以下:

公眾號結構:

public struct OfficialAccount
    {

        public string AccountPageurl;
        public string WeChatId;
        public string Name;
        public string Introduction;
        public bool IsAuth; 
        public string QrCode;
        public string ProfilePicture;//public string RecentArticleUrl;
    } 

字段含義

字段 含義
AccountPageurl 微信公眾號頁
WeChatId 公號ID(唯一)
Name 名稱
Introduction 介紹
IsAuth 是否官方認證
QrCode 二維碼鏈接
ProfilePicture 頭像鏈接

 

 

公號群發消息結構(含圖文推送)

public struct BatchMessage
    {
        public int Meaasgeid;
        public string  SendDate;
        public string Type; //49:圖文,1:文字,3:圖片,34:音頻,62:視頻public string Content; 

        public string ImageUrl; 

        public string PlayLength;
        public int FileId;
        public string AudioSrc;

        //for type 圖文public string ContentUrl;
        public int Main;
        public string Title;
        public string Digest;
        public string SourceUrl;
        public string Cover;
        public string Author;
        public string CopyrightStat;

        //for type 視頻public string CdnVideoId;
        public string Thumb;
        public string VideoSrc;

        //others
    }

 

字段含義

字段 含義
Meaasgeid 消息號
SendDate 發出時間(unix時間戳)
Type 消息類型:49:圖文, 1:文字, 3:圖片, 34:音頻, 62:視頻
Content 文本內容(針對類型1即文字)
ImageUrl 圖片(針對類型3,即圖片)
PlayLength 播放長度(針對類型34,即音頻,下同)
FileId 音頻文件id
AudioSrc 音頻源
ContentUrl 文章來源(針對類型49,即圖文,下同)
Main 不明確
Title 文章標題
Digest 不明確
SourceUrl 可能是閱讀原文
Cover 封面圖
Author 作者
CopyrightStat 可能是否原創?
CdnVideoId 視頻id(針對類型62,即視頻,下同)
Thumb 視頻縮略圖
VideoSrc 視頻鏈接

 

 

文章結構

  public struct Article
    {
        public string Url;
        public List<string>Imgs;
        public string Title;
        public string Brief;
        public string Time;
        public string ArticleListUrl;
        public OfficialAccount officialAccount;
    }

 

字段含義
字段 含義
Url 文章鏈接
Imgs 封面圖(可能多個)
Title 文章標題
Brief 簡介
Time 發表日期(unix時間戳)
OfficialAccount 關聯的公眾號(信息不全,僅供參考)
 
         

 

搜索榜結構

public struct HotWord
    {
        public int Rank;//排行
        public string Word;
        public string JumpLink; //相關鏈接
        public int HotDegree; //熱度
    }

 

 

 

三 、xpath介紹

 什么是 XPath?

  • XPath 使用路徑表達式在 XML 文檔中進行導航
  • XPath 包含一個標准函數庫
  • XPath 是 XSLT 中的主要元素
  • XPath 是一個 W3C 標准

簡而言之,Xpath是XML元素的位置,下面是W3C教程時間,老鳥直接跳過

 

XML 實例文檔

我們將在下面的例子中使用這個 XML 文檔。

<?xml version="1.0" encoding="ISO-8859-1"?>

<bookstore>

<book>
  <title lang="eng">Harry Potter</title>
  <price>29.99</price>
</book>

<book>
  <title lang="eng">Learning XML</title>
  <price>39.95</price>
</book>

</bookstore>
 

 

選取節點

XPath 使用路徑表達式在 XML 文檔中選取節點。節點是通過沿着路徑或者 step 來選取的。

下面列出了最有用的路徑表達式:

表達式 描述
nodename 選取此節點的所有子節點。
/ 從根節點選取。
// 從匹配選擇的當前節點選擇文檔中的節點,而不考慮它們的位置。
. 選取當前節點。
.. 選取當前節點的父節點。
@ 選取屬性。

實例

在下面的表格中,我們已列出了一些路徑表達式以及表達式的結果:

路徑表達式 結果
bookstore 選取 bookstore 元素的所有子節點。
/bookstore

選取根元素 bookstore。

注釋:假如路徑起始於正斜杠( / ),則此路徑始終代表到某元素的絕對路徑!

bookstore/book 選取屬於 bookstore 的子元素的所有 book 元素。
//book 選取所有 book 子元素,而不管它們在文檔中的位置。
bookstore//book 選擇屬於 bookstore 元素的后代的所有 book 元素,而不管它們位於 bookstore 之下的什么位置。
//@lang 選取名為 lang 的所有屬性。

謂語(Predicates)

謂語用來查找某個特定的節點或者包含某個指定的值的節點。

謂語被嵌在方括號中。

實例

在下面的表格中,我們列出了帶有謂語的一些路徑表達式,以及表達式的結果:

路徑表達式 結果
/bookstore/book[1] 選取屬於 bookstore 子元素的第一個 book 元素。
/bookstore/book[last()] 選取屬於 bookstore 子元素的最后一個 book 元素。
/bookstore/book[last()-1] 選取屬於 bookstore 子元素的倒數第二個 book 元素。
/bookstore/book[position()<3] 選取最前面的兩個屬於 bookstore 元素的子元素的 book 元素。
//title[@lang] 選取所有擁有名為 lang 的屬性的 title 元素。
//title[@lang='eng'] 選取所有 title 元素,且這些元素擁有值為 eng 的 lang 屬性。
/bookstore/book[price>35.00] 選取 bookstore 元素的所有 book 元素,且其中的 price 元素的值須大於 35.00。
/bookstore/book[price>35.00]/title 選取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值須大於 35.00。

選取未知節點

XPath 通配符可用來選取未知的 XML 元素。

通配符 描述
* 匹配任何元素節點。
@* 匹配任何屬性節點。
node() 匹配任何類型的節點。

實例

在下面的表格中,我們列出了一些路徑表達式,以及這些表達式的結果:

路徑表達式 結果
/bookstore/* 選取 bookstore 元素的所有子元素。
//* 選取文檔中的所有元素。
//title[@*] 選取所有帶有屬性的 title 元素。

 

 

 

 

 

 

如圖,假設我要抓取首頁一個banner圖,可以在chrome按下F12參考該元素的Xpath,

 

即該圖片對應的Xpth為: //*[@id="loginWrap"]/div[4]/div[1]/div[1]/div/a[4]/img

解讀:該圖片位於ID= loginWrap下面的第4個div下的...的img標簽內

 

為什么這里介紹Xpath,是因為我們網頁分析是使用HtmlAgilityPack來解析, 他可以把根據Xpath解析我們所需的元素。
比如我們調試確定一個文章頁面的特定位置為標題,圖片,作者,內容,鏈接的Xpath即可完全批量化且准確地解析以上信息
 

 

四、 使用HtmlAgilityPack解析網頁內容

HttpTool類里封裝了一個較多參數的HTTP GET操作,用於獲取搜狗的頁面:

因為搜狗本身是做搜索引擎的緣故,所以反爬蟲是非常嚴厲的,因此HTTP GET的方法要注意攜帶很多參數,且不同頁面要求不一樣.一般地,要帶上默認的

referer和host 然后請求頭的UserAgent 要偽造,常用的useragent有

public static List<string> _agent = new List<string>
{
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; AcooBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)",
"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.35; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
"Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)",
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
"Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
"Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0",
"Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5",
"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.8) Gecko Fedora/1.9.0.8-1.fc10 Kazehakase/0.5.6",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.20 (KHTML, like Gecko) Chrome/19.0.1036.7 Safari/535.20",
"Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; fr) Presto/2.9.168 Version/11.52",
};

 

 

 

自定義的GET 方法

  /// <summary>
        /// 指定header參數的HTTP Get方法
        /// </summary>
        /// <param name="headers"></param>
        /// <param name="url"></param>
        /// <returns>respondse</returns>
        public string Get(WebHeaderCollection headers, string url ,string responseEncoding="UTF-8",bool isUseCookie = false)
        {
            string responseText = "";
            try
            {
                var request = (HttpWebRequest)WebRequest.Create(url);
                    request.Method = "GET";
                foreach (string key in headers.Keys)
                {
                    switch (key.ToLower())
                    {
                        case "user-agent":
                            request.UserAgent = headers[key];
                            break;
                        case "referer":
                            request.Referer = headers[key];
                            break;
                        case "host":
                            request.Host = headers[key];
                            break;
                        case "contenttype":
                            request.ContentType = headers[key];
                            break;
                        case "accept":
                            request.Accept = headers[key];
                            break;
                        default:
                            break;
                    }
               }
                if (string.IsNullOrEmpty(request.Referer))
                {
                    request.Referer = "http://weixin.sogou.com/";
                };
                if (string.IsNullOrEmpty(request.Host))
                {
                    request.Host = "weixin.sogou.com";
                };
                if (string.IsNullOrEmpty(request.UserAgent))
                {
                    Random r = new Random();
                    int index = r.Next(WechatSogouBasic._agent.Count - 1);
                    request.UserAgent = WechatSogouBasic._agent[index];
                }
                if (isUseCookie)
                {
                    CookieCollection cc = Tools.LoadCookieFromCache();
                    request.CookieContainer = new CookieContainer();
                    request.CookieContainer.Add(cc);
                }
                HttpWebResponse response = (HttpWebResponse)request.GetResponse();
                if (isUseCookie && response.Cookies.Count >0)
                {
                    var cookieCollection = response.Cookies;
                    WechatCache cache = new WechatCache(Config.CacheDir, 3000);
                    if (!cache.Add("cookieCollection", cookieCollection, 3000)) { cache.Update("cookieCollection", cookieCollection, 3000); };
                }
                // Get the stream containing content returned by the server.
                Stream dataStream = response.GetResponseStream();
                //如果response是圖片,則返回以base64方式返回圖片內容,否則返回html內容
                if (response.Headers.Get("Content-Type") == "image/jpeg" || response.Headers.Get("Content-Type") == "image/jpg")
                {
                    Image img = Image.FromStream(dataStream, true);
                    using (MemoryStream ms = new MemoryStream())
                    {
                        // Convert Image to byte[]
                        //img.Save("myfile.jpg");
                        img.Save(ms,System.Drawing.Imaging.ImageFormat.Jpeg);
                        byte[] imageBytes = ms.ToArray();
                        // Convert byte[] to Base64 String
                        string base64String = Convert.ToBase64String(imageBytes);
                        responseText = base64String;
                    }
                }
                else //read response string
                {
                    // Open the stream using a StreamReader for easy access.
                    Encoding encoding;
                    switch (responseEncoding.ToLower())
                    {
                        case "utf-8":
                            encoding = Encoding.UTF8;
                            break;
                        case "unicode":
                            encoding = Encoding.Unicode;
                            break;
                        case "ascii":
                            encoding = Encoding.ASCII;
                            break;
                        default:
                            encoding = Encoding.Default;
                            break;
                               
                    }
                    StreamReader reader = new StreamReader(dataStream, encoding);//System.Text.Encoding.Default
                    // Read the content.
                    if (response.StatusCode == HttpStatusCode.OK)
                    {
                        responseText = reader.ReadToEnd();
                        if (responseText.Contains("用戶您好,您的訪問過於頻繁,為確認本次訪問為正常用戶行為,需要您協助驗證"))
                        {
                            _vcode_url = url;
                            throw new Exception("weixin.sogou.com verification code");
                        }
                    }
                    else
                    {
                        logger.Error("requests status_code error" + response.StatusCode);
                        throw new Exception("requests status_code error");
                    }
                    reader.Close();
                }

                dataStream.Close();
                response.Close();
            }
            catch (Exception e)
            {
                logger.Error(e);
            }
            return responseText;
        }

 

 

前面關於Xpath廢話太多,直接上一個案例,解析公眾號頁面:

public List<OfficialAccount> SearchOfficialAccount(string keyword, int page = 1)
        {
            List<OfficialAccount> accountList = new List<OfficialAccount>();
            string text = this._SearchAccount_Html(keyword, page);//返回了一個搜索頁面的html代碼
            HtmlDocument pageDoc = new HtmlDocument();
            pageDoc.LoadHtml(text);
            HtmlNodeCollection targetArea = pageDoc.DocumentNode.SelectNodes("//ul[@class='news-list2']/li");
            if (targetArea != null)
            {
                foreach (HtmlNode node in targetArea)
                {
                    try
                    {
                        OfficialAccount accountInfo = new OfficialAccount();
                        //鏈接中包含了&amp; html編碼符,要用htmdecode,不是urldecode
                        accountInfo.AccountPageurl = WebUtility.HtmlDecode(node.SelectSingleNode("div/div[@class='img-box']/a").GetAttributeValue("href", ""));
                        //accountInfo.ProfilePicture = node.SelectSingleNode("div/div[1]/a/img").InnerHtml;
                        accountInfo.ProfilePicture = WebUtility.HtmlDecode(node.SelectSingleNode("div/div[@class='img-box']/a/img").GetAttributeValue("src", ""));
                        accountInfo.Name = node.SelectSingleNode("div/div[2]/p[1]").InnerText.Trim().Replace("<!--red_beg-->", "").Replace("<!--red_end-->", "");
                        accountInfo.WeChatId = node.SelectSingleNode("div/div[2]/p[2]/label").InnerText.Trim();
                        accountInfo.QrCode = WebUtility.HtmlDecode(node.SelectSingleNode("div/div[3]/span/img").GetAttributeValue("src", ""));
                        accountInfo.Introduction = node.SelectSingleNode("dl[1]/dd").InnerText.Trim().Replace("<!--red_beg-->","").Replace("<!--red_end-->", "");
                        //早期的賬號認證和后期的認證顯示不一樣?,對比 bitsea 和 NUAA_1952 兩個賬號
                        //現在改為包含該script的即認證了
                        if (node.InnerText.Contains("document.write(authname('2'))"))
                        {
                            accountInfo.IsAuth = true;
                        }
                        else
                        {
                            accountInfo.IsAuth = false;
                        }
                        accountList.Add(accountInfo);
                    }
                    catch (Exception e)
                    {
                        logger.Warn(e);
                    }
                }
            }
            
          
            return accountList; 
        }

 

 
以上,說白了,解析就是Xpath調試,關鍵是看目標內容是是元素標簽內容,還是標簽屬性,
如果是標簽內容即形式為 <h> 我是內容</h>
則: node.SelectSingleNode(" div/div[2]/p[2]/label").InnerText.Trim();
 
如果要提取的目標內容是標簽屬性,如 <a  href="/im_target_url.htm" >點擊鏈接</a>
則node.SelectSingleNode(" div/div[@class='img-box']/a").GetAttributeValue("href", "")
 
 
 
 
 

五 、驗證碼處理以及文件緩存

  公眾號的主頁(示例廣州大學公眾號 https://mp.weixin.qq.com/profile?src=3×tamp=1505923231&ver=1&signature=gWXdb*Jzt1oByDAzW5aTzEWnXo6mkUwg3Ynjm3CYvKV0kdCLxALBR7JJ-EheLBI-v6UcocJqGmPbUY2KMXuSsg==)因為頁面是屬於微信的,反爬蟲非常嚴格,因此多次刷新容易產生要輸入驗證碼的頁面
 
比如公號主頁多次刷新會出現驗證碼
 
此時要向一個網址post驗證碼才可以解封
 
 
 
解封操作如下
/// <summary>
        /// 頁面出現驗證碼,輸入才能繼續,此驗證依賴cookie, 獲取驗證碼的requset有個cookie,每次不同,需要在post驗證碼的時候帶上
        /// </summary>
        /// <returns></returns>
        public bool VerifyCodeForContinute(string url ,bool isUseOCR)
        {
            bool isSuccess = false;
            logger.Debug("vcode appear, use VerifyCodeForContinute()");
            DateTime Epoch = new DateTime(1970, 1, 1,0,0,0,0);
            var timeStamp17 = (DateTime.UtcNow - Epoch).TotalMilliseconds.ToString("R"); //get timestamp with 17 bit
            string codeurl = "https://mp.weixin.qq.com/mp/verifycode?cert=" + timeStamp17;
            WebHeaderCollection headers = new WebHeaderCollection();
            var content = this.Get(headers, codeurl,"UTF-8",true);
            ShowImageHandle showImageHandle = new ShowImageHandle(DisplayImageFromBase64);
            showImageHandle.BeginInvoke(content, null, null);
            Console.WriteLine("請輸入驗證碼:");
            string verifyCode = Console.ReadLine();
            string postURL = "https://mp.weixin.qq.com/mp/verifycode";
            timeStamp17 = (DateTime.UtcNow - Epoch).TotalMilliseconds.ToString("R"); //get timestamp with 17 bit
            string postData = string.Format("cert={0}&input={1}",timeStamp17,verifyCode );// "{" + string.Format(@"'cert':'{0}','input':'{1}'", timeStamp17, verifyCode) + "}";
            headers.Add("Host", "mp.weixin.qq.com");
            headers.Add("Referer", url);
            string remsg = this.Post(postURL, headers, postData,true);
            try
            {
                JObject jo = JObject.Parse(remsg);//把json字符串轉化為json對象  
                int statusCode = (int)jo.GetValue("ret");
                if (statusCode == 0)
                {
                    isSuccess = true;
                }
                else
                {
                    logger.Error("cannot unblock because " + jo.GetValue("msg"));
                    var vcodeException = new WechatSogouVcodeException();
                    vcodeException.MoreInfo = "cannot jiefeng because " + jo.GetValue("msg");
                    throw vcodeException;
                }
            }catch(Exception e)
            {
                logger.Error(e);
            }
            return isSuccess;
        }

 

 
 
解釋下:
先訪問一個驗證碼產生頁面,帶17位時間戳
var timeStamp17 = (DateTime.UtcNow - Epoch).TotalMilliseconds.ToString("R"); //get timestamp with 17 bit
再向這個url query post你的驗證碼:
因此這里記得要啟用如果啟用了cookie,會通過FileCache類將cookie保存在緩存文件,下次請求如果開啟cookie container的話就會帶上此cookie
CookieCollection cc = Tools.LoadCookieFromCache();
request.CookieContainer = new CookieContainer();
request.CookieContainer.Add(cc);

 

 

 

六、后話

   
    上面只是一部分,剛開始寫的時候也沒想到會有這么多坑,但是沒辦法,坑再多只能自己慢慢填了,比如OCR,第三方打碼接入,多線程等等后期再實現。一個人的精力畢竟有限,相對滿大街的Python爬蟲,C#的爬蟲性質的項目本來就不多,盡管代碼寫得非常粗糙,但是我選擇了開放源碼希望更多人參與,歡迎各位看官收藏,可以的話給個星或者提交代碼
 
 


免責聲明!

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



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