AWS Lambda 中的 Cron 表达式解析器





3.00/5 (1投票)
在 AWS Lambda 上使用 Node.js 编写的 Cron 表达式解析器
引言
这个项目对我来说有点像一次练习。我最近刚开始学习 AWS 的基础知识,所以我想通过创建一个简单的 API 来测试 Lambda 函数。本文将介绍如何使用 AWS Lambda 创建 API 函数,使用 AWS Amplify 创建一个简单的网站,并通过 AWS API Gateway 将两者连接起来。
Lambda 函数本身是一个用 Node.js 编写的 Cron 表达式解析器。它以 Cron 表达式作为参数,计算并返回该 Cron 表达式触发事件的下一个时间。Cron 解析器使用在该 文章 中发布和解释的 RegEx 表达式。
代码解释
我使用了两个不同版本的 Cron 表达式,一个简单版 (POSIX) 和一个扩展版 (两个版本都在该 文章 中进行了说明)。
通过 API 事件收到的表达式首先会针对这两个表达式进行测试,如果匹配成功,就会被拆分成数组进行解析。
var matches_POSIX = regex_POSIX.exec(testexp);
var matches_EXT = regex_EXT.exec(testexp);
var arr = [];
if(matches_POSIX === null && matches_EXT === null) {
console.log("Expression does not match!!!");
return null;
}
else if(matches_POSIX !== null) {
console.log("Expression is POSIX");
arr.push("*");
matches_POSIX.forEach((m, i) => i != 0 ? arr.push(m) : null);
arr.push("*");
if(arr[3] != "?" && arr[5] == "*") arr[5] = "?";
if(arr[5] != "?" && arr[3] == "*") arr[3] = "?";
}
else {
console.log("Expression is EXTENDED");
matches_EXT.forEach((m, i) => i != 0 ? arr.push(m) : null);
}
主函数是 getNextTime
。它会遍历数组的所有元素,并调用 resolve
函数,传入适当的参数。它还会测试解析后的值是否有错误,基本上是在返回数组的第一个元素中值为 -1
。
解析的顺序是:年-月-星期几-日-小时-分钟-秒。之所以采用这种有点“倒序”的方法,是因为,例如,要知道一个月有多少天,我们需要知道是哪一年;要知道特定日期的星期几,我们需要知道月份和年份,等等。
在确定 Cron 事件将被触发的下一个时刻时,我们也会采用这种方法,因为我们希望通过逐渐缩小时间范围来找到未来最近的可能日期;这将在后面的文本中进行更详细的解释。
Resolve 函数
该函数被分为几个部分,取决于正在评估的 Cron 表达式的哪个部分——这是通过第二个参数传递的;参数值的含义在每个 case 上方的注释中进行了说明。
switch(i) {
// year
case 6:
if(m === "*") return Array.apply(null, Array(10)).map(function(e, i)
{ return ts.getFullYear() + i; }); // current and next 9 years
if(!m.includes("-") && !m.includes(","))
return [parseInt(m, 10)]; // single value
return resolveList(m, i); // list of years
// month
case 4:
if(m === "*") return Array.apply(null,
Array(12)).map(function(e, i) { return i; }); // return all months
if(!m.includes("-") && !m.includes(",") && !isNaN(parseInt(m, 10)))
return [parseInt(m, 10)-1]; // single value numeric
if(isNaN(parseInt(m, 10)) && month2num(m) != -1)
return [month2num(m)-1]; // single value string
return resolveList(m, i).map(function(e,i)
{ return (e > 0 ? e-1 : e) }); // list of months
// day of month
case 3:
if(m === "?") return undefined; // empty - use dw instead
if(m === "*") return Array.apply(null, Array
(new Date(yyyy, mm+1, 0).getDate())).map(function(e, i)
{ return i + 1; }); // return all days
if(m.replace(/L|W/g,"").length != m.length)
return null; // last (week)day of month - skip,
// resolve outside this function
if(!m.includes("-") && !m.includes(","))
return [parseInt(m, 10)]; // single value
return resolveList(m, i); // list of days
// day of week
case 5:
if(m.replace(/L|#/g,"").length != m.length)
return null; // just ignore special cases,
// to be resolved outside this function
if(m === "?") return undefined; // empty - use dd instead
if(m === "*") return Array.apply(null, Array(7)).map
(function(e, i) { return i; }); // return all days
if(!m.includes("-") && !m.includes(","))
return [parseInt(m, 10)]; // single value numeric
if(isNaN(parseInt(m, 10)) && day2num(m) != -1)
return [day2num(m)]; // single value string
return resolveList(m, i); // list of days
// hour
case 2:
if(m === "*") return Array.apply(null,
Array(24)).map(function(e, i) { return i; }); // return all hours
if(!m.includes("-") && !m.includes(",") && !m.includes("/"))
return [parseInt(m, 10)]; // single value
return resolveList(m, i); // list of hours
// min / sec
case 1: case 0:
if(m === "*") return Array.apply(null, Array(60)).map(function(e, i)
{ return i; }); // return all min/sec
if(!m.includes("-") && !m.includes(",") && !m.includes("/"))
return [parseInt(m, 10 )]; // single value
return resolveList(m, i); // list of min/sec
}
resolve
函数尝试返回 Cron 每个部分的所有可能值的数组 (年份除外;由于年份是无限的,该函数将返回最多 10 年内的未来值)。
如果提供了星号 (*
),这意味着所有值都是可能的,因此首先使用 Array.apply
函数创建一个数组,以 null
作为初始元素值,然后使用 map
函数设置元素,例如 12 个月。
if(m === "*") return Array.apply(null, Array(12)).map(function(e, i)
{ return i; }); // return all months
可能的输出是所有值 (*) 、单个值,或者列表,这些列表可以是以逗号分隔的列表或范围——这在一个名为 resolveList
的单独函数中进行解析。
function resolveList(m, i) {
var retValue = [];
var msplit;
var k;
var limit;
if(m.includes("-")) { // all in between
msplit = m.split("-").map(function(e) {
if(i == 4) e = month2num(e);
if(i == 5) e = day2num(e);
return parseInt(e, 10);
});
if (msplit[0] < msplit[1]) {
for(k = msplit[0]; k <= msplit[1]; k++) retValue.push(k);
}
else {
console.log("error: illegal expression " + m);
retValue.push(-1);
}
return retValue;
}
else if(m.includes(",")) { // all listed
m.split(",").map(function(e) { // convert to int
return parseInt(e, 10);
}).forEach(k => { // remove duplicates
//console.log("currentValue=" + k + " ; retValue=" + retValue.toString());
if(!retValue.includes(k)) retValue.push(k);
});
return retValue.sort(); // sort
//m.split(",").forEach(k => retValue.push(parseInt(k)));
}
else if(m.includes("/")) {
msplit = m.split("/");
if(msplit[0] == "*") msplit[0] = "0";
msplit = msplit.map(function(e) {
return parseInt(e, 10);
});
if(i <= 1) limit = 60; // seconds/minutes
if(i == 2) limit = 12; // seconds/minutes
for(k = msplit[0] + msplit[1]; k <= limit;
k = k + msplit[1]) retValue.push(k == limit ? 0 : k);
}
return retValue;
}
特殊情况
所有特殊值,例如 L、W 或 _#_,都在 resolve
函数之外进行解析,因为只有在我们已经缩小了时间范围后才能解析它们。它们在 specialDay
和 specialDate
函数中进行解析,这两个函数直接从主函数中调用。
function specialDate(exp, mm, yyyy) {
if(exp == "L")
return new Date(yyyy, mm + 1, 0).getDate();
if(exp == "LW")
for(var i = new Date(yyyy, mm + 1, 0).getDate(); i > 0; i--) {
if([1, 2, 3, 4, 5].includes(new Date(yyyy, mm, i).getDay()))
return i;
}
if(exp.substring(0, 1) == "L-")
if(!isNaN(parseInt(exp.substring(2), 10)))
return new Date(yyyy, mm + 1, 0).getDate()+1 -
parseInt(exp.substring(2), 10);
}
function specialDay(exp, mm, yyyy) {
if(exp.includes("L"))
for(var i = new Date(yyyy, mm + 1, 0).getDate(); i > 0; i--)
{ // start from end of month and look for last specified weekday in that month
if(parseInt(exp.substring(0,1),10) == new Date(yyyy, mm, i).getDay())
return i;
}
if(exp.includes("#")) {
var n = 0;
for(i = 1; i <= new Date(yyyy, mm + 1, 0).getDate(); i++)
{ // start from beginning of the month and count (n)
// the occurences of specified weekday in that month
if(parseInt(exp.substring(0,1),10) == new Date(yyyy, mm, i).getDay()) {
n++;
if(n == parseInt(exp.substring(2,3),10)) return i;
i = i+6;
}
}
}
return undefined;
}
AWS 设置
AWS Lambda
转到 AWS Lambda 并点击“创建函数”。
选择“从头开始创作”,输入函数名称,例如“cron-parse
”,在“运行时”下选择“Node.js”并点击“创建函数”。
您将看到一个包含“函数概览”和“代码源”部分的屏幕。
将您的代码粘贴到“index.js”下,然后点击“部署”。
在“测试”下拉菜单 (箭头菜单) 下,点击“配置测试事件”。通过此操作,您可以指定一个测试表达式 (JSON) 并查看结果输出。例如,您可以输入
{
"key1": "5/15 12-15 3-5 ? 7,5,3 WED-FRI 2021-2022"
}
为您的事件命名,假设为 test1
,然后按“创建”按钮。
然后,当您点击“测试”按钮时,您应该会看到此输出:
AWS API Gateway
转到 AWS API Gateway 并点击“创建 API”。
点击“REST API”下的“构建”。
选择“REST – 新建 API”。输入名称,例如 cronAPI
。
在“端点类型”下,选择“Edge 优化”。
点击“创建 API”。
在“资源”下,转到“操作”菜单并选择“创建方法”。
在创建的方法中,选择“POST”并单击确认标记。
选择“Lambda 函数”作为“集成类型” (确保您使用的是与创建 Lambda 函数相同的区域),输入您的 Lambda 函数名称 (cron-parse
),然后点击“保存”,接着点击“确定”。
再次转到“操作”菜单,选择“启用 CORS”,然后点击“启用 CORS 并替换现有...”。
在“操作”菜单中,选择“部署 API”,“部署阶段” = “新阶段”,输入阶段名称 (例如:dev
),然后点击“部署”。
复制页面顶部的“调用 URL”——稍后您将需要它。
AWS Amplify
转到 AWS Amplify,一直向下滚动,然后在“开发”块中点击“开始”。
输入应用程序名称,例如“Cron parser”。确认部署,然后放松并享受 AWS 在后台为您完成所有工作。
转到“前端环境”选项卡,选择“不使用 Git 提供商进行部署”并点击“连接分支”。
在您的计算机上 (本地),使用以下代码创建一个名为“index.html”的简单 HTML 文件。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Cron parser</title>
<script>
// define the callAPI function that takes a first name and last name as parameters
var callAPI = (key1)=>{
// instantiate a headers object
var myHeaders = new Headers();
// add content type header to object
myHeaders.append("Content-Type", "application/json");
// using built in JSON utility package turn object
// to string and store in a variable
var raw = JSON.stringify({"key1":key1});
// create a JSON object with parameters for API call and store in a variable
var requestOptions = {
method: 'POST',
headers: myHeaders,
body: raw,
redirect: 'follow'
};
// make API call with parameters and use promises to get response
fetch("YOUR API INVOKE URL HERE", requestOptions)
.then(response => response.text())
.then(result => document.getElementById('result').innerHTML =
result/*alert(JSON.parse(result).body)*/)
.catch(error => document.getElementById('result').innerHTML =
error/*console.log('error', error)*/);
}
</script>
</head>
<body>
<label for="incron">Enter Cron expression here </label>
<input type="text" name="incron" id="incron">
<button type="button" onclick="callAPI(document.getElementById('incron').value)">
Submit</button><br/><hr/><br/>
<label for="result">Result: </label><div name="result" id="result"></div>
</body>
</html>
将“YOUR API INVOKE URL HERE”替换为您在上一步中保存的 URL。
保存“index.html”文件,然后将其 ZIP (压缩) 起来 (您可以随意命名 ZIP 文件)。
现在返回 AWS 控制台 (您的 Amplify 应用),点击“选择文件”,然后打开包含“index.html”的 zip 文件。保存并部署。
就是这样。现在您可以使用“域名”下的 URL 来打开您的 HTML 网页。输入一个 Cron 表达式进行测试,然后点击 提交,您的 API 应该会在 结果 下返回一个值。
历史
- 2021 年 8 月 29 日:初始版本