c# 基于Titanium爬取微信公众号历史文章列表
github:https://github.com/justcoding121/Titanium-Web-Proxy
什么是Titanium
基于C#的跨平台异步HTTP(S)代理服务器
类似的还有:
https://github.com/http-party/node-http-proxy
原理简述
对于HTTP
顾名思义,其实代理就是一个「中间人」角色,对于连接到它的客户端来说,它是服务端;对于要连接的服务端来说,它是客户端。它就负责在两端之间来回传送HTTP报文。
对于HTTPS
由于HTTPS加入了CA证书的校验,服务端可不验证客户端的证书,中间人可以伪装成客户端与服务端成功完成TLS握手;但是中间人没有服务端证书私钥,无论如何也无法伪装成服务端跟客户端建立TLS连接,所以我们需要换个方式代理HTTPS请求。
HTTPS在传输层之上建立了安全层,所有的HTTP请求都在安全层上传输。既然无法通过像代理一般HTTP请求的方式在应用层代理HTTPS请求,那么我们就退而求其次为在传输层为客户端和服务器建立起TCP连接。一旦TCP连接建好,代理无脑转发后续流量即可。所以这种代理,理论上适用于任意基于TCP的应用层协议,HTTPS网站使用的TLS协议当然也可以。这也是这种代理为什么被称为隧道的原因。
但是这样子无脑转发我们就无法获取到他们交互的数据了,怎么办?
此时就需要代理变身为伪HTTPS服务器,然后让客户端信任我们自定义的根证书,从而在客户端和代理、代理和服务端之间都能成功建立TLS连接。对于代理来说,两端的TLS流量都是可以解密的。
最后如果这个代理我们可编程,那么我们就可以对传送的HTTP报文做控制。
相关的应用场景有访问控制、防火墙、内容过滤、Web缓存、内容路由等等。
为什么要爬取历史文章
微信官方其实已经提过了素材列表接口
https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/Get_materials_list.html
但是接口获取的素材地址并不是真实在公众号上推送的地址,所以不存在阅读、评论、好看等功能。
这时候需要我们去公众号的历史文章页取数据。
实现步骤
开发环境:VisualStudioCommunity2019forMacVersion8.5.6(build11)
Frameworks:.NETCore3.1.0
NuGet:Newtonsoft.Json12.0.3、Titanium.Web.Proxy3.1.1301
大致思路
1、先实现通过代理抓包HTTPS,拦截微信客户端数据交互。
2、过滤其他地址,只监测微信文章。
3、访问任意微信文章页,获取header和cookie。
4、模拟微信访问历史页、分析抓取历史文章列表。
核心代码
SpiderProxy.cs
usingSystem;
usingSystem.Collections.Generic;
usingSystem.Linq;
usingSystem.Net;
usingSystem.Text.RegularExpressions;
usingSystem.Threading;
usingSystem.Threading.Tasks;
usingSystem.Web;
usingNewtonsoft.Json.Linq;
usingTitanium.Web.Proxy;
usingTitanium.Web.Proxy.Http;
usingTitanium.Web.Proxy.Models;
namespaceWechatArticleSpider
{
publicclassSpiderProxy
{
privatereadonlySemaphoreSlim@lock=newSemaphoreSlim(1);
privatereadonlyProxyServerproxyServer;
privatereadonlyExplicitProxyEndPointexplicitEndPoint;
publicSpiderProxy()
{
proxyServer=newProxyServer();
//在响应之前事件
proxyServer.BeforeResponse+=ProxyServer_BeforeResponse;
//绑定监听端口
explicitEndPoint=newExplicitProxyEndPoint(IPAddress.Any,8000,true);
Console.WriteLine("监听地址127.0.0.1:8000");
//隧道请求连接前事件,HTTPS用
explicitEndPoint.BeforeTunnelConnectRequest+=ExplicitEndPoint_BeforeTunnelConnectRequest;;
//代理服务器注册监听地址
proxyServer.AddEndPoint(explicitEndPoint);
}
publicvoidStart()
{
Console.WriteLine("开始监听");
//Start方法会检测证书,若为空会调用CertificateManager.EnsureRootCertificate();为我们自动生成根证书。
proxyServer.Start();
}
publicvoidStop()
{
//Unsubscribe&Quit
explicitEndPoint.BeforeTunnelConnectRequest-=ExplicitEndPoint_BeforeTunnelConnectRequest;
proxyServer.BeforeResponse-=ProxyServer_BeforeResponse;
Console.WriteLine("结束监听");
proxyServer.Stop();
}
privateasyncTaskExplicitEndPoint_BeforeTunnelConnectRequest(objectsender,Titanium.Web.Proxy.EventArguments.TunnelConnectSessionEventArgse)
{
stringhostname=e.HttpClient.Request.RequestUri.Host;
awaitWriteToConsole("Tunnelto:"+hostname);
if(!hostname.StartsWith("mp.weixin.qq.com"))
{
//是否要解析SSL,不解析就直接转发
e.DecryptSsl=false;
}
}
privateasyncTaskProxyServer_BeforeResponse(objectsender,Titanium.Web.Proxy.EventArguments.SessionEventArgse)
{
varrequest=e.HttpClient.Request;
//判断是否是微信文章页。
if(request.Host.Contains("mp.weixin.qq.com")&&(request.RequestUriString.StartsWith("/s?")||request.RequestUriString.StartsWith("/s/")))
{
byte[]bytes=awaite.GetResponseBody();
stringbody=System.Text.Encoding.UTF8.GetString(bytes);
ThreadPool.QueueUserWorkItem((stateInfo)=>{CrawlAsync(body,e.HttpClient.Request);});
}
}
privateasyncTaskCrawlAsync(stringbody,Requestrequest)
{
//采用正则表达式匹配数据
Matchmatch=Regex.Match(body,@"(.+)");
MatchmatchGhid=Regex.Match(body,@"varuser_name=""(.+)"";");
if(!match.Success||!matchGhid.Success)
{
return;
}
MatchCollectionmatches=Regex.Matches(body,@"(.+)");
if(match.Groups.Count==0)
{
return;
}
awaitWriteToConsole("检测到微信文章页:"+match.Groups[1].Value+""+matches[0].Groups[1].Value+""+matches[1].Groups[1].Value+"");
varqueryString=HttpUtility.ParseQueryString(request.RequestUriString.Substring(3));
varhttpClient=newHttpClient(request.Headers);
awaitWriteToConsole("Client实例化,已获取header,cookie");
//获取历史页信息
stringresult=httpClient.Get(string.Format("https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz={0}&scene=124#wechat_redirect",queryString["__biz"]));
//封接口检测
if(result.Contains("操作频繁,请稍后再试"))
{
awaitWriteToConsole("操作频繁,请稍后再试限制24小时请更换微信");
awaitWriteToConsole("已停止爬虫任务,请更换之后重启助手。");
}
//是否抓取完
boolend=false;
//下标
intoffset=0;
do
{
//获取历史消息
stringjsonResult=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"]));
JObjectjobject=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"]);
stringstrList=jobject["general_msg_list"].ToString();
JObjecttemp=JObject.Parse(strList);
Listlist=temp["list"].ToList();
foreach(variteminlist)
{
JTokencomm_msg_info=item["comm_msg_info"];
JTokenapp_msg_ext_info=item["app_msg_ext_info"];
if(app_msg_ext_info==null)
{
continue;
}
//发布时间
stringpublicTime=comm_msg_info["datetime"].ToString();
//文章标题
stringtitle=app_msg_ext_info["title"].ToString();
//文章摘要
stringdigest=app_msg_ext_info["digest"].ToString();
//文章地址
stringcontent_url=app_msg_ext_info["content_url"].ToString();
//文章封面
stringcover=app_msg_ext_info["cover"].ToString();
//作者
stringauthor=app_msg_ext_info["author"].ToString();
awaitWriteToConsole(String.Format("{0},{1},{2},{3},{4},{5}",publicTime,title,digest,content_url,cover,author));
//今天发布了多条消息
if(app_msg_ext_info["is_multi"].ToString()=="1")
{
foreach(varmultiIteminapp_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();
awaitWriteToConsole(String.Format("{0},{1},{2},{3},{4},{5}",publicTime,title,digest,content_url,cover,author));
}
}
}
}
else
{
end=true;
}
//每5秒翻页一次
awaitTask.Delay(5000);
}while(!end);
awaitWriteToConsole("历史文章抓取完成");
}
privateasyncTaskWriteToConsole(stringmessage,ConsoleColor?consoleColor=null)
{
await@lock.WaitAsync();
if(consoleColor.HasValue)
{
ConsoleColorexisting=Console.ForegroundColor;
Console.ForegroundColor=consoleColor.Value;
Console.WriteLine(message);
Console.ForegroundColor=existing;
}
else
{
Console.WriteLine(message);
}
@lock.Release();
}
}
}
HttpClient.cs
usingSystem;
usingSystem.IO;
usingSystem.Net;
usingSystem.Threading;
usingTitanium.Web.Proxy.Http;
namespaceWechatArticleSpider
{
classHttpClient
{
privatereadonlyHeaderCollectionheaderCollection;
privatereadonlyCookieContainercookieContainer=newCookieContainer();
///
///微信请求客户端
///
///拦截微信请求头集合
publicHttpClient(HeaderCollectionheaderCollection)
{
cookieContainer=newCookieContainer();
ServicePointManager.DefaultConnectionLimit=512;
ServicePointManager.SecurityProtocol=SecurityProtocolType.Tls12|SecurityProtocolType.Tls11;
this.headerCollection=headerCollection;
}
///
///带微信参数的GET请求
///
///请求地址
///请求结果
publicstringGet(stringurl)
{
stringret=Retry(()=>
{
HttpWebRequestwebRequest=WebRequest.CreateHttp(url);
webRequest=PretendWechat(webRequest);
HttpWebResponseresponse=webRequest.GetResponse()asHttpWebResponse;
stringresult=newStreamReader(response.GetResponseStream()).ReadToEnd();
returnresult;
});
returnret;
}
///
///伪造微信请求
///
///需要伪造的request
///
publicHttpWebRequestPretendWechat(HttpWebRequestrequest)
{
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(newUri("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;
returnrequest;
}
catch(Exceptione)
{
Console.WriteLine(e.Message);
throw;
}
}
///
///三次重试机制
///
///参数类型
///方法
///
privatestaticTRetry(Funcfunc)
{
interr=0;
while(err<3)
{
try
{
returnfunc();
}
catch(WebExceptionwebExp)
{
err++;
Thread.Sleep(5000);
if(err>2)
{
throwwebExp;
}
}
}
returnfunc();
}
}
}
测试结果
首先我们主动设置一下系统代理。
接着启动代理。
对于不是目标地址的https请求,一律过滤直接转发。
此时Titanium应该会为我们生成了根证书。
右键-》GetInfo-》Trust-》选择AlwaysTrust,如果不信任根证书,会发现ProxyServer_BeforeResponse不执行。
最后我们随意的访问一篇公众号文章,代理就会执行脚本去抓公众号的历史文章列表了。
demo:链接:https://pan.baidu.com/s/1ZafgBH1dEiDcdB9E77osFg密码:tuuv
以上就是c#基于Titanium爬取微信公众号历史文章列表的详细内容,更多关于c#基于Titanium爬取公众号历史文章的资料请关注毛票票其它相关文章!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。