0%

docsify 是一个动态生成文档网站的工具。不同于 GitBook、Hexo 的地方是它不会将 .md 转成 .html 文件,所有转换工作都是在运行时进行。

需要到docsify-cli来启动web服务器

我在ubuntu18.04的环境下用node v16.19.1 (npm v9.5.1) 可以安装成功,其他环境需要自己试一下

使用nvm安装node

1
nvm install v16

升级npm

1
npm install -g npm@9.5.1

安装docsify-cli

1
npm i docsify-cli -g

初始化文档目录

1
docsify init .

启动web服务器

1
docsify serve .

默认启动在3000端口,可以通过nginx做反向代理

参考

docsify

主页模板

默认生成的index.html不太好用,这里给一个好用的模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>docsify</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0"
/>
<link
rel="stylesheet"
href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/vue.css"
title="vue"
/>
<link
rel="stylesheet"
href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/dark.css"
title="dark"
disabled
/>
<link
rel="stylesheet"
href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/buble.css"
title="buble"
disabled
/>
<link
rel="stylesheet"
href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/pure.css"
title="pure"
disabled
/>
<link
rel="stylesheet"
href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/dolphin.css"
title="dolphin"
disabled
/>
<style>
nav.app-nav li ul {
min-width: 100px;
}

#carbonads {
box-shadow: none !important;
width: auto !important;
}
</style>
</head>

<body>
<div id="app">Loading ...</div>
<script src="//cdn.jsdelivr.net/npm/docsify-plugin-carbon@1"></script>
<script>

// Docsify configuration
window.$docsify = {
auto2top: true,
coverpage: false,
executeScript: true,
loadSidebar: true,
loadNavbar: false,
mergeNavbar: false,
maxLevel: 4,
subMaxLevel: 4,
name: 'docsify',
search: 'auto',
};
</script>
<script src="//cdn.jsdelivr.net/npm/docsify@4/lib/docsify.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/docsify@4/lib/plugins/search.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-bash.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-markdown.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-nginx.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-php.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/vue@2/dist/vue.min.js"></script>
<!-- <script src="//cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script> -->
</body>
</html>

自动生成_sidebar.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# -*- coding: utf-8 -*-
import sys
import os
from functools import cmp_to_key


def mycmp_filename(name1, name2):
if name1.upper() == "README.MD":
return -1
elif name2.upper() == "README.MD":
return 1
elif name1 < name2:
return -1
elif name2 < name1:
return 1
return 0


def gen(e, path, level):
content = ""
name, ext = os.path.splitext(e)
if os.path.isfile(path):
if not name.startswith('_') and ext.lower() == ".md":
if os.path.getsize(path):
content += "{}- [{}]({})\n".format(" "*level, name, path.replace('\\', '/') )
elif os.path.isdir(path):
if name.startswith('_'):
return ""
content2 = ""
for e2 in sorted(os.listdir(path), key=cmp_to_key(mycmp_filename)):
content2 += gen(e2, os.path.join(path, e2), level+1)
if content2:
if name:
content += "{}- {}\n".format(" "*level, name)
content += content2
else:
content = ""
return content


def main():
with open("_sidebar.md", "w", encoding="utf-8") as fp:
fp.write("<!-- docs/_sidebar.md -->\n")
fp.write("{}\n".format(gen("", ".", -1)))

if __name__ == '__main__':
main()

json.loads时报异常

1
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x** in position **: invalid continuation byte

一般是因为编码中有中文,并且和默认的解码方式(utf-8)不匹配造成的,在中国来说通常用最常见的非utf-8编码就是gb2312。(如果你知道里面包含了日语那么则应该尝试按Shift_JIS解码而不是gb2312,等等),另外如果实在解不出,有时候实在解不出或许也可以丢弃,比如注释中的文字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import re
import json

# 删除非ascii的编码
def omit_unascii(data):
new_data = b''
p = 0
m = re.search(b"[^\x00-\x7F]+", data[p:])
while m:
new_data += data[p:p+m.start()]
p += m.end()
m = re.search(b"[^\x00-\x7F]+", data[p:])
new_data += data[p:]
return new_data


def json_loads(msg):
try:
# 先尝试正常解析(按UTF8解码)
obj = json.loads(msg)
except Exception as e:
try:
# 尝试先按GBK解码再解析
obj = json.loads(msg.decode("gb2312", "ignore"))
except Exception as e:
try:
# 尝试删除所有非ascii编码后再解析
obj = json.loads(omit_unascii(msg))
except Exception as e:
raise e
return obj

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# -*- coding: utf-8 -*-
import pymongo
import threading


class _HashedSeq(list):
__slots__ = 'hashvalue'

def __init__(self, tup, hash=hash):
self[:] = tup
self.hashvalue = hash(tup)

def __hash__(self):
return self.hashvalue


def _make_key(args, kw, typed=False):
key = args
if kw:
kwd_mark = (object(),)
key += kwd_mark
for item in kw.items():
key += item
if typed:
key += tuple(type(v) for v in args)
if kw:
key += tuple(type(v) for v in kw.values())
elif len(key) == 1 and type(key[0]) in {int, str}:
return key[0]
return _HashedSeq(key)


local = threading.local()

class MongoDB:
@staticmethod
def instance(*args, **kw):
if not hasattr(local, "mongodb") or local.mongodb is None:
local.mongodb = {}
key = _make_key(args, kw, typed=False)
if key not in local.mongodb:
local.mongodb[key] = MongoDB(*args, **kw)
return local.mongodb[key]

def __init__(self, host="127.0.0.1", port=27017, dbname=None, username=None, password=None, **kw):
self.client = pymongo.MongoClient(f"mongodb://{host}:{port}/{dbname}", username=username, password=password, **kw)
if dbname:
self.db = self.client[dbname]
else:
self.db = self.client.test

def close(self):
return self.client.close()

def __getattr__(self, key):
return self.db[key]

def __getitem__(self, key):
return self.db[key]

def has_collection(self, name):
return name in self.db.list_collection_names()


# 常用运算符
# 逻辑运算
# {$or:[expression1,expression2,...]}
# {$and:[expression1,expression2,...]}
# {$not:expression1}}
# 比较运算
# {field:{$eq:value}} ==
# {field:{$ne:value}} !=
# {field:{$lt:value}} <
# {field:{$lte:value}} <=
# {field:{$gt:value}} >
# {field:{$gte:value}} >=
# {field:{$in:[value1,value2,...]} in
# {field:{$nin:[value1,value2,...]} not in
# 正则匹配
# {field:{"$regex": "正则表达式"}

# query: 查询条件, ex: {"type": "stock"}
# fields: 提取字段, ex: ["code", "sec_name"]
# sort: 排序字段和排序方式, ex: [("code", -1)] 表示按code排倒序
# limit: 之取前limit个, ex: 10
# **kw: 查询条件,把query展开成参数来写,作用是相同的
def get_securities(query={}, fields=None, sort=None, limit=None, **kw):
q = gen_query("securities", query, fields, sort, **kw)
return [doc for doc in (q.limit(limit) if limit else q)]

def gen_query(collection, query={}, fields=None, sort=None, **kw):
mongo = MongoDB.instance(host="192.168.1.99", dbname="ftresearch", username="ftresearch", password="******")
q = mongo[collection].find(dict(**query, **kw), to_projection_dict(fields))
if sort:
q = q.sort(to_sort_list(sort))
return q


def to_projection_dict(fields):
if fields is None:
return {"_id": 0}
elif isinstance(fields, dict):
return fields
else:
ret = {"_id": 0}
for k in fields:
ret[k] = 1
return ret


def to_sort_list(sort):
if not sort:
return None
ret = []
if isinstance(sort, list):
for e in sort:
if isinstance(e, str):
ret.append((e, 1))
else:
ret.append(e)
else:
if isinstance(sort, str):
ret.append((sort, 1))
else:
ret.append(sort)
return ret


if __name__ == '__main__':
# 查询以68编码开头的所有股票,只取code, sec_name字段
arr = get_securities(code={"$regex": "^68"}, type="stock", fields=["code", "sec_name"])
for e in arr:
print(e)

# 查询所有ST状态和*ST状态的未退市的股票信息,按股票代码排倒序,取前10只
arr = get_securities(
query={
"type": "stock",
"contract_state": "Active",
"special_type": {"$in": ["ST", "StarST"]},
},
fields=["code", "sec_name", "exchange", "special_type"],
sort=[("code", -1)],
limit=10)
for e in arr:
print(e)

# 取2023-01-04日当天在交易状态的股指期货合约代码
codes = [e["code"]
for e in get_securities(
fields=["code"],
type="future",
product={"$in": ["IC", "IF", "IH", "IM"]},
listed_date={"$lte":"2023-01-04"},
de_listed_date={"$gte":"2023-01-04"}
)
]
print(codes)

字段名称 样例 获取命令(Linux)
局域网IP LIP=172.19.51.148; ifconfig -a
网卡物理地址 MAC=00163E066936; ifconfig -a
硬盘序列号 HD=TF667AY92GHRVL; lsblk --nodeps -no serial /dev/sda
PC终端设备名 PCN=hostname; hostname
CPU序列号 CPU=bfebfbff21040652; dmidecode -t 4 | grep ID |sort -u |awk -F': ' '{print $2}'
硬盘分区信息 PI=SDA^EXT4^512G; df -hT|grep '/$'|awk '{print $1"^"$2"^"$3}'
系统盘卷标号 VOL=0004-2CC4; fdisk -l|grep -e "Disk identifier" -e "Disk /dev/"

公司对接东方证券和中泰证券的行情都是使用的盛立EFH接口,在东方证券已经部署在实盘正常使用,在中泰证券部署时发现收不到行情。

中泰证券多了个行情接入认证程序,其实怀疑是否这边有问题,但是通过

1
tcpdump -i 网口 udp

或明确指定组播来源

1
tcpdump -i 网口 host 组播IP

可以抓到组播服务器发出的数据包,说明组播到达了网口,那也说明行情接入认证是成功的。

于是深入代码调试,发现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ...
while (true) {
if (m_thrade_quit_flag) {
return NULL;
}

socklen_t len = sizeof(sockaddr_in);

// 始终返回-1
n_rcved = recvfrom(m_sock, line, RCV_BUF_SIZE, 0, (struct sockaddr*)&muticast_addr, &len);
if ( n_rcved < 0) {
// 在此处打印 errno 是 11,即 EAGAIN
continue;
}
else if (0 == n_rcved) {
continue;
}
else {
report_user(EVENT_RECEIVE, m_id, line, n_rcved);
}
}
// ...

因为m_sock被设置成了异步的,所以在没有数据时立刻返回-1,errno==EAGAIN就是正常现象。

搜索recvfrom 始终返回-1 tcpdump可以抓到包发现有人说是被防火墙拦截了。

1
2
service firewalld.service stop
systemctl disable firewalld.service

执行后果然可以收到了,说明tcpdump的处理在防火墙之前,可以抓到被防火墙拦截的数据。

总忘,每次用到都要搜,记录一下

1
2
3
4
5
6
7
8
9
10
11
from functools import cmp_to_key

score = [97, 80, 90, 63, 72, 84]
idx = range(len(score))

# cmp传入的是类似C语言qsort的比较函数,返回负、零、正来表示大小关系
idx = sorted(idx, key=cmp_to_key(lambda i, j: score[i]-score[j]))
print(idx) # idx的序是按照score[idx[i]]的值排的
sorted_score = [score[i] for i in idx]
assert(sorted_score==sorted(score))
print(sorted_score)

在Windows上通过net use,在linux通过mount可以把网络共享路径挂载到本地的一个盘符或一个目录,在Windows上可以直接判断路径是否存在,而在Linux挂载点总是存在的,即使挂载失败也存在一个空的文件夹,所以不能用路径是否存在来判断,可以在df命令中找挂载点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import os
import platform
import traceback


def exec(cmd:str):
pip = os.popen(cmd)
r = pip.buffer.read()
try:
return r.decode(encoding='utf8')
except Exception as e:
try:
return r.decode(encoding='gb2312')
except Exception as e:
return r
return r


def is_mounted(path):
if platform.system() == 'Windows':
return os.path.exists(path)
else:
return int(exec(f"df -h|grep {path}|wc -l")) > 0

常见的背包问题根据背包的重数和最终求的值可以分为下面6类:

1重 多重 无穷重
最大价值 0-1背包 多重背包 完全背包
方案数 0-1背包计数 多重背包计数 完全背包计数

0-1背包

以最普通的0-1背包来说,

问题描述:

n种物品,每种1个,一个容量为C的背包,第i种物品的体积是w[i],价值是v[i],问背包能装下物品的最大价值。(也就是总体积不超过C的最大物品价值)

思路:

dp[i][c]表示前i个物品在容量为c的背包时能装下的最大价值,每个物品都可以取或者不取,在面对第i个物品时,如果取那么

1
dp[i][c] = dp[i-1][c-w[i]]+v[i] if c>=w[i]

如果不取那么

1
dp[i][c] = dp[i-1][c]

并且dp[i][c]对于c应该是单调不减的,因为容量变大总价值不可能减少。

所以有

1
dp[i][c] >= dp[i][c-1]

综上,我们有

1
2
3
4
dp[i][c] = max(dp[i-1][c], dp[i][c-1]);
if(c>=w[i]) {
dp[i][c] = max(dp[i][c], dp[i-1][c-w[i]]+v[i]);
}

有时候要求背包必须是满的,则没有上述单调性,或者我们总是不考虑单调性,而在给出最终答案前去遍历dp[n-1]得到最大值,鉴于此我们把上式简化,并加上循环

1
2
3
4
5
6
7
8
for(int i=0;i<n;i++) {
for(int c=0;c<=C;c++) {
dp[i][c] = dp[i-1][c];
if(c>=w[i]) {
dp[i][c] = max(dp[i][c], dp[i-1][c-w[i]]+v[i]);
}
}
}

注意到dp[i][c] = dp[i-1][c]相当于对于每个新的物品我们总是在前一物品算好的dp数组上做修改,那么能不能把第一维去掉直接修改呢,问题就在于dp[i-1][c-w[i]]我们会用到c-w[i]位置,而它可能在第i趟循环时修改了,如果我们再利用这个状态相当于物品i被用了两次,所以我们需要第i-1趟循环时的值,考虑到只会引用到c更小是位置,我们可以把内层循环反向,这样就不会在第i趟循环时修改到c-w[i]位置的值了。

1
2
3
4
5
for(int i=0;i<n;i++) {
for(int c=C;c>=w[i];c--) {
dp[c] = max(dp[c], dp[c-w[i]]+v[i]);
}
}

循环完成后 max(dp)就是背包能取得的最大价值。

0-1背包算法时间复杂度O(n*C),空间复杂度O(C)

多重背包

问题描述:

n种物品,每种m[i]个,一个容量为C的背包,第i种物品的体积是w[i],价值是v[i],问背包能装下物品的最大价值。

思路:

如果把m[i]个相同的物品视作m[i]种物品则问题可以规约为0-1背包,时间复杂度O(∑m[i]*C),那么能不能利用m[i]个物品是相同的特性来优化时间复杂度呢?是可以的,对于第i种物品,我们不把它视作m[i]个相同的物品,而是把他们按二进制的方式组合,分成1个物品组合,2个物品组合,4个物品组合,……,这样m[i]个物品就被组合成了log(m[i])个物品,再规约为0-1背包,显然不影响最终解。算法时间复杂度O(∑log(m[i])*C),组合成的新物品可以边循环边生成因此没有空间开销,空间复杂度还是O(C)

完全背包

问题描述:

n种物品,每种无穷个,一个容量为C的背包,第i种物品的体积是w[i],价值是v[i],问背包能装下物品的最大价值。

思路:

虽然每种物品个数是无穷的,但背包实际的容量的有限的,对于第i种物品最多装下C/v[i]个,我们可以令m[i]=C/v[i]即可把问题规约为多重背包问题。而实际上还有更简单的解法,我们在思考0-1背包空间降维时遇到的问题,“问题就在于dp[i-1][c-w[i]]我们会用到c-w[i]位置,而它可能在第i趟循环时修改了,如果我们再利用这个状态相当于物品i被用了两次”,而这对于完全背包问题来说正是我们需要的,因此只有把0-1背包内层循环的顺序改成正向的就实现了完全背包。

1
2
3
4
5
for(int i=0;i<n;i++) {
for(int c=w[i];c<=C;c++) {
dp[c] = max(dp[c], dp[c-w[i]]+v[i]);
}
}

循环完成后 max(dp)就是背包能取得的最大价值。

完全背包的复杂度和0-1背包相同,算法时间复杂度O(n*C),空间复杂度O(C)

0-1背包计数

问题描述:

n种物品,每种1个,一个容量为C的背包,第i种物品的体积是w[i],问恰好装满背包的方案数。

思路:

对于计数问题通常会忽略价值,或者认为价值等于体积,用dp[i][c]表示前i个物品在容量为c的背包时恰好装满的方案数,则有

1
dp[i][c] = dp[i-1][c] + (dp[i-1][c-w[i]] if c>=w[i] else 0)

即要么不用第i个物品,要么用第i个物品。

同样我们可以用0-1背包类似的思想把空间降成1维,内层循环用倒序。

1
2
3
4
5
6
dp[0] = 1;
for(int i=0;i<n;i++) {
for(int c=C;c>=w[i];c--) {
dp[c] += dp[c-w[i]];
}
}

多重背包计数

对于多重背包,要明确一点,相同种类的背包视为相同背包(例如5个第i种背包,取3个只会形成1种方案,而不是5C3种方案),那么还是可以按二进制的方式组合。

完全背包计数

问题描述:

n种物品,每种无穷个,一个容量为C的背包,第i种物品的体积是w[i],问恰好装满背包的方案数。

思路:

和完全背包类似思路,直接上代码:

1
2
3
4
5
6
dp[0] = 1;
for(int i=0;i<n;i++) {
for(int c=w[i];c<=C;c++) {
dp[c] += dp[c-w[i]];
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import math

def coord_dist(lat1, lnt1, lat2, lnt2):
sin = math.sin
cos = math.cos
acos = math.acos
deg2rad = math.radians

R = 63713930
lat1 = deg2rad(lat1)
lnt1 = deg2rad(lnt1)
lat2 = deg2rad(lat2)
lnt2 = deg2rad(lnt2)
l = R * acos(cos(lnt1)*cos(lnt2)*cos(lat1-lat2)+sin(lnt1)*sin(lnt2))
return l

CRON时间表达式

我们知道在Linux上的任务计划可以通过cron服务管理,cron服务设置任务运行时间有着自定义的时间表达式,形如

1
2
# m h  dom mon dow   command
58 8 * * 1-5 /root/run.sh

这条记录表示在每周一到周五的08:58自动运行/root/run.sh脚本,除去command,我们看到时间表达式有m, h, dom, mon, dow几个部分,分别表示分钟、小时、每月几号、月份、星期几。几号和星期几通常是二选一,指定了一个另一个就会指定成*表示任意,也可以都指定成*,则表示每天。

dow1-5表示周一到周五,也可以用逗号分割列举具体星期几,例如1,3,5表示每周一三五,除了周几,其他时间项也都可以使用此表示法。

除了用-表示范围,还可以用/表示频率例如:

1
2
# m   h   dom mon dow   command
0/5 9-14 * * 1-5 /root/run.sh

表示每周一~周五,小时为9点到14点,从0分开始每5分钟运行一次脚本,这样每天第一次运行的时间是09:00,每天最后一次运行的时间是14:55,中间都是每隔5分钟运行一次。

扩展CRON时间表达式

增加秒

一些程序允许使用扩展的CRON时间表达式,一般会增加的表示

可能通过第一位也可能通过最后一位表示。

1
s  m   h   dom mon dow

1
m   h   dom mon dow  s

具体要阅读文档来查询。

增加随机

还有一种扩展是增加随机性指定,通过以字母H开头表示

例如

1
H(55-59) 8  *  *  1-5  /root/run.sh

表示周一到周五,每天[08:55, 09:00)之间的一个随机时间运行/root/run.sh,这样通常是为了缓解多个任务同时启动的压力。

Python中通过带时区的datetime使用croniter

1
2
3
4
5
6
7
8
9
10
11
12
13
from croniter import croniter
from datetime import datetime
from dateutil import tz

now = datetime.now().replace(tzinfo=tz.gettz()) # 得到当前时间(带时区)
cron = croniter('0/5 9-14 * * 1-5', now) # 初始化cron对象

dt = cron.get_next(datetime) # 取得下次触发时间,实际上做的是迭代器去next()然后返回
print(dt)
dt = cron.get_next(datetime) # 所在第二次调用的时候,迭代器继续next(),这里返回的是第二次触发的时间
print(dt)
dt = cron.get_prev(datetime) # 同理,这里将返回第一次触发的时间,而不是初始化时间之前的上一次触发时间
print(dt)

有时候我们即需要上次触发时间,又需要下次触发时间,要么我们理解了迭代器的方式通过取prev再取next的next,要么我们创建两个cron对象分别取prev和next。

croniter允许通过datetime对象或者timestamp(即time.time()的返回值,float类型)来使用cron对象,但是我们的时间表达式是带有时区概念的,所以cron对象必须明确知道我们的时区,否则将不能正确工作,一种方式是我们使用datetime初始化cron对象,取next或prev的时候也使用datetime类型接收,这时候即使时区不明确也不要紧,但是不能使用timestamp接收next和prev的返回值,否则cron对象会默认使用UTC时区导致返回的float值不正确,如果我们使用带时区的datetime来初始化cron对象则没有这个问题,上面例子就是通过带时区的方式初始化的。

另外,python的cron对象是同时支持秒扩展和随机扩展的,秒放在最后一位,使用随机需要在初始化时指定hash_id,不同的hash_id将影响随机结果,例如我还是上面的例子,但是我需要触发时间不在分钟的整点,而在这一分钟内随机的秒数上,就可以写成下面这样

1
cron = croniter('0/5 9-14 * * 1-5 H(0-59)', now, hash_id="hash_id")