c# 基于Titanium爬取微信公眾號(hào)歷史文章列表
github:https://github.com/justcoding121/Titanium-Web-Proxy
什么是Titanium
基于C#的跨平臺(tái)異步HTTP(S)代理服務(wù)器
類似的還有:
https://github.com/http-party/node-http-proxy
原理簡(jiǎn)述
對(duì)于HTTP

顧名思義,其實(shí)代理就是一個(gè)「中間人」角色,對(duì)于連接到它的客戶端來說,它是服務(wù)端;對(duì)于要連接的服務(wù)端來說,它是客戶端。它就負(fù)責(zé)在兩端之間來回傳送 HTTP 報(bào)文。
對(duì)于HTTPS
由于HTTPS加入了CA證書的校驗(yàn),服務(wù)端可不驗(yàn)證客戶端的證書,中間人可以偽裝成客戶端與服務(wù)端成功完成 TLS 握手;但是中間人沒有服務(wù)端證書私鑰,無論如何也無法偽裝成服務(wù)端跟客戶端建立 TLS 連接,所以我們需要換個(gè)方式代理HTTPS請(qǐng)求。

HTTPS在傳輸層之上建立了安全層,所有的HTTP請(qǐng)求都在安全層上傳輸。既然無法通過像代理一般HTTP請(qǐng)求的方式在應(yīng)用層代理HTTPS請(qǐng)求,那么我們就退而求其次為在傳輸層為客戶端和服務(wù)器建立起TCP連接。一旦 TCP 連接建好,代理無腦轉(zhuǎn)發(fā)后續(xù)流量即可。所以這種代理,理論上適用于任意基于 TCP 的應(yīng)用層協(xié)議,HTTPS 網(wǎng)站使用的 TLS 協(xié)議當(dāng)然也可以。這也是這種代理為什么被稱為隧道的原因。
但是這樣子無腦轉(zhuǎn)發(fā)我們就無法獲取到他們交互的數(shù)據(jù)了,怎么辦?
此時(shí)就需要代理變身為偽HTTPS服務(wù)器,然后讓客戶端信任我們自定義的根證書,從而在客戶端和代理、代理和服務(wù)端之間都能成功建立 TLS 連接。對(duì)于代理來說,兩端的TLS流量都是可以解密的。
最后如果這個(gè)代理我們可編程,那么我們就可以對(duì)傳送的HTTP報(bào)文做控制。
相關(guān)的應(yīng)用場(chǎng)景有訪問控制、防火墻、內(nèi)容過濾、Web緩存、內(nèi)容路由等等。
為什么要爬取歷史文章
微信官方其實(shí)已經(jīng)提過了素材列表接口
https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/Get_materials_list.html
但是接口獲取的素材地址并不是真實(shí)在公眾號(hào)上推送的地址,所以不存在閱讀、評(píng)論、好看等功能。
這時(shí)候需要我們?nèi)ス娞?hào)的歷史文章頁取數(shù)據(jù)。
實(shí)現(xiàn)步驟
開發(fā)環(huán)境:Visual Studio Community 2019 for Mac Version 8.5.6 (build 11)
Frameworks:.NET Core 3.1.0
NuGet:Newtonsoft.Json 12.0.3、Titanium.Web.Proxy 3.1.1301
大致思路
1、先實(shí)現(xiàn)通過代理抓包HTTPS,攔截微信客戶端數(shù)據(jù)交互。
2、過濾其他地址,只監(jiān)測(cè)微信文章。
3、訪問任意微信文章頁,獲取header和cookie。
4、模擬微信訪問歷史頁、分析抓取歷史文章列表。
核心代碼
SpiderProxy.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Newtonsoft.Json.Linq;
using Titanium.Web.Proxy;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Models;
namespace WechatArticleSpider
{
public class SpiderProxy
{
private readonly SemaphoreSlim @lock = new SemaphoreSlim(1);
private readonly ProxyServer proxyServer;
private readonly ExplicitProxyEndPoint explicitEndPoint;
public SpiderProxy()
{
proxyServer = new ProxyServer();
//在響應(yīng)之前事件
proxyServer.BeforeResponse += ProxyServer_BeforeResponse;
//綁定監(jiān)聽端口
explicitEndPoint = new ExplicitProxyEndPoint(IPAddress.Any, 8000, true);
Console.WriteLine("監(jiān)聽地址127.0.0.1:8000");
//隧道請(qǐng)求連接前事件,HTTPS用
explicitEndPoint.BeforeTunnelConnectRequest += ExplicitEndPoint_BeforeTunnelConnectRequest; ;
//代理服務(wù)器注冊(cè)監(jiān)聽地址
proxyServer.AddEndPoint(explicitEndPoint);
}
public void Start()
{
Console.WriteLine("開始監(jiān)聽");
//Start方法會(huì)檢測(cè)證書,若為空會(huì)調(diào)用CertificateManager.EnsureRootCertificate();為我們自動(dòng)生成根證書。
proxyServer.Start();
}
public void Stop()
{
// Unsubscribe & Quit
explicitEndPoint.BeforeTunnelConnectRequest -= ExplicitEndPoint_BeforeTunnelConnectRequest;
proxyServer.BeforeResponse -= ProxyServer_BeforeResponse;
Console.WriteLine("結(jié)束監(jiān)聽");
proxyServer.Stop();
}
private async Task ExplicitEndPoint_BeforeTunnelConnectRequest(object sender, Titanium.Web.Proxy.EventArguments.TunnelConnectSessionEventArgs e)
{
string hostname = e.HttpClient.Request.RequestUri.Host;
await WriteToConsole("Tunnel to: " + hostname);
if (!hostname.StartsWith("mp.weixin.qq.com"))
{
//是否要解析SSL,不解析就直接轉(zhuǎn)發(fā)
e.DecryptSsl = false;
}
}
private async Task ProxyServer_BeforeResponse(object sender, Titanium.Web.Proxy.EventArguments.SessionEventArgs e)
{
var request = e.HttpClient.Request;
//判斷是否是微信文章頁。
if (request.Host.Contains("mp.weixin.qq.com")&&(request.RequestUriString.StartsWith("/s?") || request.RequestUriString.StartsWith("/s/")))
{
byte[] bytes = await e.GetResponseBody();
string body = System.Text.Encoding.UTF8.GetString(bytes);
ThreadPool.QueueUserWorkItem((stateInfo) => { CrawlAsync(body, e.HttpClient.Request); });
}
}
private async Task CrawlAsync(string body,Request request)
{
//采用正則表達(dá)式匹配數(shù)據(jù)
Match match = Regex.Match(body, @"<strong class=""profile_nickname"">(.+)</strong>");
Match matchGhid = Regex.Match(body, @"var user_name = ""(.+)"";");
if (!match.Success || !matchGhid.Success)
{
return;
}
MatchCollection matches = Regex.Matches(body, @"<span class=""profile_meta_value"">(.+)</span>");
if (match.Groups.Count == 0)
{
return;
}
await WriteToConsole("檢測(cè)到微信文章頁: " + match.Groups[1].Value + " " + matches[0].Groups[1].Value + " " + matches[1].Groups[1].Value + " ");
var queryString = HttpUtility.ParseQueryString(request.RequestUriString.Substring(3));
var httpClient = new HttpClient(request.Headers);
await WriteToConsole("Client實(shí)例化,已獲取header,cookie");
//獲取歷史頁信息
string result = httpClient.Get(string.Format("https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz={0}&scene=124#wechat_redirect", queryString["__biz"]));
//封接口檢測(cè)
if (result.Contains("操作頻繁,請(qǐng)稍后再試"))
{
await WriteToConsole("操作頻繁,請(qǐng)稍后再試 限制24小時(shí) 請(qǐng)更換微信");
await WriteToConsole("已停止爬蟲任務(wù),請(qǐng)更換之后重啟助手。");
}
//是否抓取完
bool end = false;
//下標(biāo)
int offset = 0;
do
{
//獲取歷史消息
string jsonResult = httpClient.Get(string.Format("https://mp.weixin.qq.com/mp/profile_ext?action=getmsg&__biz={0}&f=json&offset={1}&count=10&is_ok=1&scene=124&uin={2}&key={3}&pass_ticket={4}&wxtoken=&x5=0&f=json", queryString["__biz"], offset, queryString["uin"], queryString["key"], queryString["pass_ticket"]));
JObject jobject = JObject.Parse(jsonResult);
if (Convert.ToInt32(jobject["ret"]) == 0)
{
if (Convert.ToInt32(jobject["can_msg_continue"]) == 0)
{
end = true;
}
offset = Convert.ToInt32(jobject["next_offset"]);
string strList = jobject["general_msg_list"].ToString();
JObject temp = JObject.Parse(strList);
List<JToken> list = temp["list"].ToList();
foreach (var item in list)
{
JToken comm_msg_info = item["comm_msg_info"];
JToken app_msg_ext_info = item["app_msg_ext_info"];
if (app_msg_ext_info == null)
{
continue;
}
//發(fā)布時(shí)間
string publicTime = comm_msg_info["datetime"].ToString();
//文章標(biāo)題
string title = app_msg_ext_info["title"].ToString();
//文章摘要
string digest = app_msg_ext_info["digest"].ToString();
//文章地址
string content_url = app_msg_ext_info["content_url"].ToString();
//文章封面
string cover = app_msg_ext_info["cover"].ToString();
//作者
string author = app_msg_ext_info["author"].ToString();
await WriteToConsole(String.Format("{0},{1},{2},{3},{4},{5}", publicTime, title, digest, content_url, cover, author));
//今天發(fā)布了多條消息
if (app_msg_ext_info["is_multi"].ToString() == "1")
{
foreach (var multiItem in app_msg_ext_info["multi_app_msg_item_list"].ToList())
{
title = multiItem["title"].ToString();
digest = multiItem["digest"].ToString();
content_url = multiItem["content_url"].ToString();
cover = multiItem["cover"].ToString();
author = multiItem["author"].ToString();
await WriteToConsole(String.Format("{0},{1},{2},{3},{4},{5}", publicTime, title, digest, content_url, cover, author));
}
}
}
}
else
{
end = true;
}
//每5秒翻頁一次
await Task.Delay(5000);
} while (!end);
await WriteToConsole("歷史文章抓取完成");
}
private async Task WriteToConsole(string message, ConsoleColor? consoleColor = null)
{
await @lock.WaitAsync();
if (consoleColor.HasValue)
{
ConsoleColor existing = Console.ForegroundColor;
Console.ForegroundColor = consoleColor.Value;
Console.WriteLine(message);
Console.ForegroundColor = existing;
}
else
{
Console.WriteLine(message);
}
@lock.Release();
}
}
}
HttpClient.cs
using System;
using System.IO;
using System.Net;
using System.Threading;
using Titanium.Web.Proxy.Http;
namespace WechatArticleSpider
{
class HttpClient
{
private readonly HeaderCollection headerCollection;
private readonly CookieContainer cookieContainer = new CookieContainer();
/// <summary>
/// 微信請(qǐng)求客戶端
/// </summary>
/// <param name="headerCollection">攔截微信請(qǐng)求頭集合</param>
public HttpClient(HeaderCollection headerCollection)
{
cookieContainer = new CookieContainer();
ServicePointManager.DefaultConnectionLimit = 512;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11;
this.headerCollection = headerCollection;
}
/// <summary>
/// 帶微信參數(shù)的GET請(qǐng)求
/// </summary>
/// <param name="url">請(qǐng)求地址</param>
/// <returns>請(qǐng)求結(jié)果</returns>
public string Get(string url)
{
string ret = Retry<string>(() =>
{
HttpWebRequest webRequest = WebRequest.CreateHttp(url);
webRequest = PretendWechat(webRequest);
HttpWebResponse response = webRequest.GetResponse() as HttpWebResponse;
string result = new StreamReader(response.GetResponseStream()).ReadToEnd();
return result;
});
return ret;
}
/// <summary>
/// 偽造微信請(qǐng)求
/// </summary>
/// <param name="request">需要偽造的request</param>
/// <returns></returns>
public HttpWebRequest PretendWechat(HttpWebRequest request)
{
try
{
request.Host = headerCollection.Headers["Host"].Value;
request.UserAgent = headerCollection.Headers["User-Agent"].Value;
request.Headers.Set(headerCollection.Headers["Accept-Language"].Name, headerCollection.Headers["Accept-Language"].Value);
request.Headers.Set(headerCollection.Headers["Accept-Encoding"].Name, headerCollection.Headers["Accept-Encoding"].Value);
cookieContainer.SetCookies(new Uri("https://mp.weixin.qq.com"), headerCollection.Headers["Cookie"].Value.Replace(";", ","));
request.KeepAlive = true;
request.Accept = headerCollection.Headers["Accept"].Value;
request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
request.CookieContainer = cookieContainer;
request.AllowAutoRedirect = true;
request.ServicePoint.Expect100Continue = false;
request.Timeout = 35000;
return request;
}
catch (Exception e)
{
Console.WriteLine(e.Message);
throw;
}
}
/// <summary>
/// 三次重試機(jī)制
/// </summary>
/// <typeparam name="T">參數(shù)類型</typeparam>
/// <param name="func">方法</param>
/// <returns></returns>
private static T Retry<T>(Func<T> func)
{
int err = 0;
while (err < 3)
{
try
{
return func();
}
catch (WebException webExp)
{
err++;
Thread.Sleep(5000);
if (err > 2)
{
throw webExp;
}
}
}
return func();
}
}
}
測(cè)試結(jié)果
首先我們主動(dòng)設(shè)置一下系統(tǒng)代理。

接著啟動(dòng)代理。

對(duì)于不是目標(biāo)地址的https請(qǐng)求,一律過濾直接轉(zhuǎn)發(fā)。
此時(shí)Titanium應(yīng)該會(huì)為我們生成了根證書。

右鍵-》Get Info-》Trust-》選擇Always Trust,如果不信任根證書,會(huì)發(fā)現(xiàn)ProxyServer_BeforeResponse不執(zhí)行。
最后我們隨意的訪問一篇公眾號(hào)文章,代理就會(huì)執(zhí)行腳本去抓公眾號(hào)的歷史文章列表了。

demo:鏈接:https://pan.baidu.com/s/1ZafgBH1dEiDcdB9E77osFg 密碼:tuuv
以上就是c# 基于Titanium爬取微信公眾號(hào)歷史文章列表的詳細(xì)內(nèi)容,更多關(guān)于c# 基于Titanium爬取公眾號(hào)歷史文章的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- c# Selenium爬取數(shù)據(jù)時(shí)防止webdriver封爬蟲的方法
- C# 爬蟲簡(jiǎn)單教程
- 用C#做網(wǎng)絡(luò)爬蟲的步驟教學(xué)
- c#爬蟲爬取京東的商品信息
- C#程序如何調(diào)用C++?dll詳細(xì)教程
- C# 利用代理爬蟲網(wǎng)頁的實(shí)現(xiàn)方法
- 利用C#實(shí)現(xiàn)最基本的小說爬蟲示例代碼
- C#簡(jiǎn)單爬蟲案例分享
- C#多線程爬蟲抓取免費(fèi)代理IP的示例代碼
- C#制作多線程處理強(qiáng)化版網(wǎng)絡(luò)爬蟲
- 利用C#實(shí)現(xiàn)網(wǎng)絡(luò)爬蟲
相關(guān)文章
C#設(shè)計(jì)模式之適配器模式與裝飾器模式的實(shí)現(xiàn)
創(chuàng)建型設(shè)計(jì)模式主要是為了解決創(chuàng)建對(duì)象的問題,而結(jié)構(gòu)型設(shè)計(jì)模式則是為了解決已有對(duì)象的使用問題。本文將用C#語言實(shí)現(xiàn)結(jié)構(gòu)型設(shè)計(jì)模式中的適配器模式與裝飾器模式,感興趣的可以了解一下2022-04-04
c#使用Dataset讀取XML文件動(dòng)態(tài)生成菜單的方法
這篇文章主要介紹了c#使用Dataset讀取XML文件動(dòng)態(tài)生成菜單的方法,涉及C#使用Dataset操作XML文件的相關(guān)技巧,需要的朋友可以參考下2015-05-05
Unity實(shí)現(xiàn)識(shí)別圖像中主體及其位置
EasyDL基于飛槳開源深度學(xué)習(xí)平臺(tái),面向企業(yè)AI應(yīng)用開發(fā)者提供零門檻AI開發(fā)平臺(tái),實(shí)現(xiàn)零算法基礎(chǔ)定制高精度AI模型。本文將利用Unity和EasyDL實(shí)現(xiàn)識(shí)別圖像中主體及其位置,感興趣的可以了解一下2022-02-02
C#驗(yàn)證控件validator的簡(jiǎn)單使用
這篇文章主要介紹了C#驗(yàn)證控件validator的簡(jiǎn)單使用方法和示例,十分的簡(jiǎn)單實(shí)用,有需要的小伙伴可以參考下。2015-06-06
總結(jié)C#網(wǎng)絡(luò)編程中對(duì)于Cookie的設(shè)定要點(diǎn)
這篇文章主要介紹了總結(jié)C#網(wǎng)絡(luò)編程中對(duì)于Cookie的設(shè)定要點(diǎn),文中還給出了一個(gè)cookie操作實(shí)例僅供參照,需要的朋友可以參考下2016-04-04
通過C#實(shí)現(xiàn)在Excel單元格中寫入文本、或數(shù)值
在商業(yè)、學(xué)術(shù)和日常生活中,Excel 的使用極為普遍,本文將詳細(xì)介紹如何使用免費(fèi).NET庫將數(shù)據(jù)寫入到 Excel 中,包括文本、數(shù)值、數(shù)組、和DataTable數(shù)據(jù)的輸入,需要的朋友可以參考下2024-07-07

