说起测试GA,真是一件枯燥乏味,重复性很高的工作,那么为什么我们不使用自动化测试代替它呢,显然,很多公司的产品迭代太快,ga也变化的比较频繁,但是确保ga工作正常,对于其他部门的工作是有很大帮助的,由于公司对于这块比较注重,而且曾经出现过ga被前端修复bug而影响,所以抽空倒腾了下如何对ga进行自动化测试,由于自身比较习惯使用ruby,所以本帖都是ruby的代码,思路是一样的,喜欢的童鞋可以用其他语言去实现。

首先说说开始考虑的实现方案:

1. 使用selenium+firefox的插件抓取request生成har文件,尝试过后发现不可行,点看此文章 http://www.cnblogs.com/timsheng/p/7209964.html

2. 使用proxy,讲浏览器请求server的request转发到本地,proxy的库有很多,ruby内置的webrick就很好用,但是尝试过后发现依然不行,webrick只能抓取http的request,我们网站是https协议的,抓取不到。

3. 使用evil-proxy, https://github.com/bbtfr/evil-proxy 这个库很强大,可以结合selenium使用,原理是运行时会生成自己的签名文件,然后将生成的签名文件import到浏览器就行了,具体如何操作请参考wiki,但是问题又来了,我们网站https的request都可以抓到,除了google的https request无法抓取,会提示无效签名。

4. ok,这些简单的方式都无法成功抓取ga的request,只能出绝招了,可能大家都知道,phantomjs是一个很强大的工具,它可以结合其他框架做headless网站测试,可以截图,不同于selenium截取当前页面图,它可以截取全屏截图,另外它可以做网页测试,最关键的是它可以进行网络监控。 传送门在此,http://phantomjs.org/

所以我们需要使用的是phantomjs, 去进行页面自动化,并且抓取生成的ga requests,用ruby去分析日志,并且进行校验pageview和event的事件是否触发,参数是否正确

不多说上代码:

首先看一下目录结构

我们先来看一下student_ga.js文件

/*** Wait until the test condition is true or a timeout occurs. Useful for waiting* on a server response or for a ui change (fadeIn, etc.) to occur.** @param testFx javascript condition that evaluates to a boolean,* it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or* as a callback function.* @param onReady what to do when testFx condition is fulfilled,* it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or* as a callback function.* @param timeOutMillis the max amount of time to wait. If not specified, 3 sec is used.*/"use strict";function waitFor(testFx, onReady, timeOutMillis) {var maxtimeOutMillis = timeOutMillis || 8000, //< Default Max Timout is 3sstart = new Date().getTime(),condition = false,interval = setInterval(function() {if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) {// If not time-out yet and condition not yet fulfilledcondition = (typeof(testFx) === "string" ? eval(testFx) : testFx()); //< defensive code} else {if(!condition) {// If condition still not fulfilled (timeout but condition is 'false')console.log("'waitFor()' timeout");phantom.exit(1);} else {// Condition fulfilled (timeout and/or condition is 'true')console.log("'waitFor()' finished in " + (new Date().getTime() - start) + "ms.");typeof(onReady) === "string" ? eval(onReady) : onReady(); //< Do what it's supposed to do once the condition is fulfilledclearInterval(interval); //< Stop this interval
            }}}, 250); //< repeat check every 250ms
};// initialise various variables
var page = require('webpage').create(),system = require('system'),address;page.viewportSize = {width: 1280,height: 800
};// how long should we wait for the page to load before we exit
// in ms
var WAIT_TIME = 30000;// if the page hasn't loaded after this long, something is probably wrong
// in ms
var MAX_EXECUTION_TIME = 30000;// output error messages
var DEBUG = true// a list of regular expressions of resources (urls) to log when we load them
var resources_to_log = [new RegExp('^http(s)?://(www|ssl)\.google-analytics\.com.*'),new RegExp('^http(s)?://stats\.g\.doubleclick\.net.*')
];// page.settings.resourceTimeout = 10000;// check we have a url, if not exit
if (system.args.length === 1) {console.log('Usage: get_ga_resources.js http://www.yoururl.com');phantom.exit(1);
} else {// address is the url passedaddress = system.args[1];// create a function that is called every time a resource is requested// http://phantomjs.org/api/webpage/handler/on-resource-requested.htmlpage.onResourceRequested = function (res) {// loop round all our regexs to see if this url matches any of themvar length = resources_to_log.length;while(length--) {if (resources_to_log[length].test(res.url)){// we have a match, log it
              console.log(res.url);}}};// if debug is true, log errors, else ignore thempage.onError = function(msg, trace){if (DEBUG) {console.log('ERROR: ' + msg);console.log(trace);}};// output console message// page.onConsoleMessage = function(msg) {//     console.log(msg);// };// page.onResourceTimeout = function(request) {//     console.log(request.errorCode);//     console.log(request.errorString);//     console.log(request.url);//     // console.log('Response (#' + request.id + '): ' + JSON.stringify(request));// };// page.onResourceError = function(resourceError) {//     console.log('Unable to load resource (#' + resourceError.id + 'URL:' + resourceError.url + ')');//     console.log('Error code: ' + resourceError.errorCode + '. Description: ' + resourceError.errorString);// };// now all we have to do is open the page, wait WAIT_TIME ms and exittry {page.open(address, function (status) {console.log("Starting to open --" + address);if (status !== 'success') {console.log("FAILED: to load " + system.args[1]);console.log(page.reason_url);console.log(page.reason);phantom.exit();} else {// page is loaded!if(address != page.url){console.log('Redirected: ' + page.url)}// start to do actions on websiteconsole.log("Success to open --" + address);page.render("screenshot/homepage.png");// select top city london to clickpage.evaluate(function(s) {console.log("Click top city -> London");document.querySelector(".top-cities__city:nth-child(1)>a>img").click();console.log("click it done!!!!");});// submit enquiry until srp load completelysetTimeout(function(){waitFor(function() {// Check in the page if a specific element is now visiblereturn page.evaluate(function() {console.log("determine if contact an expert element is visible? ")cae_element = document.querySelector(".advicebar__btn.button.button--keppel.js-account-modal-enquire");if (cae_element.offsetWidth > 0 && cae_element.offsetHeight > 0) {return true;} else {return false;};});}, function() {console.log("Page_Url:" + page.url);console.log("--- Get into search result page ---");page.render("screenshot/search_result_page.png");page.evaluate(function() {console.log(document.querySelector(".advicebar__btn.button.button--keppel.js-account-modal-enquire").innerHTML);document.querySelector(".advicebar__btn.button.button--keppel.js-account-modal-enquire").click();console.log("Click contact an expert button");});console.log("--- Get into entry screen ---");page.render("screenshot/entry_screen.png");page.evaluate(function() {document.querySelector("#js-account-modal-enquiry .account-modal__step--visible .button.button--secondary").click();console.log("--- Get into sign up screen ---");document.querySelector("#js-account-modal-enquiry input[name='full_name']").value = 'tim sheng';document.querySelector("#js-account-modal-enquiry input[name='telephone']").value = '123123123';document.querySelector("#js-account-modal-enquiry input[name='email']").value = ("tim.sheng+" + (new Date().getTime()) + "@student.com");document.querySelector("#js-account-modal-enquiry input[name='password']").value = 'abc20052614';});page.render("screenshot/sign_up_screen.png");page.evaluate(function() {console.log('Click sign up button');document.querySelector("#set-password-button").click();});waitFor(function() {return page.evaluate(function() {console.log("determine if confirm button is visible? on about you screen");confirm_element = document.querySelector("#js-account-modal-enquiry #submit-about-you-button");if (confirm_element.offsetWidth > 0 && confirm_element.offsetHeight > 0) {return true;} else {return false;};});}, function() {console.log("--- Get into about you screen ---");page.render("screenshot/about_you_screen.png");page.evaluate(function() {document.querySelector("#js-account-modal-enquiry #submit-about-you-button").click();});waitFor(function() {return page.evaluate(function() {console.log("determine if budget field is visible? on about listing screen");budget_element = document.querySelector("#js-account-modal-enquiry input[name='budget']");if (budget_element.offsetWidth > 0 && budget_element.offsetHeight > 0) {return true;} else {return false;};});}, function() {console.log("--- Get into about listing screen ---");page.render("screenshot/about_listig_screen_unfilled.png");page.evaluate(function() {// click date picker plugindocument.querySelector("#js-account-modal-enquiry .date-picker").click();// select move in datedocument.querySelectorAll("#js-account-modal-enquiry .js-date-picker-move-in-fieldset input[class='js-date-picker-move-in-month']:not(:disabled)+label")[0].click();// select move out datedocument.querySelectorAll("#js-account-modal-enquiry .js-date-picker-move-out-fieldset input[class='js-date-picker-move-out-month']:not(:disabled)+label")[0].click();// input budget valuedocument.querySelector("#js-account-modal-enquiry input[name='budget']").value = '1234';// input university valuedocument.querySelector("#js-account-modal-enquiry .account-modal__step--visible input[name='university']").value = 'london';// dispatch inputing event to elemvar event = new Event('inputing');input_elem = document.querySelector("#js-account-modal-enquiry .account-modal__step--visible input[name='university']");input_elem.focus();input_elem.dispatchEvent(event);});waitFor(function() {return page.evaluate(function() {console.log("determine if university is visible? on autocomplete list");uni_element = document.querySelector('#js-account-modal-enquiry .autocomplete__item:first-child .autocomplete__item__link');if (uni_element.offsetWidth > 0 && uni_element.offsetHeight > 0) {return true;} else {return false;};});}, function() {console.log("--- University is visible on autocomplete list");page.render("screenshot/about_listing_university.png");page.evaluate(function() {document.querySelector('#js-account-modal-enquiry .autocomplete__item:first-child .autocomplete__item__link').click();});page.render("screenshot/about_listing_screen_filled.png");page.evaluate(function() {console.log("Click submit enquiry button");document.querySelector("#js-account-modal-enquiry .account-modal__step--visible #submit-about-stay-button").click();});waitFor(function() {return page.evaluate(function() {console.log("determine if success button is visible? on leads process screen");success_element = document.querySelector("#js-account-modal-enquiry .account-modal__step--visible .button.button--primary");if (success_element.offsetWidth > 0 && success_element.offsetHeight > 0) {return true;} else {return false;};});}, function() {console.log("submit enquiry from srp cae successfully!");page.render("screenshot/enquiry_success_screen.png");});});});});});},5000);setTimeout(function () {phantom.exit();}, WAIT_TIME);}});} finally {// if we are still running after MAX_EXECUTION_TIME ms exitsetTimeout(function() {console.log("FAILED: Max execution time " + Math.round(MAX_EXECUTION_TIME) + " seconds exceeded");phantom.exit(1);}, MAX_EXECUTION_TIME);}
}

  

然后写个ruby类去解析这个log, cop.rb

require 'uri'module Copclass Loggerattr_accessor :ga_requestsdef initialize path@ga_requests = open_log_file pathend# fetch pageview gadef pageviewGas.new ga_requests, 'pageview'end# fetch event gadef eventGas.new ga_requests, 'event'end# get all google analytics request recordsdef open_log_file pathall_ga_requests = []File.open("#{path}").each do |line|all_ga_requests << line if line.include? 'google'endall_ga_requestsendend# Gas is a class which is consisted of a list of specific ga requestsclass Gasattr_accessor :gas, :typeVALID_KEYS = ['dl','dp','ul','dt','ec','ea','el','cd17','cd15','cd5','cd1','cd14','cg1','cg2','cd18','cd2']def initialize all_gas, typeh_gas = handle_gas all_gas, type@gas = get_expected_gas h_gas@type = typeend# return the count of gasdef countgas.countend# use the value of key to get corresponding pageview record# it is better for pageview ga using dp to get value# use the value of key to get corresponding event record# it is better for event ga using el to get valuedef get_gas_by valuegas.each do |ga|ga.each do |k,v|if v == valuereturn gaendendendendprivate# fetch ga requests by typedef handle_gas all_gas, typenew_gas_arr = []all_gas.each do |all_ga|if all_ga.include? typedecoded_all_ga = URI.decode(all_ga)new_gas_arr << qs_to_hash(decoded_all_ga)endendnew_gas_arrend# get expected gasdef get_expected_gas gasexpected_gas = []gas.each do |ga|expected_ga = {}ga.each do |k,v|if VALID_KEYS.include? kexpected_ga[k] = velsenextendendexpected_gas << expected_gaendexpected_gasend# decode urldef qs_to_hash querykeyvals = query.split('&').inject({}) do |result, q|k,v = q.split('=')if !v.nil?result.merge({k => v})elsif !result.key?(k)result.merge({k => true})elseresultendendkeyvalsendend
end

Gas类返回的是hash,我希望取hash的value根据object.xx  的形式,而不是hash[] 的方式,所以重新打开hash类,根据ga的常用参数使用define_method动态定义一些方法

hash.rb

class HashVALID_KEYS = ['dl','dp','ul','dt','ec','ea','el','cd17','cd15','cd5','cd1','cd14','cg1','cg2','cd18','cd2']def self.ga namedefine_method "#{name}" doself["#{name}"]endendVALID_KEYS.each do |e|ga "#{e}"endend

基本准备工作差不多了,现在我们用rspec去管理测试用例,在执行case前,我们需要去清洗一下环境,删除log文件,截图,然后执行phantomjs脚本,

bridge.rb

module Copdef clear_env`rm -rf screenshot/*.png``rm -rf log/*.log`enddef submit_lead_from_srp_cae`phantomjs js/student_ga.js https://hurricane-cn.dandythrust.com > log/ga.log`end
end

让我们在根目录下rspec --init一下,生成spec_helper.rb 文件,在此文件中,引入cop.rb, hash.rb, bridge.rb以便于在_spec文件中使用

spec_helper.rb

require 'cop'
require 'hash'
require 'bridge'
include Cop

新建个ga_spec.rb文件,开始编写case

require "spec_helper"describe "GA Checking" dodescribe "New user submit lead from srp cae" dobefore(:all) doclear_envsubmit_lead_from_srp_caeendlet(:logger) { logger= Cop::Logger.new "log/ga.log" }context "Pageview" dolet(:pageview_gas) { logger.pageview }it "should be correct on homepage" doresult = pageview_gas.get_gas_by "/"expect(result.dp).to eql "/"expect(result.ul).to eql "en-us"expect(result.cd17).to eql "3rd Party Login" unless result.cd17.nil?expect(result.cd15).to eql "zh-cn"expect(result.cd5).to eql "home"expect(result.cd1).to eql "desktop"expect(result.cd14).to eql "Special Offers"expect(result.cg1).to eql "Home Page"endit "should be correct on search result page" doresult = pageview_gas.get_gas_by "/uk/london"expect(result.dp).to eql "/uk/london"expect(result.ul).to eql "en-us"expect(result.cd17).to eql "3rd Party Login" unless result.cd17.nil?expect(result.cd18).to eql "231004024"expect(result.cd15).to eql "zh-cn"expect(result.cd5).to eql "search"expect(result.cd2).to eql "London"expect(result.cd1).to eql "desktop"expect(result.cg1).to eql "Search"expect(result.cg2).to eql "City"endit "should be correct on entry screen" doresult = pageview_gas.get_gas_by "/modal/enquiry/cae_srp/signup"expect(result.dp).to eql "/modal/enquiry/cae_srp/signup"expect(result.ul).to eql "en-us"endit "should be correct on sign up screen" doresult = pageview_gas.get_gas_by "/modal/enquiry/cae_srp/signup/email"expect(result.dp).to eql "/modal/enquiry/cae_srp/signup/email"expect(result.ul).to eql "en-us"endit "should be correct on about you screen" doresult = pageview_gas.get_gas_by "/modal/enquiry/cae_srp/signup/email/confirm_contact_info"expect(result.dp).to eql "/modal/enquiry/cae_srp/signup/email/confirm_contact_info"expect(result.ul).to eql "en-us"endit "should be correct on about stay screen" doresult = pageview_gas.get_gas_by "/modal/enquiry/cae_srp/signup/email/about_stay"expect(result.dp).to eql "/modal/enquiry/cae_srp/signup/email/about_stay"expect(result.ul).to eql "en-us"endit "should be correct on enquiry success screen" doresult = pageview_gas.get_gas_by "/modal/enquiry/cae_srp/signup/email/enq_submitted"expect(result.dp).to eql "/modal/enquiry/cae_srp/signup/email/enq_submitted"expect(result.ul).to eql "en-us"endendcontext "Event" dolet(:event_gas) { logger.event }it "click top city on homepage is correct" doresult = event_gas.get_gas_by "topCities"expect(result.dp).to eql "/"expect(result.ul).to eql "en-us"expect(result.ea).to eql "topCities"expect(result.ec).to eql "homePage"expect(result.el).to eql "city:231004024"expect(result.cd17).to eql "3rd Party Login"expect(result.cd15).to eql "zh-cn"expect(result.cd5).to eql "home"expect(result.cd1).to eql "desktop"expect(result.cd14).to eql "Special Offers"expect(result.cg1).to eql "Home Page"endit "click contact an expert on srp is correct" doresult = event_gas.get_gas_by "cae > button:getInTouch"expect(result.dp).to eql "/uk/london"expect(result.ul).to eql "en-us"expect(result.ea).to eql "cae > button:getInTouch"expect(result.ec).to eql "searchClick"expect(result.el).to eql "231004024-London"expect(result.cd17).to eql "3rd Party Login"expect(result.cd15).to eql "zh-cn"expect(result.cd5).to eql "search"expect(result.cd2).to eql "London"expect(result.cd1).to eql "desktop"expect(result.cg1).to eql "Search"expect(result.cg2).to eql "City"endit "click sign up with email on entry screen is correct" doresult = event_gas.get_gas_by "continueWithEmail"expect(result.dp).to eql "/modal/enquiry/cae_srp/signup"expect(result.ul).to eql "en-us"expect(result.ea).to eql "signupScreen"expect(result.ec).to eql "enquiryFlow"expect(result.el).to eql "continueWithEmail"endit "click continue button on sign up screen is correct" doresult = event_gas.get_gas_by "continueBtnClicked"expect(result.dp).to eql "/modal/enquiry/cae_srp/signup/email"expect(result.ul).to eql "en-us"expect(result.ea).to eql "signupWithEmailScreen"expect(result.ec).to eql "enquiryFlow"expect(result.el).to eql "continueBtnClicked"endit "click request details button on about you screen is correct" doresult = event_gas.get_gas_by "requestDetailsBtnClicked"expect(result.dp).to eql "/modal/enquiry/cae_srp/signup/email/confirm_contact_info"expect(result.ul).to eql "en-us"expect(result.ea).to eql "confirmContactInfo:email"expect(result.ec).to eql "enquiryFlow"expect(result.el).to eql "requestDetailsBtnClicked"endit "focus destination uni on about stay screen is correct" doresult = event_gas.get_gas_by "focus:destinationUniversity"expect(result.dp).to eql "/modal/enquiry/cae_srp/signup/email/about_stay"expect(result.ul).to eql "en-us"expect(result.ea).to eql "aboutStay"expect(result.ec).to eql "enquiryFlow"expect(result.el).to eql "focus:destinationUniversity"endit "click submit form button on about stay screen is correct" doresult = event_gas.get_gas_by "submitBtnClicked"expect(result.dp).to eql "/modal/enquiry/cae_srp/signup/email/about_stay"expect(result.ul).to eql "en-us"expect(result.ea).to eql "aboutStay"expect(result.ec).to eql "enquiryFlow"expect(result.el).to eql "submitBtnClicked"endit "enquiry submitted is correct" doresult = event_gas.get_gas_by "cae_srp"expect(result.dp).to eql "/modal/enquiry/cae_srp/signup/email/enq_submitted"expect(result.ul).to eql "en-us"expect(result.ea).to eql "success"expect(result.ec).to eql "enquiry"expect(result.el).to eql "cae_srp"endendend

end

The End

转载于:https://www.cnblogs.com/timsheng/p/7380902.html

ruby + phantomjs 自动化测试 - GA相关推荐

  1. ruby+watir-webdriver自动化测试入门

    百度搜索(python): from selenium import webdriverdriver=webdriver.Chrome() driver.get("http://www.ba ...

  2. php dubbo 接口测试工具,dubbo服务自动化测试搭建

    java实现dubbo的消费者服务编写:ruby实现消费者服务的接口测试:通过消费者间接测试dubbo服务接口的逻辑 内容包括:dubbo服务本地调用环境搭建,dubbo服务启动,消费者部署,脚本编写 ...

  3. 前后端分离后的前端时代

    什么是前后端分离,要区分前端和后端,需要有个明确的界限.一般,用户可以直接看到的东西,都是属于前端的范畴,除了前端之外都属于后端了. 在传统的像ASP,JSP和PHP等开发模式中,前端是处在一个混沌的 ...

  4. 前后端分离后的前端时代 1

    本文从前端开发的视角,聊一聊前后端分离之后的前端开发的那些事儿.阅读全文,大约需要8分钟. 什么是前后端分离 除了前端之外都属于后端了. 你负责貌美如花,我负责赚钱养家 在传统的像ASP,JSP和PH ...

  5. 前后端分离后的前端时代,使用前端技术能做哪些事?

    什么是前后端分离,要区分前端和后端,需要有个明确的界限.一般,用户可以直接看到的东西,都是属于前端的范畴,除了前端之外都属于后端了. 在传统的像ASP,JSP和PHP等开发模式中,前端是处在一个混沌的 ...

  6. 手机html input打开数字,html5 input的type属性启动数字输入法

    POJ 2653 Pick-up sticks (线段相交) 题意:给你n条线段依次放到二维平面上,问最后有哪些没与前面的线段相交,即它是顶上的线段 题解:数据弱,正向纯模拟可过 但是有一个陷阱:如果 ...

  7. python--爬虫--爬虫学习路线指南

    目标 拥有爬取大规模数据的能力 爬虫的作用 利用爬虫我们可以获取大量的价值数据,从而获得感性认识中不能得到的信息,比如: 知乎:爬取优质答案,为你筛选出各话题下最优质的内容. 豆瓣: 优质的电影 淘宝 ...

  8. watir安装及中文支持问题

    watir( Web Application Testing in Ruby) 是一款基于ruby的自动化测试工具,使用watir写的语句在执行时,IE(如果使用IE的话)将被运行,并在框中输入内容, ...

  9. 前后端分离的前生今世

    本文从前端开发的视角,聊一聊前后端分离之后的前端开发的那些事儿. 阅读全文,大约需要8分钟. 什么是前后端分离 要区分前端和后端,需要有个明确的界限.一般,用户可以直接看到的东西,都是属于前端的范畴, ...

最新文章

  1. 详细记录如何在跨域请求中携带cookie
  2. 【数据结构与算法】之深入解析“合并K个升序链表”的求解思路与算法示例
  3. 【OS学习笔记】十二 现代处理器的结构和特点
  4. jsp mysql环境_MySQL在JSP环境下的操作应用
  5. 802.1X和NAP整合实验手册
  6. 贪心算法—建立雷达(POJ 1328)
  7. python ndimage_Python ndimage.zoom方法代码示例
  8. 基于北京二手房价数据的探索性数据分析和房价评估——房价评估模型构建
  9. 小甲鱼 OllyDbg 教程系列 (十四) : 模态对话框 和 非模态对话框 之 URlegal 和 movgear...
  10. 金蝶K3工资模块个税计算公式
  11. Mac上利用iTunes制作铃声
  12. 魔兽对战平台服务器更新维护什么,魔兽官方对战平台更新:公会系统正式上线!...
  13. [隐写术] J_UNIWARD介绍
  14. 使用神器vscode代替beyond compare进行文本比较高亮显示
  15. 4p营销组合策略案例_营销组合策略的4P讲解
  16. 使用Python绘制圣诞树教程(附源代码)
  17. PAT A1010 Radix (25 分)
  18. SQL中获取当前时间的函数、在日期上减去指定的天数的函数
  19. JavaScript中的标签语句
  20. 找不到该项目 请确认该项目的位置_裕同拟在上海投7亿建包装新项目;可回收、可再用、可降解的生物基涂层雪糕包装来了;利安德巴塞尔美国50万吨/年PE新装置试车...

热门文章

  1. vue打开后端html文件,vue中怎么请求后端数据?
  2. java自动关闭吗_JAVA问题--浏览器老是自动关闭
  3. 如何通便清肠快速见效_如何三个月合理瘦身减脂
  4. python 会计专用格式_python-2.7 – 如何使用xlsxwriter将格式应用为“文本”和“会计”...
  5. java线程池应用的好处_java高级应用:线程池全面解析
  6. php常用操作数组函数,PHP常见数组函数用法小结
  7. mysql strtolower_GitHub - redfoxli/mysqlstr: a php extension provide string processing of mysql
  8. 傅里叶变换处理音频c++_KWS-SoC——基于Wujian100的音频流关键词检测SoC拓展开发笔记之一...
  9. Java防止Xss注入json_浅谈 React 中的 XSS 攻击
  10. 做 SQL 性能优化真是让人干瞪眼