使用背景
最近在使用c#(dotnetcore)编写一些爬虫进行实践,在模拟网站请求的时候,往往在请求参数里含有一个根据请求内容实时生成的token,通过对前端js文件的调用,找到了用来生成token的js方法,但是将js代码翻译成c#代码有点太费劲费时,于是想要找到一个这样的框架,可以直接从c#调用js的方法并返回值。
尝试的框架
我前前后后尝试了好几个框架,大致分为两类1、浏览器内核/无头浏览器,2、js引擎/框架。主要区别是一个是模拟浏览器去请求一个完整的网页,另外的则是单单去计算调用一个纯粹的js方法,我更倾向于后者。
浏览器内核/无头浏览器
js引擎/框架
- Microsoft.AspNetCore.NodeServices (接口方法已标注过时,后续有可能不会在.net 新版本中支持)
- Microsoft.ClearScript (主要是使用v8引擎)
- JavaScriptEngineSwitcher.ChakraCore (使用的ChakraCore引擎)
最后根据项目需求选择了JavaScriptEngineSwitcher.ChakraCore,因为支持在linux平台运行,在Windows上运行的时候需要额外引用JavaScriptEngineSwitcher.ChakraCore.Native.win-x64,在linux上运行时需要额外引用JavaScriptEngineSwitcher.ChakraCore.Native.linux-x64,这两个包可以同时引用。
PuppeteerSharp时puppeteer的c#版本,由于我在使用时好像发现他在运行时需要额外下载内容,且下载失败,故没有做仔细研究便弃用。
Microsoft.AspNetCore.NodeServices,功能满足需求,可以在linux上运行,但由于在使用Systemd运行应用时无法调用Nodejs,且暂未找到问题原因,且方法接口等已经标注过时,考虑以后的维护升级考虑,只作备选方案。
Microsoft.ClearScript.V8功能满足需求,但是不满足在linux上运行,故不选择。
代码示例
注:以下代码实例均是使用目标框架 .Net Core 3.1,先在Windows平台测试,后在Linux下测试,使用到的框架应该也均有 .Net Framework 的版本支持,但对于此点并没有做验证。测试做的并不完全,如有疏漏或错误欢迎指正。
Microsoft.AspNetCore.NodeServices
先决条件:Nodejs环境,设置环境变量NODE_PATH,脚本需要按照Nodejs的模块导出的格式将方法写成导出的形式。
c#
using Microsoft.AspNetCore.NodeServices;
using Microsoft.Extensions.DependencyInjection;
namespace NodeServicesDemo
{
public class Demo
{
[Obsolete]
private readonly INodeServices _nodeServices;
private IServiceCollection _nodeServiceCollections = new ServiceCollection();
[Obsolete]
public XieChengScrapyService(IServiceProvider services)
{
_nodeServiceCollections.AddNodeServices(options =>
{
options.NodeInstanceOutputLogger = loggerFactory.CreateLogger("nodeservices");
options.ProjectPath = Environment.CurrentDirectory; // 设置项目为挡墙项目目录
});
var sp = _nodeServiceCollections.BuildServiceProvider();
_nodeServices = sp.GetRequiredService<INodeServices>();
}
public async void Execute()
{
var result = await _nodeServices.InvokeAsync<string>("./Scripts/demo", input); // 要调用的模块的相对路径,参数
}
}
}
demo.js(Nodejs模块)
function m(t) {
return p(v(t))
}
module.exports = function (callback, t){
var output = m(t);
callback(null, output);
}
Microsoft.ClearScript
引入Nuget包,Microsoft.ClearScript
demo.js(原生javascript)
function m(t, e, r) {
p(v(t))
}
注:除了NodeServices中,其他要调用的js文件均为以此为示例,后面不再复述。
引入Nuget包,Microsoft.ClearScript
using Microsoft.ClearScript.JavaScript;
using Microsoft.ClearScript.V8;
初始化
using (var engine = new V8ScriptEngine())
{
engine.DocumentSettings.AccessFlags = Microsoft.ClearScript.DocumentAccessFlags.EnableFileLoading;
engine.DefaultAccess = Microsoft.ClearScript.ScriptAccess.Full; // 这两行是为了允许加载js文件
// do something
}
调用脚本有多种方案。
方案一:调用engine.ComplieDocument方法直接加载js文件,然后调用engine.Execute将引入的脚本执行一遍,这样后面就可以调用js方法,m就是js的方法名,调用格式与js相同。
V8Script script = engine.CompileDocument(ScriptFilePath); // 载入并编译js文件, 然后Execute, 就可以直接调用。
engine.Execute(script);
var result = engine.Script.m("SHAURCOnewayduew&^%5d54nc'KH");
方案二:将要导入的js方法的代码读出来,然后执行一遍,再调用要执行的js方法
string scriptContent = string.Empty;
using(FileStream fs = new FileStream(ScriptFilePath, FileMode.Open, FileAccess.Read))
{
using(StreamReader sr = new StreamReader(fs))
{
scriptContent = sr.ReadToEnd().Replace("
", "");
}
}
engine.Execute(scriptContent); // 取得脚本里的所有内容,Execute一下,然后,调用engine.Script.func(x,y)执行一下。
var result = engine.Script.m("SHAURCOnewayduew&^%5d54nc'KH");
直接调用执行调用的方法的js代码也是可以的
string scriptContent = string.Empty;
using(FileStream fs = new FileStream(ScriptFilePath, FileMode.Open, FileAccess.Read))
{
using(StreamReader sr = new StreamReader(fs))
{
scriptContent = sr.ReadToEnd().Replace("
", "");
}
}
scriptContent += "m("SHAURCOnewayduew&^%5d54nc'KH");"; // 在js代码的结尾加上执行的代码
engine.Execute(scriptContent); // 取得脚本里的所有内容,Execute一下,然后,调用engine.Script.func(x,y)执行一下。
var result = engine.Script.m("SHAURCOnewayduew&^%5d54nc'KH");
特殊情况,调用js全局方法,就是调用js的默认的那些方法
var result = engine.Invoke("encodeURIComponent", "SHAURCOnewayduew&^%5d54nc'KH"); //只能调用全局方法,如encodeURIComponent
JavaScriptEngineSwitcher.ChakraCore
引入Nuget包,JavaScriptEngineSwitcher.ChakraCore,JavaScriptEngineSwitcher.ChakraCore.Native.win-x64,JavaScriptEngineSwitcher.ChakraCore.Native.linux-x64
using JavaScriptEngineSwitcher.ChakraCore;
using JavaScriptEngineSwitcher.Core;
使用,同样是先把js文件执行一遍,然后再去调用要使用的方法。
string ScriptPath = Path.Combine(Directory.GetCurrentDirectory(), "Scripts", "demo.js");
var switcher = JsEngineSwitcher.Current;
switcher.EngineFactories.Add(new ChakraCoreJsEngineFactory());
switcher.DefaultEngineName = ChakraCoreJsEngine.EngineName;
IJsEngine engine = JsEngineSwitcher.Current.CreateDefaultEngine();
engine.ExecuteFile(ScriptPath, Encoding.UTF8);
string result = engine.CallFunction<string>("m", "SHAURCOnewayduew&^%5d54nc'KH");
参考资料:
webmote-org/netcore-javascript
microsoft/ChakraCore -- github
Microsoft/ClearScript -- V8ScriptEngine Class