前言

在进行某些爬虫任务的时候,我们经常会遇到仅用Http协议难以攻破的情况,比如协议中带有加密参数,破解需要花费大量时间,那这时候就会用Selenium去模拟浏览器进行页面上的元素抓取

大多数情况下我们用Selenium只是爬取一下页面上可见的元素信息或者做一些模拟人工的操作,但页面可见元素的数据字段毕竟有限,有许多有用的字段隐藏在接口响应中的,但是要如何拿到接口响应内容呢?

在网上搜索Selenium如何获取Chrome中Network数据包响应结果,大多数的文章都是Python或者Java,C#的资源少之又少,虽然知道原理,但每个语言之间SDK代码实现相差很大,C#的SDK真的有点魔改,需要自己慢慢摸索

探索

通过寻找资料,大致就2种方案

方案1:通过Selenium指定本地一个代理去截取所有请求,类似于常见抓包工具的原理,但是C#是没有这种插件,也有可能是我没找到,比如Python和Java有一个叫Browsermob-Proxy的插件,可以和Selenium深度结合实现代理抓包。我利用FiddlerCore做了一个本地代理工具,但是并不好用,不能和Selenium进行深度绑定使用,会导致Selenium爬取过程和请求截取是异步进行的,强行用代码实现同步又很难受,达不到我想要的要求

方案2:Selenium通过chromedriver开启浏览器的性能日志功能,记录类型为Performance的日志,该功能在Selenium中叫做 PerformanceLoggingPreferences。网页加载完成后,可以通过Selenium拿到浏览器Performance Logs摘要信息,再利用Log中的RequestId调用Chrome CDP命令去浏览器端获取日志的完整内容。Selenium封装的CDP,本质上还是Http请求,只是带着驱动窗口的SessionId和Chrome的API做交互

方案1参考资料:

https://blog.csdn.net/qq_32502511/article/details/101536325  (Python + Browsermob-Proxy)

https://blog.csdn.net/fontcolor0/article/details/103297635/  (Java + Browsermob-Proxy)

https://www.cnblogs.com/airoot/articles/14888284.html  (C# + FiddlerCore)

方案2参考资料:

https://chromedevtools.github.io/devtools-protocol/  (Chrome DevTools Protocol 介绍)

https://www.jianshu.com/p/615e3c0140a5  (Python + 开启PerfLoggingPref)

https://blog.csdn.net/weixin_49855251/article/details/112281901  (不同日志类型的介绍)

https://blog.csdn.net/bigcarp/article/details/115065730  (Java + 原生CDP协议获取日志内容)

上手

启用Logging:

C#伪代码示例:

首先需要安装最新的Nuget包:OpenQA.Selenium

var option = new ChromeOptions();
option.SetLoggingPreference("performance",OpenQA.Selenium.LogLevel.Info); //启用performance日志,等级为Info即可
option.PerformanceLoggingPreferences = new ChromiumPerformanceLoggingPreferences() {
IsCollectingNetworkEvents = true //采集网络请求事件
};
using (ChromeDriver driver = new ChromeDriver(driverPath,option,TimeSpan.FromSeconds(5))) {
driver.Navigate().GoToUrl("https://item.m.jd.com/product/10052422060501.html");
Thread.Sleep(3 * 1000); //等待页面加载完成
   var logs = driver.Manage().Logs.GetLog("performance"); //获取所有performance日志
}

顺便放个Python代码做个对比:

caps = {
'browserName': 'chrome',
'loggingPrefs': {
'performance': 'Info', //启用performance日志,等级为Info即可
},
'goog:chromeOptions': {
'perfLoggingPrefs': {
'enableNetwork': True, //采集网络请求事件
},
'w3c': False,
},
}
driver = webdriver.Chrome(desired_capabilities=caps)
//TODO 获取日志
//.....

分析Logging:

我们从返回的日志列表里随便挑一个看看原始内容:

{[2022-08-03T08:47:31Z] [Info] {"message":{"method":"Network.responseReceived","params":{"frameId":"78D9BC6F0CBE162DA6779F410AA1500C","hasExtraInfo":true,"loaderId":"33814E6BD702343CF0A2A38C976C772F","requestId":"33814E6BD702343CF0A2A38C976C772F","response":{"connectionId":242,"connectionReused":false,"encodedDataLength":546,"fromDiskCache":false,"fromPrefetchCache":false,"fromServiceWorker":false,"headers":{"access-control-allow-credentials":"true","access-control-allow-headers":"Origin, X-Requested-With, Content-Type, multipart/form-data, Accept, Authorization","access-control-allow-methods":"POST, GET, PATCH, DELETE, PUT, OPTIONS","access-control-allow-origin":"*","access-control-max-age":"3600","cache-control":"no-cache,no-store","content-encoding":"gzip","content-language":"zh-CN","content-type":"text/html;charset=UTF-8","date":"Wed, 03 Aug 2022 08:47:33 GMT","hit":"bj-9153118133147527","server":"jfe","strict-transport-security":"max-age=86400","vary":"Accept-Encoding"},"mimeType":"text/html","protocol":"h2","remoteIPAddress":"106.39.169.120","remotePort":443,"responseTime":1.659516451658764e+12,"securityDetails":{"certificateId":0,"certificateTransparencyCompliance":"compliant","cipher":"AES_256_GCM","issuer":"GlobalSign RSA OV SSL CA 2018","keyExchange":"","keyExchangeGroup":"X25519","protocol":"TLS 1.3","sanList":["*.jd.com","*.360buy.com","*.360buyimg.com","*.3.cn","*.7fresh.com","*.baitiao.com","*.chinabank.com.cn","*.e.jd.com","*.jd.co.th","*.jddglobal.com","*.jd.hk","*.jd.id","*.jdpay.com","*.jd.ru","*.jdworldwide.com","*.jdx.com","*.joybuy.com","*.joybuy.es","*.jr.jd.com","*.k.jd.com","*.m.jd.com","*.m.yhd.com","*.shop.jd.com","*.wangyin.com","*.yhd.com","*.yiyaojd.com","360buy.com","360buyimg.com","3.cn","7fresh.com","baitiao.com","chinabank.com.cn","jd.co.th","jddglobal.com","jd.hk","jd.id","jdpay.com","jd.ru","jdworldwide.com","jdx.com","joybuy.com","joybuy.es","wangyin.com","yhd.com","yiyaojd.com","jd.com"],"signedCertificateTimestampList":[{"hashAlgorithm":"SHA-256","logDescription":"Sectigo 'Mammoth' CT log","logId":"6F5376AC31F03119D89900A45115FF77151C11D902C10029068DB2089A37D913","origin":"Embedded in certificate","signatureAlgorithm":"ECDSA","signatureData":"3045022017A2AC492303F50786758D0B4B63EEB8D031850832031FC5A43139C0CDA5EB4F0221009063F884220327718857A6897B87ED5D9F785FFC97F23BD45C84975A11DE721E","status":"Verified","timestamp":1.634109209345e+12},{"hashAlgorithm":"SHA-256","logDescription":"Google 'Argon2022' log","logId":"2979BEF09E393921F056739F63A577E5BE577D9C600AF8F94D5D265C255DC784","origin":"Embedded in certificate","signatureAlgorithm":"ECDSA","signatureData":"3045022071D6D51A59CAA1764E478598AA8EE34B628C8F856B09CDB8382E090CF5D6D97D022100A8FE17E028BB6D439387721974ED3629B9CC44C90AC524B505B9B2375C75CBB9","status":"Verified","timestamp":1.634109210136e+12},{"hashAlgorithm":"SHA-256","logDescription":"Sectigo 'Sabre' CT log","logId":"5581D4C2169036014AEA0B9B573C53F0C0E43878702508172FA3AA1D0713D30C","origin":"Embedded in certificate","signatureAlgorithm":"ECDSA","signatureData":"3045022015D94080264A3FCA83C0F3DE2B85A703384BB678FEBBE5408B4FF7D30BD40900022100DFBAB7992EE99420724CA35A5C2C252298B750994B0EAA98ACB22992F97D545E","status":"Verified","timestamp":1.634109209393e+12}],"subjectName":"*.jd.com","validFrom":1634109205,"validTo":1668410005},"securityState":"secure","status":200,"statusText":"","timing":{"connectEnd":96.834,"connectStart":19.521,"dnsEnd":19.521,"dnsStart":0,"proxyEnd":-1,"proxyStart":-1,"pushEnd":0,"pushStart":0,"receiveHeadersEnd":217.707,"requestTime":2272960.91089,"sendEnd":97.25,"sendStart":97.03,"sslEnd":96.829,"sslStart":55.726,"workerFetchStart":-1,"workerReady":-1,"workerRespondWithSettled":-1,"workerStart":-1},"url":"https://item.m.jd.com/product/10052422060501.html"},"timestamp":2272961.1294,"type":"Document"}},"webview":"78D9BC6F0CBE162DA6779F410AA1500C"}}

我们格式化一下,可以发现日志里已经包含了请求的大部分描述信息,我们需要遍历过滤出method为Network.responseReceived的日志,当然你也可以再根据其他的一些参数过滤出你要的请求

每个日志都有一个requestId,我们再通过该值调CDP命令[Network.getResponseBody]换取具体内容

{
"message": {
"method": "Network.responseReceived",
"params": {
"frameId": "78D9BC6F0CBE162DA6779F410AA1500C",
"hasExtraInfo": true,
"loaderId": "33814E6BD702343CF0A2A38C976C772F",
"requestId": "33814E6BD702343CF0A2A38C976C772F",
"response": {
"connectionId": 242,
"connectionReused": false,
"encodedDataLength": 546,
"fromDiskCache": false,
"fromPrefetchCache": false,
"fromServiceWorker": false,
"headers": {
"access-control-allow-credentials": "true",
"access-control-allow-headers": "Origin, X-Requested-With, Content-Type, multipart/form-data, Accept, Authorization",
"access-control-allow-methods": "POST, GET, PATCH, DELETE, PUT, OPTIONS",
"access-control-allow-origin": "*",
"access-control-max-age": "3600",
"cache-control": "no-cache,no-store",
"content-encoding": "gzip",
"content-language": "zh-CN",
"content-type": "text/html;charset=UTF-8",
"date": "Wed, 03 Aug 2022 08:47:33 GMT",
"hit": "bj-9153118133147527",
"server": "jfe",
"strict-transport-security": "max-age=86400",
"vary": "Accept-Encoding"
},
"mimeType": "text/html",
"protocol": "h2",
"remoteIPAddress": "106.39.169.120",
"remotePort": 443,
"responseTime": 1.659516451658764e+12,
"securityDetails": {
"certificateId": 0,
"certificateTransparencyCompliance": "compliant",
"cipher": "AES_256_GCM",
"issuer": "GlobalSign RSA OV SSL CA 2018",
"keyExchange": "",
"keyExchangeGroup": "X25519",
"protocol": "TLS 1.3",
"sanList": ["*.jd.com", "*.360buy.com", "*.360buyimg.com", "*.3.cn", "*.7fresh.com", "*.baitiao.com", "*.chinabank.com.cn", "*.e.jd.com", "*.jd.co.th", "*.jddglobal.com", "*.jd.hk", "*.jd.id", "*.jdpay.com", "*.jd.ru", "*.jdworldwide.com", "*.jdx.com", "*.joybuy.com", "*.joybuy.es", "*.jr.jd.com", "*.k.jd.com", "*.m.jd.com", "*.m.yhd.com", "*.shop.jd.com", "*.wangyin.com", "*.yhd.com", "*.yiyaojd.com", "360buy.com", "360buyimg.com", "3.cn", "7fresh.com", "baitiao.com", "chinabank.com.cn", "jd.co.th", "jddglobal.com", "jd.hk", "jd.id", "jdpay.com", "jd.ru", "jdworldwide.com", "jdx.com", "joybuy.com", "joybuy.es", "wangyin.com", "yhd.com", "yiyaojd.com", "jd.com"],
"signedCertificateTimestampList": [{
"hashAlgorithm": "SHA-256",
"logDescription": "Sectigo 'Mammoth' CT log",
"logId": "6F5376AC31F03119D89900A45115FF77151C11D902C10029068DB2089A37D913",
"origin": "Embedded in certificate",
"signatureAlgorithm": "ECDSA",
"signatureData": "3045022017A2AC492303F50786758D0B4B63EEB8D031850832031FC5A43139C0CDA5EB4F0221009063F884220327718857A6897B87ED5D9F785FFC97F23BD45C84975A11DE721E",
"status": "Verified",
"timestamp": 1.634109209345e+12
}, {
"hashAlgorithm": "SHA-256",
"logDescription": "Google 'Argon2022' log",
"logId": "2979BEF09E393921F056739F63A577E5BE577D9C600AF8F94D5D265C255DC784",
"origin": "Embedded in certificate",
"signatureAlgorithm": "ECDSA",
"signatureData": "3045022071D6D51A59CAA1764E478598AA8EE34B628C8F856B09CDB8382E090CF5D6D97D022100A8FE17E028BB6D439387721974ED3629B9CC44C90AC524B505B9B2375C75CBB9",
"status": "Verified",
"timestamp": 1.634109210136e+12
}, {
"hashAlgorithm": "SHA-256",
"logDescription": "Sectigo 'Sabre' CT log",
"logId": "5581D4C2169036014AEA0B9B573C53F0C0E43878702508172FA3AA1D0713D30C",
"origin": "Embedded in certificate",
"signatureAlgorithm": "ECDSA",
"signatureData": "3045022015D94080264A3FCA83C0F3DE2B85A703384BB678FEBBE5408B4FF7D30BD40900022100DFBAB7992EE99420724CA35A5C2C252298B750994B0EAA98ACB22992F97D545E",
"status": "Verified",
"timestamp": 1.634109209393e+12
}],
"subjectName": "*.jd.com",
"validFrom": 1634109205,
"validTo": 1668410005
},
"securityState": "secure",
"status": 200,
"statusText": "",
"timing": {
"connectEnd": 96.834,
"connectStart": 19.521,
"dnsEnd": 19.521,
"dnsStart": 0,
"proxyEnd": -1,
"proxyStart": -1,
"pushEnd": 0,
"pushStart": 0,
"receiveHeadersEnd": 217.707,
"requestTime": 2272960.91089,
"sendEnd": 97.25,
"sendStart": 97.03,
"sslEnd": 96.829,
"sslStart": 55.726,
"workerFetchStart": -1,
"workerReady": -1,
"workerRespondWithSettled": -1,
"workerStart": -1
},
"url": "https://item.m.jd.com/product/10052422060501.html"
},
"timestamp": 2272961.1294,
"type": "Document"
}
},
"webview": "78D9BC6F0CBE162DA6779F410AA1500C"
}

完整代码:

var option = new ChromeOptions();
option.SetLoggingPreference("performance",OpenQA.Selenium.LogLevel.Info); //启用performance日志,等级为Info即可
option.PerformanceLoggingPreferences = new ChromiumPerformanceLoggingPreferences() {
IsCollectingNetworkEvents = true //采集网络请求事件
};
using (ChromeDriver driver = new ChromeDriver(driverPath,option,TimeSpan.FromSeconds(5))) {
driver.Navigate().GoToUrl("https://item.m.jd.com/product/10052422060501.html");
Thread.Sleep(3 * 1000); //等待页面加载完成
var logs = driver.Manage().Logs.GetLog("performance").Where(o => o.Message.Contains("\"Network.responseReceived\""));//获取所有performance日志,并过滤出所有类型为Network.responseReceived的日志
   foreach (var log in logs) {
      //日志找不到就会抛出异常,必须要捕获异常
      try {
        var json = JObject.Parse(log.Message);
        var url = json["message"]["params"]["response"]["url"].ToString(); //请求url,可通过url过滤出你要的请求
        var requestId = json["message"]["params"]["requestId"].ToString();
        //利用RequestId做为参数,执行CDP命令获取日志详细内容。 踩坑警告:返回的是一个字典,需要转换为Dictionary<string,object>
        var response = driver.ExecuteCdpCommand("Network.getResponseBody",new Dictionary<string,object>() {{ "requestId",requestId }}) as Dictionary<string,object>;
        if (response.TryGetValue("body",out object? bodyObj) && bodyObj != null) {
          string body = bodyObj.ToString();
          Console.WriteLine($"输出Body内容:{body}");
        }
      } catch(Exception ex){
        //记录错误日志
      }
   }
}

封装

为了以后方便复用,我封装了一个类

其中过滤条件的入参,可以根据实际情况自行修改,如果不需要对body内容进行过滤可以去掉这个参数,这样就不用每次等待拿到结果再过滤,性能会好很多

   /// <summary>
/// Selenium网络请求日志帮助类
/// </summary>
public class NetworkLoggingHelper
{
static readonly Logger logger = NlogProvider.GetLogger(); /// <summary>
/// 开启网络请求日志
/// </summary>
/// <param name="option"></param>
public static void OpenNetworkPerformanceLogging(ref ChromeOptions option)
{
option.SetLoggingPreference("performance",OpenQA.Selenium.LogLevel.Info);
option.PerformanceLoggingPreferences = new ChromiumPerformanceLoggingPreferences() {
IsCollectingNetworkEvents = true
};
} /// <summary>
/// 获取网络请求数据
/// </summary>
/// <param name="driver"></param>
/// <param name="filter">过滤条件 入参1:请求的url | 入参2:请求的mimeType | 入参3:请求的body</param>
/// <returns></returns>
public static Dictionary<string,string> GetNetworkApiDatas(ChromeDriver driver,Func<string,string,string,bool> filter)
{
Dictionary<string,string> datas = new Dictionary<string,string>();
try {
var logs = driver.Manage().Logs.GetLog("performance")?.Where(o => o.Message.Contains("\"Network.responseReceived\""));
foreach (var log in logs) {
try {
var json = JObject.Parse(log.Message);
if (json["message"]["params"] == null || json["message"]["params"]["response"] == null) {
continue;
}
var url = json["message"]["params"]["response"]["url"].ToString();
var mimeType = json["message"]["params"]["response"]["mimeType"].ToString();
var requestId = json["message"]["params"]["requestId"].ToString();
var response = driver.ExecuteCdpCommand("Network.getResponseBody",new Dictionary<string,object>() {
{ "requestId",requestId }
}) as Dictionary<string,object>;
if (response != null && response.Count > 0) {
//是否base64编码
var isBase64Encode = false;
if (response.TryGetValue("base64Encoded",out object? base64Encoded) && base64Encoded != null) {
isBase64Encode = (bool)base64Encoded;
}
//获取响应内容
string body = string.Empty;
if (response.TryGetValue("body",out object? bodyObj) && bodyObj != null) {
body = bodyObj.ToString();
if (isBase64Encode) {
body = body.DecodeBase64(Encoding.UTF8);
}
}
//根据条件过滤,如果不需要body内容参与过滤条件判断,这个if语句可以移到获取response的上面,性能会好很多
if (filter.Invoke(url,mimeType,body)) {
datas.Add(url,body);
}
}
} catch (Exception ex) {
logger.Error(ex.Message);
}
}
} catch (Exception ex) {
logger.Error($"获取日志失败:{ex.Message}");
}
return datas;
}
}

调用示例:

var option = new ChromeOptions();
NetworkLoggingHelper.OpenNetworkPerformanceLogging(ref option); //开启日志
using (ChromeDriver driver = new ChromeDriver(driverPath,option,TimeSpan.FromSeconds(5))) {
driver.Navigate().GoToUrl("https://item.m.jd.com/product/10052422060501.html");
Thread.Sleep(3 * 1000); //等待页面加载完成 var datas = NetworkLoggingHelper.GetNetworkApiDatas(driver,(url,mimeType,body) => {
return url.Contains("//item.m.jd.com/product/") || mimeType.Contains("application/json") || body.Contains("windows.itemInfo");
});
Console.WriteLine(datas.Count);
}

Selenium工作原理

叙述一下selenium工作的过程

1.selenium client(python等语言编写的自动化测试脚本)初始化一个service服务,通过Webdriver启动浏览器驱动程序chromedriver.exe

2.通过RemoteWebDriver向浏览器驱动程序发送HTTP请求,浏览器驱动程序解析请求,打开浏览器,并获得sessionid,如果再次对浏览器操作需携带此id

3.打开浏览器,绑定特定的端口,把启动后的浏览器作为webdriver的remote server

3.打开浏览器后,所有的selenium的操作(访问地址,查找元素等)均通过RemoteConnection链接到remote server,然后使用execute方法调用_request方法通过urlib3向remote server发送请求

4.浏览器通过请求的内容执行对应动作

5.浏览器再把执行的动作结果通过浏览器驱动程序返回给测试脚本

6.  webdriver.是 w3c 的标准协议。提供一组接口,用于发现和操作web文档中的DOM元素。

webdriver.是一系列的API.它给测试代码提供了定位和操作 wEB元素的能力。不同的开发语言有相应的wsbdriver.

↑↑↑↑ 该部分内容来自:https://www.cnblogs.com/xrxc/p/14776895.html

作者:Harry

原文出处:https://www.cnblogs.com/simendancer/articles/16546199.html

有些文本描述和图片源自网络,如有侵犯请私信告知

C#爬虫之通过Selenium获取浏览器请求响应结果的更多相关文章

  1. VS2008 C++ 利用WinHttp API获取Http请求/响应头部Header

    http://www.cnblogs.com/LCCRNblog/p/3833472.html 这一篇博客中,实现了获取http请求/响应后的html源码,现在需要获取http请求/响应的头部Head ...

  2. Spider-Python爬虫之使用Selenium模拟浏览器行为

    分析 他的代码比较简单,主要有以下的步骤:使用BeautifulSoup库,打开百度贴吧的首页地址,再解析得到id为new_list标签底下的img标签,最后将img标签的图片保存下来. header ...

  3. selenium.获取浏览器大小、设置浏览器位置、最大化浏览器

    此篇博客学习控制浏览器的api,分别有: get_window_size() 获取浏览器大小 set_window_size() 设置浏览器位置 get_window_position() 获取浏览器 ...

  4. LoadRunner 获取接口请求响应信息

    Action() { int nHttpRetCode; // 默认最大长度为256,get请求需注意缓存问题,需要根据content-length进行修改 web_set_max_html_para ...

  5. [转] LoadRunner 获取接口请求响应信息

    Action() { int nHttpRetCode; // 默认最大长度为256,get请求需注意缓存问题,需要根据content-length进行修改 web_set_max_html_para ...

  6. 爬虫模块介绍--selenium (浏览器自动化测试工具,模拟可以调用浏览器模拟人操作浏览器)

    selenium主要的用途就是控制浏览器,模仿真人操作浏览器的行为 模块安装:pip3 install selenium 需要控制的浏览器 from selenium import webdriver ...

  7. 爬虫实战--使用Selenium模拟浏览器抓取淘宝商品美食信息

    from selenium import webdriver from selenium.webdriver.common.by import By from selenium.common.exce ...

  8. [技巧篇]11.JavaScript原生态如何获取浏览器请求地址中的参数

    var getAccessParams = function(){ var i,ilen,strs,keyName,keyValue, params={}, path = window.locatio ...

  9. selenium获取浏览器控制台日志

    public void logsTest(){ WebDriver driver = null; try { System.setProperty("webdriver.chrome.dri ...

随机推荐

  1. Spring Boot 2.7.0发布,2.5停止维护,节奏太快了吧

    这几天是Spring版本日,很多Spring工件都发布了新版本, Spring Framework 6.0.0 发布了第 4 个里程碑版本,此版本包含所有针对 5.3.20 的修复补丁,以及特定于 6 ...

  2. 详解TCP三次握手(建立TCP连接过程)

    在讲述TCP三次握手,即建立TCP连接的过程之前,需要先介绍一下TCP协议的包结构. 这里只对涉及到三次握手过程的字段做解释 (1) 序号(Sequence number) 我们通过 TCP 协议将数 ...

  3. 20 HTTP 长连接与短连接

    20 HTTP 长连接与短连接 每日一句 纸上得来终觉浅,绝知此事要躬行. 每日一句 Never give up until the fight is over. 永远不要放弃,要一直战斗到最后一秒. ...

  4. 记一次Tomcat卡死在 Deploying web application 步骤的问题

    公司有一个历史的遗留项目是传统的MVC架构的前后不分离的项目,一开始使用JDK1.7写的,后来前一阵老板说想在这个远古项目上加点功能,顺带换换皮,于是乎一帮程序员们就用JDK1.8重新翻新了一遍项目顺 ...

  5. 盘点微信小程序跨页面传值的若干方式

    直接给大家上干货 1.跳转页面传递参数 pageA.wxml <button type="primary" bindtap="jumpTo">点击跳 ...

  6. JAVA - 启动线程有哪几种方式

    JAVA - 启动线程有哪几种方式 一.继承Thread类创建线程类 (1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务.因此把run()方法称为执行 ...

  7. 拥抱Spring全新OAuth解决方案

    以下全文 Spring Authorization Server 简称为: SAS 背景 Spring 团队正式宣布 Spring Security OAuth 停止维护,该项目将不会再进行任何的迭代 ...

  8. SCI论文写作注意事项

    1. 先写结论:(划定范围,以防添加无效的内容)     并非一开始就把整个结论都写出来,而是把

  9. JMeter - 生成随机数/随机字符串/随机变量/随机日期

    1. Random - 随机数 1.1 作用 1.2 声明 1.3 例子 2. __RandomDate - 随机日期 2.1 作用 2.2 声明参数 2.3 例子 3. RandomString - ...

  10. JS:逗号运算符

    逗号运算符: 会把逗号隔开的表达式全部执行 最后一个运行的表达式的结果就是逗号运算符的结果   例: var a = (1, 2, 3, 4, 5, 6); console.log(a); //6 隐 ...