c# 通过webView2模拟登陆小红书网页版,解析无水印视频图片,以及解决X-s,X-t签名验证【2023年4月15日】

news/2024/4/24 19:46:28/

一、c# WebView2简介
  1.一开始使用WebBrowser,因为WebBrowser控件使用的是ie内核,经过修改注册表切换为Edge内核后,
发现Edge内核版本较低,加载一些视频网站提示“浏览器版本过低“,”视频无法加载“。

2.WebBrowser内核版本与WebView2比较

WebBrowser内核版本:
内核版本 (Version) Edge 18.9200 兼容 WebKit 537.36  Chrome 70 
UserAgent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.9200

当前Edge内核版本:
内核版本 (Version)    WebKit 537.36  Chrome 111.0.0.0
UserAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.54

WebView2内核版本:
内核版本 (Version)    WebKit 537.36  Chrome 111.0.0.0
UserAgent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.51

可见,WebView2内核版本跟Edge一样,能顺利打开视频网站。WebBrowser内核版本过低。

WebView2加载视频网站

3.WebView2概述
  Microsoft Edge WebView2 控件允许在本机应用中嵌入 web 技术(HTML、CSS 以及 JavaScript)。 WebView2 控件使用 Microsoft Edge 作为绘制引擎,以在本机应用中显示 web 内容。
虽说无法跨平台,但是在windows应用下做为原浏览器控件替代品还是不错的。

4.安装webview2
   打开NuGet,搜索WebView2,安装之后,可以看到左侧就有了webview2控件,可以直接拖到窗体内。

二、问题分析
1、关于登陆会话的问题
网页端必须打开小红书网站。小红书打开后,在浏览器Cookie里,有一个字段:
web_session=040069b3b3f6625dade26f8d1d364b44f72186
这是记录登陆会话信息的。请求时headers中需要x-s、x-t,cookie中需要有web_session。
经测试,这个web_session会在浏览器保存一段时间,具体多久还有待验证(B站也有个类似的session,是一个月)。其它字段无关紧要。
不使用WebView2打开网站的话,需要到网站申请web_session,这里WebView2已经替我们弄好了。
通过c# WebView2获取cookie信息的方法:

private Dictionary<string, string> mCookies = new Dictionary<string, string>();//保存Cookie到字典中/// <summary>/// WebView2异步获取cookie/// </summary>/// <param name="url">与cookie关联的域名</param>private async void getCookie(string url){List<CoreWebView2Cookie> cookieList = await webView.CoreWebView2.CookieManager.GetCookiesAsync(url);mCookies.Clear();for (int i = 0; i < cookieList.Count; ++i){CoreWebView2Cookie cookie = webView.CoreWebView2.CookieManager.CreateCookieWithSystemNetCookie(cookieList[i].ToSystemNetCookie());mCookies.Add(cookie.Name, cookie.Value);}}/// <summary>/// 提取cookie中的一个字段;/// </summary>/// <param name="url">域名</param>/// <param name="key">关键字,如:web_session</param>/// <param name="t">延时(没用到)</param>/// <returns></returns>public string getCookieEx(string url, string key, int t){getCookie(url);if (mCookies.ContainsKey(key)){string cookies = "";foreach (var cookie in mCookies){cookies += cookie.Key + "=" + cookie.Value + ";";}cookies = key + "=" + mCookies[key];return cookies;}return null;}

2.笔记信息接口

目前笔记信息接口: /api/sns/web/v1/feed
请求时headers中需要x-s、x-t,cookie中需要有web_session。

3.X-S
定位方法很多,可以全局搜 "X-s" 。往上找可以发现该段为 sign 方法,function sign(e, t) {}

全部复制到本地,然后根据报错把缺的方法和环境补一下,比如a0_0x4dee00、a0_0x5c27、a0_0x543e等方法,

然后把常用的navigator、location、document、window加上就好了。

该过程中根据具体错误再调试分析, 比如sign方法的 case "6",修改为var vr = window 、在case "7"中可以手动修改为 dr = ur['sNYMU']
这里已经拿到了function sign(e, t) {}的JavaScript版,需要的+v:byc6352

三、WebView2中C#和JavaScript代码互操作
 1.需要创建一个ScriptHost对象,并注册到WebView2中:

    /// <summary>/// 网页调用C#方法/// </summary>[ClassInterface(ClassInterfaceType.AutoDual)][ComVisible(true)]public class ScriptHost

2.在WebView2初始化完成事件中注册ScriptHost对象

        /// <summary>/// CoreWebView2初始化完成/// </summary>/// <param name="sender"></param>/// <param name="e"></param>private void webView_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e){//注册winning,winasync脚本c#互操作webView.CoreWebView2.AddHostObjectToScript("scriptHost", scriptHost);//注册全局变量winning 同步操作;winasync:异步操作;webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("var winasync= window.chrome.webview.hostObjects.scriptHost;");webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("var winning= window.chrome.webview.hostObjects.sync.scriptHost;");}

3.ScriptHost中暴露的公共方法,都可以在前端JavaScript中调用。

        /// <summary>/// 日志记录(JavaScript前端调用/// </summary>/// <param name="message">JavaScript前端信息</param>public void log(string message){Log.i(message);//记录到文本文件中//MessageBox.Show(message);}
..............................................
winning.log(data);//JavaScript端调用(同步调用);

四、下载封面及视频


using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Configuration;
using System.Text;
using System.Threading.Tasks;namespace XhsVideo
{/// <summary>/// 下载视频/// </summary>internal class VideoDown{public int id { get; set; }public VideoInfo video { get; set; }public string savedir { get; set; }public string msg { get; set; }public bool success { get; set; }public string headers { get; set; }public VideoDown(int id,VideoInfo video,string savdir,string headers=null) {this.id = id;this.video = video;this.savedir = savdir;if (System.IO.Directory.Exists(savdir)) System.IO.Directory.CreateDirectory(savdir);this.headers = headers;}public void process(){string filename = MakeValidFileName( video.title,"");  //去除文件名中的非法字符string videoname = savedir + "\\" + filename + ".mp4";string covername = savedir + "\\" + filename + ".webp";if (System.IO.File.Exists(covername)) System.IO.File.Delete(covername);if (System.IO.File.Exists(videoname)) System.IO.File.Delete(videoname);Log.i(videoname);Log.i(covername);//NetHelper.downloadfileAsync(video.videoUrl, videoname);NetHelper.downloadfileAsync(video.coverUrl, covername);NetHelper.DownloadFileAsync(video.videoUrl, videoname, showProgress);//显示下载视频的进度}/*** @param text: 原始串* @param replacement: 要替换的字符串*/public static string MakeValidFileName(string text, string replacement = "_"){StringBuilder str = new StringBuilder();var invalidFileNameChars = System.IO.Path.GetInvalidFileNameChars();foreach (var c in text){if (invalidFileNameChars.Contains(c)){str.Append(replacement ?? "");}else{str.Append(c);}}return str.ToString();}/// <summary>/// 下载视频的进度回调函数/// </summary>/// <param name="msg">下载进度信息</param>public void showProgress(string msg){this.msg = msg;Log.i(msg);fMainForm.GetFMainForm().syncContext.Send(fMainForm.GetFMainForm().SetTextSafePost, this);//Post 将信息发送到窗体显示}}/// <summary>/// 视频信息类,由:标题,封面地址,视频地址组成。/// </summary>public class VideoInfo{public string title { get; set; }public string coverUrl { get; set; }public string videoUrl { get; set; }public VideoInfo(string title,string coverUrl,string videoUrl){this.title = title;this.coverUrl = coverUrl;this.videoUrl = videoUrl;}}
}

五、日志记录


using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace XhsVideo
{/// <summary>/// 日志记录 到文本文件/// </summary>internal class Log{private static  Log log;private static string logName;private Log(string filename) {logName = filename;}public static Log GetLog(string filename){if (log == null) { log = new Log(filename); }return log;}public static Log GetLog(){return log;}public static void i(string msg){string now = DateTime.Now.ToString();string[] text = new string[2];text[0] = now;text[1] = msg;try{using (StreamWriter sw = new StreamWriter(logName,true,Encoding.UTF8)){foreach (string s in text){sw.WriteLine(s);}sw.Close();}}catch (Exception e){//Console.WriteLine("Exception: " + e.Message);}finally{//Console.WriteLine("Executing finally block.");}}}
}

六、网络访问组件


using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Headers;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.IO;
using System.Windows.Forms;namespace XhsVideo
{/// <summary>/// 网络访问组件/// </summary>internal class NetHelper{#region 文件下载/// <summary>/// 下载文件/// </summary>/// <param name="url">文件下载地址</param>/// <param name="savePath">本地保存路径+名称</param>/// <param name="downloadCallBack">下载回调(总长度,已下载,进度)</param>/// <returns></returns>/// <exception cref="Exception"></exception>public static async Task DownloadFileAsync(string url, string savePath, Action<string> downloadCallBack = null){try{downloadCallBack?.Invoke($"文件【{url}】开始下载!");HttpResponseMessage response = null;using (HttpClient client = new HttpClient())response = await client.GetAsync(url);if (response == null){downloadCallBack?.Invoke("文件获取失败");return;}var total = response.Content.Headers.ContentLength ?? 0;var stream = await response.Content.ReadAsStreamAsync();var file = new FileInfo(savePath);using (var fileStream = file.Create())using (stream){if (downloadCallBack == null){await stream.CopyToAsync(fileStream);downloadCallBack?.Invoke($"文件【{url}】下载完成!");}else{byte[] buffer = new byte[1024];long readLength = 0;int length;double temp = 0;string msg = "";while ((length = await stream.ReadAsync(buffer, 0, buffer.Length)) != 0){// 写入到文件fileStream.Write(buffer, 0, length);//更新进度readLength += length;double progress = Math.Round((double)readLength / total * 100, 2, MidpointRounding.AwayFromZero);//.ToZeroif ((progress % 1) == 0 && (progress % 1) != temp){ msg = $"总大小:【{total}】,已下载:【{readLength}】,进度:【{progress}】";downloadCallBack?.Invoke(msg);}temp = progress % 1;//下载完毕立刻关闭释放文件流if (total == readLength && progress == 100){fileStream.Close();fileStream.Dispose();msg = $"总大小:【{total}】,已下载:【{readLength}】,进度:【{progress}】下载完成。";downloadCallBack?.Invoke(msg);}}}}}catch (Exception ex){downloadCallBack?.Invoke($"下载文件失败:{ex.Message}!");}}#endregion/// <summary>/// 异步下载文件/// </summary>/// <param name="url"></param>/// <param name="filename"></param>public static async void downloadfileAsync(string url,string filename){using (var web = new WebClient()){await web.DownloadFileTaskAsync(url, filename);}}/// <summary>/// 地址重定向/// </summary>/// <param name="url">原地址</param>/// <param name="domain">域名</param>/// <param name="ua">userAgent</param>/// <returns>重定向后的地址</returns>public static string getRedirectedUrl(string url, string domain, string ua){var str = getRedirectedUrl_T(url);return str.Result;}/// <summary>/// 异步地址重定向/// </summary>/// <param name="url"></param>/// <returns></returns>private static async Task<string> getRedirectedUrl_T(string url){try{var handler = new HttpClientHandler(){AllowAutoRedirect = false};var client = new HttpClient(handler);var response = await client.GetAsync(url).ConfigureAwait(continueOnCapturedContext: false);int statuscode = (int)response.StatusCode;if (statuscode == 307){string location = response.Headers.Location.ToString();return location;}if (statuscode == 302){string location = response.Headers.Location.ToString();return location;}else{return "";}}catch (Exception e){System.Diagnostics.Debug.WriteLine("getRedirectedUrl_T:" + e.ToString());return "";}}//---------------------------------------------------------------post---------------------------------------------------------------------/// <summary>/// Post访问/// </summary>/// <param name="url">访问地址</param>/// <param name="args">数据</param>/// <param name="headers">HTTP头</param>/// <returns>返回服务器JSON数据</returns>public static string getPostResult(string url, string args, string headers){var str = getPostResult_T(url, args, headers);return str.Result;}/// <summary>/// 异步post调用/// </summary>/// <param name="url"></param>/// <param name="args"></param>/// <param name="headers"></param>/// <returns></returns>private static async Task<string> getPostResult_T(string url, string args, string headers){try{var handler = new HttpClientHandler() { UseCookies = false };var client = new HttpClient(handler);// { BaseAddress = baseAddress };client.Timeout = TimeSpan.FromSeconds(20);var message = new HttpRequestMessage(HttpMethod.Post, url);message.Content = new StringContent(args);message.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");//message.Headers;Dictionary<string, string> dictionary = ParseToDictionary(headers);foreach (var pair in dictionary){if (pair.Key.Equals("content-type")) continue;if (pair.Key.Equals("Content-Type")) continue;message.Headers.Add(pair.Key, pair.Value);}var result = await client.SendAsync(message).ConfigureAwait(continueOnCapturedContext: false);result.EnsureSuccessStatusCode();return await result.Content.ReadAsStringAsync();}catch (Exception e){//EventLog.GetEventLogs(e.ToString);System.Diagnostics.Debug.WriteLine("getPostResultEx_T:" + e.ToString());///console.write(e.ToString());return "";}}/// <summary>/// 字符串解析为字典数据/// </summary>/// <param name="str">以回车换行符分割的字符串</param>/// <returns>字典数据</returns>private static Dictionary<string, string> ParseToDictionary(string str){Dictionary<string, string> result = new Dictionary<string, string>();string str1 = str;int i = str1.IndexOf("\r\n");int j = 0;while (i > 0){string str2 = str1.Substring(0, i);j = str2.IndexOf(":");if (j > 0){string str21 = str2.Substring(0, j);string str22 = str2.Substring(j + 1);result.Add(str21, str22);}str1 = str1.Substring(i + 2);i = str1.IndexOf("\r\n");}j = str1.IndexOf(":");if (j > 0){string str21 = str1.Substring(0, j);string str22 = str1.Substring(j + 1);result.Add(str21, str22);}return result;}//-------------------------------------------------------------------GET -------------------------------------------------------------------/// <summary>/// 获取服务器端的HTML代码/// </summary>/// <param name="url"></param>/// <returns></returns>public static string getHtmlCode(string url){var str = getHtmlCode_T(url);return str.Result;}/// <summary>/// 异步获取服务器端的HTML代码/// </summary>/// <param name="url"></param>/// <returns></returns>public static async Task<string> getHtmlCode_T(string url){try{var http = new HttpClient();http.Timeout = TimeSpan.FromSeconds(10);var result = await http.GetStringAsync(url).ConfigureAwait(continueOnCapturedContext: false);return result;}catch (Exception e){//EventLog.GetEventLogs(e.ToString);System.Diagnostics.Debug.WriteLine("错误信息在这儿:" + e.ToString());///console.write(e.ToString());Log.i(e.ToString());return "";}}}
}

因为涉及的技术知识点太多了,一时半会写不完,后续继续完善。需要源码的加。


                                         (未完待续)


http://www.ppmy.cn/news/47220.html

相关文章

交互式shell脚本编程2

当你在终端环境下安装新的软件时&#xff0c;你可以经常看到信息对话框弹出&#xff0c;需要你的输入&#xff0c;比如&#xff1a;RHEL/CentOS自带的setup&#xff0c;对话框的类型有密码箱、检查表、菜单等等。他们可以引导你以一种直观的方式输入必要的信息&#xff0c;使用…

精通 TensorFlow 1.x:16~19

原文&#xff1a;Mastering TensorFlow 1.x 协议&#xff1a;CC BY-NC-SA 4.0 译者&#xff1a;飞龙 本文来自【ApacheCN 深度学习 译文集】&#xff0c;采用译后编辑&#xff08;MTPE&#xff09;流程来尽可能提升效率。 不要担心自己的形象&#xff0c;只关心如何实现目标。—…

UDS统一诊断服务【五】诊断仪在线0X3E服务

文章目录 前言一、诊断仪在线服务介绍二、数据格式2.1&#xff0c;请求报文2.2&#xff0c;子功能2.3&#xff0c;响应报文 前言 本文介绍UDS统一诊断服务的0X3E服务&#xff0c;希望能对你有所帮助 一、诊断仪在线服务介绍 诊断仪在线服务比较简单&#xff0c;其功能就是告诉…

【JavaEE】File、InputStream和OutputStream的使用

1.File 在计算机中目录结构如下&#xff1a; 而File就表示一个目录或者一个普通文件。 File表示目录&#xff1a; File表示普通文件&#xff1a; 我们先来看File的构造方法&#xff1a; 构造器描述File(File parent, String child)根据父目录 孩子文件路径&#xff0c;创…

玄子Share - 精选三套 JavaScript 练手项目

玄子Share - 精选三套 JavaScript 练手项目 1. 50 Projects in 50 Days - HTML/CSS and JavaScript 50 天 50 个前端练手项目 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dCWO6V2G-1682076972984)(./assets/image-20230421192413713.png)] [外链…

VUE3的使用

文章目录 一、Vue3基础语法1、Vue开发准备2、Vue的模板语法3、条件渲染4、列表渲染5、事件处理6、表单输入绑定 二、Components组件1、组件基础2、组件交互3、自定义事件的组件交互4、组件生命周期5、引入第三方组件 三、网络请求及路由配置1、Axios网络请求2、Axios网络请求封…

【rustdesk】rust入门及 windows尝试编译

rustup 微软建议用vs code开发 下载了64位的版本: vs code 插件 rust-analyer 介绍Better TOML,用于更好的展示.toml文件Error Lens, 更好的获得错误展示 One Dark Pro, 非常好看的Vscode主题 CodeLLDB, debugger程序 安装

vue:生成二维码 qrcode、vue-qr(二维码中间可带logo)

一、方法一 qrcode qrcode - npm 1.1、安装 yarn add qrcode 1.2、页面引入 import QRCode from qrcode; 1.3、方法里边使用 getQRCodeUrl(){ QRCode.toDataURL(hello world,{color: {dark:"#010599FF",light:"#FFBF60FF"}}).then((url) > {// 获…

循环结构化命令小结

shell脚本编程系列 循环是编程不可或缺的一部分。bash shell提供了三种循环命令。 for命令允许遍历一系列的值&#xff0c;无论是在命令行中提供的&#xff0c;还是包含在变量中的&#xff0c;或是通过文件名通配符匹配获得的文件名和目录名。 for var in list docommands do…

MySQL索引数据结构入门

之前松哥写过一个 MySQL 系列&#xff0c;但是当时是基于 MySQL5.7 的&#xff0c;最近有空在看 MySQL8 的文档&#xff0c;发现和 MySQL5.7 相比还是有不少变化&#xff0c;同时 MySQL 又是小伙伴们在面试时一个非常重要的知识点&#xff0c;因此松哥打算最近再抽空和小伙伴们…

在React中使用setState修改数组的值时,为什么不能使用数组的可变方法(push、unshift等)? 但在vue中可以

一、为什么React中修改数组时不能使用数组的可变方法 在React中使用setState修改数组的值时&#xff0c;不推荐使用数组的可变方法&#xff08;如push、unshift等&#xff09;。这是因为React会对比新旧状态&#xff0c;在发现状态变化后&#xff0c;更新组件的渲染。但当你调…

TiDB实战篇-TiDB Cluster部署

简介 部署TiDB Cluster部署&#xff0c;熟系集群的基础操作。 集群规划 机器拓扑 3pd,3tikv,1tidb_server.1tiflash,监控。 192.168.66.10192.168.66.20192.168.66.21 pd_servers tikv_servers tidb_servers tiflash_servers pd_servers tikv_servers monitoring_servers…

SSH升级

升级openssh版本 一、安装telnet远程管理主机1、检查是否安装telnet2、安装telnet服务 二、下载所需的安装包1、下载openssl、openssh、zlib安装包2、安装所需的相关软件3、备份原来的数据4、复制文件到/usr/local/bin/下增加执行权限 一、安装telnet远程管理主机 1、检查是否…

用SQL语句操作Oracle数据库--数据查询详解(下篇)

数据查询是数据库的核心操作。上一篇文章我们介绍了单表查询操作&#xff0c;本文将继续介绍另一种重要的查询类型—[ 多表查询 ]&#xff08;涉及多个表的数据查询&#xff09;。 本文我们将使用以下三个表来进行查询操作&#xff1a; TABLE1: S(学生基本信息表) TABLE2: C(…

设置Pycharm的背景颜色(样式)、图片

目录 一、效果 二、背景图片 三、背景颜色 一、效果 二、背景图片 1.打开Pycharm中的File-Settings 2.点击Appearance & Behavior中的Appearance&#xff0c;然后点击Bcakground Image &#xff08;图中已标明顺序&#xff09; 3.找到图片并选中。 &#xff08;图中已…

需要买apple pencil吗?ipad第三方电容笔了解下

第一款ipad早在诞生于十年前&#xff0c;并被作为一款平板电脑使用&#xff0c;其性能十分出色。随着IPAD的不断更新换代&#xff0c;IPAD已经被越来越多的人接受了。其中&#xff0c;iPad的附属配件起到了很大的作用&#xff0c;就像今天要介绍的电容笔&#xff0c;它是我们进…

ARM rootfs、linuxrc 的简单制作

一、nfs 方式启动自制简易文件夹形式的rootfs 1、什么是nfs (1) nfs 是一种网络通讯协议&#xff0c;由服务器和客户端构成。 (2) nfs 的作用。利用 nfs 协议&#xff0c;可以做出很多直接性的应用&#xff0c;我们这里使用 nfs 主要是做 rootfs 挂载。 开发板中运行 kerne…

【Linux】NanoPi-NEO2外接spi-lcd

这是目录 一、显示接口1.1、LCD接口1.2、核心板接口 二、添加驱动2.1、确认驱动型号2.2、添加驱动 三、测试四、附加4.1、交叉编译器安装4.2、内核和module编译4.3、扩展rootfs大小 本文使用环境&#xff1a; 电脑&#xff1a;Ubuntu 18.04.5 LTS 开发板&#xff1a;NanoPi-NEO…

Microsoft Power Apps部署方案

目录 前言 一、准备条件 二、Power Apps环境部署 三、应用程序部署 四、最佳实践 总结

[异常]java常见异常

Java.io.NullPointerException null 空的&#xff0c;不存在的NullPointer 空指针 空指针异常&#xff0c;该异常出现在我们操作某个对象的属性或方法时&#xff0c;如果该对象是null时引发。 String str null; str.length();//空指针异常 上述代码中引用类型变量str的值为…