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); List list=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 (Func func) { 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(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。