js请求结果拦截机器_CefSharp请求资源拦截及自定义处理

前言

在CefSharp中,我们不仅可以使用Chromium浏览器内核,还可以通过Cef暴露出来的各种Handler来实现我们自己的资源请求处理。

什么是资源请求呢?简单来说,就是前端页面在加载的过程中,请求的各种文本(js、css以及html)。在以Chromium内核的浏览器上,我们可以使用浏览器为我们提供的开发者工具来检查每一次页面加载发生的请求。

准备

鉴于本文的重心是了解CefSharp的资源拦截处理,所以我们不讨论前端的开发以及客户端嵌入CefSharp组件的细节。我们首先完成一个基本的嵌入CefSharp的WinForm程序:该程序界面如下,拥有一个地址输入栏和一个显示网页的Panel:

18e45faa52f847bf3375378645c59354.png

并且编写一个极其简单的页面,该页面会请求1个js资源和1个css资源:

demo:- index.html- test1.js- test1.css

这几个文件的代码都十分简单:

body
{background-color: aqua
}
function myFunc() {return 'test1 js file';
}
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head><meta charset="utf-8" /><title>Home</title><!-- 如下记载js、css资源 --><script type="text/javascript" src="test1.js"></script><link type="text/css" rel="stylesheet" href="test1.css"/>
</head>
<body>
<h1>Resource Intercept Example</h1>
<h2 id="result"></h2>
<script>// 调用test1.js中的myFuncdocument.getElementById('result').innerHTML = myFunc();
</script>
</body>
</html>

代码很简单,效果也很容易知道,页面加载后,页面背景色为aqua,页面上会显示文本“test1 js file”。同时,当我们使用开发工具,刷新页面,能够看到对应的资源加载:

bbe592c4c527bbe61d98defab595ed88.png

CefSharp资源拦截及自定义处理

完成上述准备后,我们进入正文:资源拦截及自定义处理。首先我们需要对目标的理解达成一致,资源拦截是指我们能够检测到上图中的html、js还有css的资源请求事件,在接下来的Example中,因为我们是使用的客户端程序,所以会在请求的过程中弹出提示;自定义处理是指,在完成拦截提示后,我们还能够替换这些资源,这里我们设定完成拦截后,可以把js和css换为我们想要另外的文件:test2.js和test2.css:

function myFunc() {return 'test2 js file';
}
body
{background-color: beige
}

即我们希望拦截并替换后,页面上的文字不再是之前的,而是“test2 js file”,页面的背景色是beige。

IRequestHandler

在CefSharp中,要想对请求进行拦截处理,最为核心的Handler就是IRequestHandler这个接口,查看官方的源码,会发现里面有数个方法的定义,通过阅读官方的summary,我们可以聚焦到如下的两个定义(注释本人进行了删减):

/// <summary>
/// Called before browser navigation.
/// 译:在浏览器导航前调用
/// If the navigation is allowed <see cref="E:CefSharp.IWebBrowser.FrameLoadStart" /> and <see cref="E:CefSharp.IWebBrowser.FrameLoadEnd" />
/// will be called. If the navigation is canceled <see cref="E:CefSharp.IWebBrowser.LoadError" /> will be called with an ErrorCode
/// value of <see cref="F:CefSharp.CefErrorCode.Aborted" />.
/// </summary>
bool OnBeforeBrowse(IWebBrowser chromiumWebBrowser,IBrowser browser,IFrame frame,IRequest request,bool userGesture,bool isRedirect);/// <summary>
/// Called on the CEF IO thread before a resource request is initiated.
/// 在一个资源请求初始化前在CEF IO线程上调用
/// </summary>
IResourceRequestHandler GetResourceRequestHandler(IWebBrowser chromiumWebBrowser,IBrowser browser,IFrame frame,IRequest request,bool isNavigation,bool isDownload,string requestInitiator,ref bool disableDefaultHandling);

于是,我们继承一个默认的名为RequestHandler的类(请区分DefaultRequestHandler),只重写上述的两个方法。

public class MyRequestHandler : RequestHandler{protected override bool OnBeforeBrowse(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool userGesture,bool isRedirect){// 先调用基类的实现,断点调试return base.OnBeforeBrowse(chromiumWebBrowser, browser, frame, request, userGesture, isRedirect);}protected override IResourceRequestHandler GetResourceRequestHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame,IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling){// 先调用基类的实现,断点调试return base.GetResourceRequestHandler(chromiumWebBrowser, browser, frame, request, isNavigation, isDownload, requestInitiator, ref disableDefaultHandling);}}

然后完成对该Handler的注册:

this._webBrowser = new ChromiumWebBrowser(string.Empty){RequestHandler = new MyRequestHandler()};

打上断点,开始访问我们的Example:index.html。这里会发现,OnBeforeBrowse调用了一次,而GetResourceRequestHandler会调用3次。检查OnBeforeBrowse中的request参数内容,是一次主页的请求,而GetResourceRequestHandler中的3次分别是:主页html资源、test1.js和test1.css。

c4af15bed1e7af90681f12408e37e47b.png

4df5744d7c196082b22ab321e6a55efb.png

结合官方注释和调试的结果,我们可以得出结论:要进行导航的拦截,我们可以重写OnBeforeBrowse方法,要想进行资源的拦截,我们需要实现自己的ResourceRequestHandler。

IResourceRequestHandler

查看IResourceRequestHandler的定义,我们再次聚焦一个函数定义:

/// <summary>
/// Called on the CEF IO thread before a resource is loaded. To specify a handler for the resource return a <see cref="T:CefSharp.IResourceHandler" /> object
/// </summary>
/// <returns>To allow the resource to load using the default network loader return null otherwise return an instance of <see cref="T:CefSharp.IResourceHandler" /> with a valid stream</returns>
IResourceHandler GetResourceHandler(IWebBrowser chromiumWebBrowser,IBrowser browser,IFrame frame,IRequest request);

该定义从注释可以看出,如果实现返回null,那么Cef会使用默认的网络加载器来发起请求,或者我们可以返回一个自定义的资源处理器ResourceHandler来处理一个合法的数据流(Stream)。也就是说,对于资源的处理,要想实现自定义的处理(不是拦截,拦截到目前为止我们可以在上述的两个Handler中进行处理)我们还需要实现一个IResourceHandler接口的实例,并在GetResourceHandler处进行返回,Cef才会在进行处理的时候使用我们的Handler。所以在GetResourceHandler中,我们进行资源的判断,如果是想要替换的资源,我们就使用WinForm提供的OpenFileDialog来选择本地的js或是css文件,并传给我们自定义的ResourceHandler,如果不是想要拦截的资源或是用户未选择任何的文件就走默认的:

public class MyResourceRequestHandler : ResourceRequestHandler
{protected override IResourceHandler GetResourceHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request){if (request.Url.EndsWith("test1.js") || request.Url.EndsWith("test1.css")){MessageBox.Show($@"资源拦截:{request.Url}");string type = request.Url.EndsWith(".js") ? "js" : "css"; // 这里简单判断js还是css,不过多编写string fileName = null;using (OpenFileDialog openFileDialog = new OpenFileDialog()){openFileDialog.Filter = $@"{type}文件|*.{type}"; // 过滤openFileDialog.Multiselect = true;if (openFileDialog.ShowDialog() == DialogResult.OK){fileName = openFileDialog.FileName;}}if (string.IsNullOrWhiteSpace(fileName)){// 没有选择文件,还是走默认的Handlerreturn base.GetResourceHandler(chromiumWebBrowser, browser, frame, request);}// 否则使用选择的资源返回return new MyResourceHandler(fileName);}return base.GetResourceHandler(chromiumWebBrowser, browser, frame, request);}
}

IResourceHandler

根据上文,我们进一步探究IResourceHandler,对该Handler,官方有一个默认的实现:RequestHandler,该Handler通过阅读源码可以知道是网络加载的Handler,这里为了实现我们自定义拦截策略,我们最好单独实现自己的IResourceHandler。对于该接口,有如下的注释:

/// <summary>
/// Class used to implement a custom resource handler. The methods of this class will always be called on the CEF IO thread.
/// Blocking the CEF IO thread will adversely affect browser performance. We suggest you execute your code in a Task (or similar).
/// To implement async handling, spawn a new Task (or similar), keep a reference to the callback. When you have a
/// fully populated stream, execute the callback. Once the callback Executes, GetResponseHeaders will be called where you
/// can modify the response including headers, or even redirect to a new Url. Set your responseLength and headers
/// Populate the dataOut stream in ReadResponse. For those looking for a sample implementation or upgrading from
/// a previous version <see cref="T:CefSharp.ResourceHandler" />. For those upgrading, inherit from ResourceHandler instead of IResourceHandler
/// add the override keywoard to existing methods e.g. ProcessRequestAsync.
/// </summary>
public interface IResourceHandler : IDisposable
{ ... }

该类的注释意思大致为:我们可以通过实现该接口来实现自定义资源的处理类。该类中的方法总是在CEF的IO线程中调用。然而,阻塞CEF IO线程将会不利于浏览器的性能。所以官方建议开发者通过把自己的处理代码放在Task(或是类似的异步编程框架)中异步执行,然后在完成或取消(失败)时,在异步中调用callback对应的操作函数(continue、cancel等方法)。当你拥有一个完全填充(fully populated)好了的Stream的时候,再执行callback(这一步对应Open方法)。一旦callback执行了,GetResponseHeaders这个方法将会调用,于是你可以在这个方法里面对Reponse的内容包括headers进行修改,或者甚至是重定向到一个新的Url。设置你自己的reponseLength和headers。接下来,通过在ReadResponse(实际上即将作废,而是Read)函数中,实现并填充dataOut这个Stream。最终CEF会对该Stream进行读取数据,获得资源数据。

事实上,该Handler的实现可以有很多花样,这里我们实现一个最简单的。

Dispose

对于通常进行资源释放的Dispose,因为我们这里只是一个Demo,所以暂时留空。

Open(ProcessRequest)

官方注释指出,ProcessRequest将会在不久的将来弃用,改为Open。所以ProcessRequest我们直接返回true。对于Open方法,其注释告诉我们:

  • 要想要立刻进行资源处理(同步),请设置handleRequest参数为true,并返回true
  • 决定稍后再进行资源的处理(异步),设置handleRequest为false,并调用callback对应的continue和cancel方法来让请求处理继续还是取消,并且当前Open返回false。
  • 要立刻取消资源的处理,设置handleRequest为true,并返回false。

也就是说,handleRequest的true或false决定是同步还是异步处理。若同步,则Cef会立刻通过Open的返回值true或false来决定后续继续进行还是取消。若为异步,则Cef会通过异步的方式来检查callback的调用情况(这里的callback实际上是要我们创建Task回调触发的)。这里我们选择同步的方式(选择异步也没有问题)编写如下的代码:

public bool Open(IRequest request, out bool handleRequest, ICallback callback)
{handleRequest = true;return true;
}

GetResponseHeaders

在上小节中我们已经完成了对资源数据的入口(Open)的分析。既然我们已经告诉了Cef我们准备开始进行资源请求的处理了,那么接下来我们显然需要着手进行资源的处理。根据前面的概要注释,我们需要实现GetResponseHeaders方法,因为这是资源处理的第二步。该方法的注释如下:

/// <summary>
/// Retrieve response header information. If the response length is not known
/// set <paramref name="responseLength" /> to -1 and ReadResponse() will be called until it
/// returns false. If the response length is known set <paramref name="responseLength" />
/// to a positive value and ReadResponse() will be called until it returns
/// false or the specified number of bytes have been read.
/// 
/// It is also possible to set <paramref name="response" /> to a redirect http status code
/// and pass the new URL via a Location header. Likewise with <paramref name="redirectUrl" /> it
/// is valid to set a relative or fully qualified URL as the Location header
/// value. If an error occured while setting up the request you can call
/// <see cref="P:CefSharp.IResponse.ErrorCode" /> on <paramref name="response" /> to indicate the error condition.
/// </summary>
void GetResponseHeaders(IResponse response, out long responseLength, out string redirectUrl);

Summary翻译解释如下:获取响应头信息。如果响应的数据长度未知,则设置responseLength-1,然后CEF会一直调用ReadResponse(即将废除,实际上是Read方法)直到该Read方法返回false。如果响应数据的长度是已知的,可以直接设置responseLength长度为一个正数,然后ReadResponseRead)将会一直调用,直到该Read方法返回false或者在已经读取的数据的字节长度达到了设置的responseLength的值。当然你也可以通过设置response.StatusCode值为重定向的值(30x)以及redirectUrl为对应的重定向Url来实现资源重定向。

在本文中,我们采取简单的方式:直接返回资源的长度,然后交给下一步的Read方法来进行真正的资源处理。在该步骤中,我们编写获取本地文件字节数据来实现js和css文件的本地加载,并且将该数据保存在该ResourceHanlder实例私有变量中。

public void GetResponseHeaders(IResponse response, out long responseLength, out string redirectUrl)
{using (FileStream fileStream = new FileStream(this._localResourceFileName, FileMode.Open, FileAccess.Read)){using (BinaryReader binaryReader = new BinaryReader(fileStream)){long length = fileStream.Length;this._localResourceData = new byte[length];// 读取文件中的内容并保存到私有变量字节数组中binaryReader.Read(this._localResourceData, 0, this._localResourceData.Length);}}responseLength = this._localResourceData.LongLength;redirectUrl = null;
}

Read

该方法的定义和注释如下:

/// <summary>
/// Read response data. If data is available immediately copy up to
/// dataOut.Length bytes into dataOut, set bytesRead to the number of
/// bytes copied, and return true. To read the data at a later time keep a
/// pointer to dataOut, set bytesRead to 0, return true and execute
/// callback when the data is available (dataOut will remain valid until
/// the callback is executed). To indicate response completion set bytesRead
/// to 0 and return false. To indicate failure set bytesRead to &lt; 0 (e.g. -2
/// for ERR_FAILED) and return false. This method will be called in sequence
/// but not from a dedicated thread.
/// 
/// For backwards compatibility set bytesRead to -1 and return false and the ReadResponse method will be called.
/// </summary>
bool Read(Stream dataOut, out int bytesRead, IResourceReadCallback callback);

Summary的翻译大致为:读取响应数据。如果数据是可以立即获得的,那么可以直接将dataOut.Length长度的字节数据拷贝到dataOut这个流中,然后设置bytesRead的值为拷贝的数据字节长度值,最后再返回true。如果开发者希望继续持有dataOut的引用(注释是pointer指针,但是个人觉得这里写为指向该dataOut的引用更好)然后在稍后填充该数据流,那么可以设置bytesRead0,通过异步方式在数据准备好的时候执行callback的操作函数,然后立刻返回true。(dataOut这个流会一直保持不被释放直到callback被调用为止)。为了让CEF知道当前的响应数据已经填充完毕,需要设置bytesRead0然后返回false。要想让CEF知道响应失败,需要设置bytesRead为一个小于零的数(例如ERR_FAILED: -2),然后返回false。这个方法将会依次调用但不是在一个专有线程。

根据上述的注释,总结如下:

  • bytesRead > 0,return true:填充了数据,但Read还会被调用
  • bytesRead = 0,return false:数据填充完毕,当前为最后一次调用
  • bytesRead < 0,return false:出错,当前为最后一次调用
  • bytesRead = 0,return true:CEF不会释放dataOut流,在异步调用中准备好数据后调用callback

针对本例,我们增加一个该类的私有变量_dataReadCount用于标识已读的资源数据字节量并在构造函数中初始化为0。

每次在Read中进行读取的时候,首先检查剩余待读取字节数this._localResourceData.LongLength - this._dataReadCount,如果该值为零,则表明已经将所有的数据通过dataOut拷贝给了外围,此时设置bytesRead为0,直接返回false;若剩余值大于0,则需要继续进行拷贝操作,但需要注意的是dataOut并不是一个无限大的流,而是一个类似于缓存的流,它的Length值为2^16 = 65536,所以我们需要设置bytesRead来让外围知道我们实际在这个流中放了多少字节的数据。同时在使用Stream.WriteAPI的时候,需要设置正确的offset和count。

最终,Read的实现如下:

public bool Read(Stream dataOut, out int bytesRead, IResourceReadCallback callback)
{int leftToRead = this._localResourceData.Length - this._dataReadCount;if (leftToRead == 0){bytesRead = 0;return false;}int needRead = Math.Min((int)dataOut.Length, leftToRead); // 最大为dataOut.LenghtdataOut.Write(this._localResourceData, this._dataReadCount, needRead);this._dataReadCount += needRead;bytesRead = needRead;return true;
}

其他的几个方法

对于Cancel和Skip方法,在本例不会调用,所以这里使用默认实现,不进行讨论,感兴趣的伙伴可以自己去研究。

最终效果

通过上文的代码设计和编写,我们最终完成了一个简单的资源拦截及自定义处理的Example。首先我们在不进行资源拦截的情况下,加载我们的web页面:

8d1103288d12a5e5f0edd51c97af62d9.png

可以看到界面中呈现“test1 js file”的字样以及背景色为海蓝色。接下来我们开启资源拦截,再次加载页面,在加载过程中会有对应资源的拦截时的弹窗以及我们需要选择我们自定义的资源文件:

045fcfdbe0a2cea63a52f3f7ab3c4189.png
拦截到JS

045fcfdbe0a2cea63a52f3f7ab3c4189.png
手动更改加载的JS

3c6c29040661e2ff6c83dff70d75a6e8.png
拦截到CSS

b9b0963ddc0573fc13d25dfbff7c35ea.png
手动更改加载的CSS

完成处理后,得到如下的显示页面:

7563cc552f7897369869b1127607aca6.png

源码

本Example的源码已经开源在Github上,整个Demo较为简单,主要是对本文的验证

CefSharpResourceInterceptExample​github.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/299336.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ListView

2019独角兽企业重金招聘Python工程师标准>>> ListView总结(多选框ListViiew,动态加载&#xff0c;多线程更新ListView中的进度条) convertView缓存及使用 ListView 更高效的使用 Adapter ListView中convertView和ViewHolder的工作原理 convertView&setTag方…

原来,程序的世界远比我想象的精彩

原来&#xff0c;程序的世界远比我想象的精彩 注&#xff1a;本文转载自http://www.cnblogs.com/dingxue/archive/2008/12/23/1360908.html&#xff0c;作者丁学。文中所有的 Pxx 指得是《Erlang程序设计》一书中的页码&#xff0c;书的信息放在文章最后。人都说无知者无畏&am…

【MSLearn 学习模块】Hi ! ⼀起来学Python

随着大数据、人工智能、物联网的兴起&#xff0c;Python越来越受到大家的关注。不论你是⼀位学生&#xff0c;是一位传统的码农&#xff0c;还是非计算机领域的从业人员都离不开Python。当然大家的角度不⼀样&#xff0c;作为学生是以学为主&#xff0c;码农是以用为主&#xf…

有趣的12张数学原理动图,令人舒心却又伤脑!你看懂几个?

全世界只有3.14 % 的人关注了爆炸吧知识下面的12张数学动图你能看懂几个&#xff0c;反正知识君是都看懂了。1.被简单证明的勾股定理给三角形加上一点厚度。从面积问题&#xff0c;跳转到了具象的体积问题。2.勾股定理的面积证明法Its a long story……慢慢看。3.周长和直径的π…

typora插入代码设置_Typora基本功能介绍

Typora是什么&#xff1f;提起Typora&#xff0c;如果是使用过Markdown的人&#xff0c;应该就不太陌生&#xff0c;Typora 是一款支持实时预览的 Markdown 文本编辑器。它有 OS X、Windows、Linux 三个平台的版本&#xff0c;并且由于仍在测试中&#xff0c;是完全免费的。在T…

【DotNetMLLearn】.NET Core人工智能系列-概述

.NETer大家好&#xff0c;今天为大家送上.NET Core 下如何完成人工智能应用的系列&#xff0c;希望给.NETer进入人工智能领域提供一个指引。.NET Core已经是一个全场景应用的技术栈方案&#xff0c;对于每一个领域都有支持。自从微软在开源发力后&#xff0c;.NET Core的社区能…

fanuc机器人控制柜接线_FANUC涂胶标定参考

FANUC涂胶标定参考1、进入SETUP界面2、选择DISPENCE界面3、选择15 flow rate control,按F2 detail进入4、进入标定界面此画面定义含义如下&#xff1a;(1)为涂胶标定单位,TCPP BEAD WIDTH宽度为基准(2)为 输入理想涂胶宽度,desired flow rate(3)为定义出胶轨迹sample program(一…

硬盘坏道修复白皮书

常识&#xff1a;硬盘坏道分类 - 不同坏道分仔细由于硬盘采用磁介质来存储数据&#xff0c;在经历长时间的使用或者使用不当之后&#xff0c;难免会发生一些问题&#xff0c;也就是我们通常所说的产生“坏道”&#xff0c;当然这种坏道有可能 是软件的错误&#xff0c;也有可能…

糟糕!复工后,最让人担心的问题又又又来了!

▲ 点击查看你复工多久了&#xff1f;当我们在家宅着的时候&#xff0c;一天到晚都想着什么时候才能出去。然而现在公司一通知复工&#xff0c;其实心里还是有点犯怵。先不说通勤了&#xff0c;上班吃饭就有又成为大家绕不开的问题。现在虽然可以在外面点外卖&#xff0c;但说实…

Myeclipse中Tomcat的两种部署方式

一、在Myeclipse软件中部署 1、 在Myeclipse中&#xff0c;创建好工程后&#xff0c;在Myeclipse菜单栏中选择 Windows -> Preferences -> Myeclipse -> Tomcat5&#xff0c;选择"Enable"&#xff1b;Tomact Home Directory要选择你机器上Tomcat所安装的目录…

配置yml_dctc:生成docker-compose.yml配置的工具

有时候&#xff0c;我们需要想知道docker容器的运行参数、环境变量等配置&#xff0c;以便容器以后被删掉之后&#xff0c;可以重新运行起来。特别是在测试环境&#xff0c;有些容器可能就是一个docker run就运行起来了。或者随便找一个目录丢了一个docker-compose.yml运行起来…

Silverlight 2 应用程序部署到任意HTML页面

Silverlight 2 应用程序部署到任意HTML页面&#xff0c;可以采用ActiveX的方法&#xff0c;在页面中放入object标签&#xff0c;设置相应的属性即可。使用 object 元素&#xff0c;可以用与所有受支持浏览器兼容的方式在 HTML 中嵌入和配置 Silverlight 插件。具体可参看&#…

对于“知识”,我们存在哪些误解?

全世界只有3.14 % 的人关注了爆炸吧知识对于不少用户来说&#xff0c;微信已经成为了获取新资讯的重要途径之一。如何保证自己找到「值得看」的内容&#xff1f;每天更新的文章这么多&#xff0c;哪些才值得你看呢&#xff1f;今天就为大家推荐一些颜值与才华并存的好评公众号。…

一个搜索需求搞垮微服务

“编程十几年&#xff0c;我只希望能把技术讲明白而已&#xff0c;当然能帮到各位更是深感荣幸“假如你是一名架构师或者技术领导人&#xff0c;一定要深深的知道&#xff0c;没有完美的技术方案&#xff0c;要不然编程技术早就万佛归一。微服务作为近几年新兴的技术概念&#…

Mybatis批量更新转

Mybatis批量更新 批量操作就不进行赘述了。减少服务器与数据库之间的交互。网上有很多关于批量插入还有批量删除的帖子。但是批量更新却没有详细的解决方案。 实现目标 这里主要讲的是1张table中。根据不同的id值&#xff0c;来update不同的property。 数据表&#xff1a;1张。…

45组超炫数学动图,原来当年我如此牛!

全世界只有3.14 % 的人关注了爆炸吧知识想当年我们在数学的海洋里如饥似渴的遨游&#xff0c;什么公式、字母和图形&#xff0c;接招拆招、扫平了无数数学难题&#xff0c;八面威风&#xff0c;牛的不行不行的&#xff01;&#xff01;&#xff01;下面的45组数学动图&#xff…

这样的书值得你读

本文来自豆瓣网网友评论 1.我正在做一个情报分析系统&#xff0c;是一个决策交易系统的基础。里面的很多东西正是我需要的。我觉得很好 我自己想的话 浪费时间。 2.一位垂直搜索开发人员来信说到&#xff1a;我非常有幸买了你们出的《自己动手写搜索引擎》&#xff0c;谢谢你们…

.NET生态现状:超一半 .NET开发者使用C# 8、.NET Framework使用量减少

文 | 局长出品 | OSC开源社区&#xff08;ID&#xff1a;oschina2013&#xff09;JetBrains 上个月发布了 2021 年开发者生态的调查报告&#xff0c;这份报告详细介绍了开发领域的最新趋势&#xff0c;以及有关工具、技术、编程语言和编程世界的其他信息。本文会重点介绍这份报…

MFC中使用SDL播放音频没有声音的解决方法

2019独角兽企业重金招聘Python工程师标准>>> 本文所说的音频是指的纯音频&#xff0c;不包含视频的那种。 在控制台中使用SDL播放音频&#xff0c;一般情况下不会有问题。 但是在MFC中使用SDL播放音频的时候&#xff0c;会出现没有声音的情况。经过长时间探索&#…

为啥非要用Python?Excel不香吗?

近日&#xff0c;PYPL发布了2月编程语言指数榜&#xff01;在这场排行较量当中&#xff0c;Python牢牢占据榜首位置&#xff0c;并与排名第二的Java拉开高达10%的距离&#xff01;PYPL 是非常流行的参考指标&#xff0c;其榜单数据的排名均是根据榜单对象在 Google 上相关的搜索…