shell工具之一:sed
sed基础
sed编辑器被称作流编辑器,与常见的交互式文本编辑器刚好相反。文本编辑器可以通过键盘来交互式地插入、删除、替换文本中的数据;而流编辑器是基于一组预先的规则来编辑数据流。
sed命令的格式如下:
sed options script file
选项 |
说明 |
-e script |
将script中指定的命令添加到运行的命令中 |
-f file |
将file中指定的命令添加到运行的命令中 |
-n |
不为每个命令生成输出,等待print命令来输出 |
说明:
script用于指定作用在数据量上的单个命令。
如果需要使用多个命令,有两种选择:可以在命令行中使用-e选项指定,不同命令之间用分号隔开;或者使用-f选项在文件中指定。
默认情况下,sed编辑器将指定的命令应用到STDIN输入流上,而不作用于数据源本身,就是说sed不会修改文本文件中的原数据。
1 替换命令substitute
s/pattern/replacement/flags
flags取值如下:
数字:表示replacement将替换每行中第几次出现的pattern
g:表示replacement将替换所有出现的pattern
p:打印用replacement替换过的行(经常与-n选项搭配使用,-n禁止sed输出,而p会输出修改过的行,二者搭配则仅输出被replacement替换过的行)
w file:将替换的结果写到file文件中(只有被替换过的行才会保存到输出文件file中)
[root@benxintuzi shell]# cat data1 benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin [root@benxintuzi shell]# sed 's/benxin/tuzi/3' data1 benxin benxin tuzi benxin benxin benxin benxin tuzi benxin benxin benxin benxin tuzi benxin benxin [root@benxintuzi shell]# sed 's/benxin/tuzi/g' data1 tuzi tuzi tuzi tuzi tuzi tuzi tuzi tuzi tuzi tuzi tuzi tuzi tuzi tuzi tuzi [root@benxintuzi shell]# cat data2 benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin hello hello hello hello hello hello [root@benxintuzi shell]# sed -n 's/benxin/tuzi/p' data2 tuzi benxin benxin benxin benxin tuzi benxin benxin benxin benxin tuzi benxin benxin benxin benxin [root@benxintuzi shell]# sed 's/benxin/tuzi/w out' data2 tuzi benxin benxin benxin benxin tuzi benxin benxin benxin benxin tuzi benxin benxin benxin benxin hello hello hello hello hello hello [root@benxintuzi shell]# cat out tuzi benxin benxin benxin benxin tuzi benxin benxin benxin benxin tuzi benxin benxin benxin benxin
2 指定作用行
默认情况下,sed会作用于所有行,如果只想作用于特定行,必须使用行寻址,格式如下:
[address]command或者 address { command1 command2 command3 ... }
address可以使用单个行号,或者是起始行号、逗号、结尾行号指定。
[root@benxintuzi shell]# sed '2s/benxin/tuzi/' data1 benxin benxin benxin benxin benxin tuzi benxin benxin benxin benxin benxin benxin benxin benxin benxin [root@benxintuzi shell]# sed '2,$s/benxin/tuzi/' data1 benxin benxin benxin benxin benxin tuzi benxin benxin benxin benxin tuzi benxin benxin benxin benxin [root@benxintuzi shell]# sed '2,${ s/benxin/tuzi/3 s/hello/world/2 }' data2 benxin benxin benxin benxin benxin benxin benxin tuzi benxin benxin benxin benxin tuzi benxin benxin hello world hello hello hello hello
delete命令会删除指定模式的所有行,如果没有加入行寻址,则会删除流中的所有文本(并不修改原文件)。
3 插入和追加
insert命令i在指定行前增加一个新行;
append命令a在指定行后增加一个新行。
格式如下:
sed '[address]command\new line'
[root@benxintuzi shell]# cat data1 benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin [root@benxintuzi shell]# sed '3i\tuzi tuzi tuzi' data1 benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin tuzi tuzi tuzi benxin benxin benxin benxin benxin [root@benxintuzi shell]# sed '3a\tuzi tuzi tuzi' data1 benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin tuzi tuzi tuzi
4 转换命令transform
转换命令y可以处理单个字符,格式如下:
[address]y/inchars/outchars/
用outchars的第一个字符替换inchars的第一个字符,outchars的第二个字符替换inchars的第二个字符,...,如果inchars和outchars的长度不同,则sed编辑器产生一个错误。
[root@benxintuzi shell]# sed 'y/bn/ti/' data1 teixii teixii teixii teixii teixii teixii teixii teixii teixii teixii teixii teixii teixii teixii teixii
5 打印命令
p: 打印文本行
=: 打印行号
l: 打印文本行和不可打印的ASCII字符
[root@benxintuzi shell]# cat data2 benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin hello hello hello hello hello hello [root@benxintuzi shell]# sed -n '2,4p' data2 benxin benxin benxin benxin benxin benxin benxin benxin benxin benxin hello hello hello hello hello hello [root@benxintuzi shell]# sed '=' data2 1 benxin benxin benxin benxin benxin 2 benxin benxin benxin benxin benxin 3 benxin benxin benxin benxin benxin 4 hello hello hello hello hello hello [root@benxintuzi shell]# sed -n '/hello/{ > = > p > }' data2 4 hello hello hello hello hello hello [root@benxintuzi shell]# sed -n 'l' data2 benxin benxin benxin benxin benxin$ benxin benxin benxin benxin benxin$ benxin benxin benxin benxin benxin$ hello hello hello hello hello hello$
6 文件命令
[address]w filename
[address]r filename
[root@benxintuzi shell]# cat data3 This is the line 1 This is the line 2 This is the line 3 This is the line 4 [root@benxintuzi shell]# sed -n '3,4w outfile' data3 [root@benxintuzi shell]# cat outfile This is the line 3 This is the line 4 [root@benxintuzi shell]# sed '1r outfile' data3 This is the line 1 This is the line 3 This is the line 4 This is the line 2 This is the line 3 This is the line 4
sed进阶
1 多行命令
N: 将数据流中的下一行加进来创建一个多行组
D: 删除多行组中的第一行
P: 打印多行组中的第一行
[root@benxintuzi shell]# cat data3 This is the line 1 This is the line 2 This is the line 3 This is the line 4 [root@benxintuzi shell]# sed '/line 2/{N; s/line/number/g}' data3 This is the line 1 This is the number 2 This is the number 3 This is the line 4
2 跳转命令
[address]b [label]
address决定了哪些行会触发跳转命令;label定义了要跳转的位置,如果没有label参数,那么将跳转到脚本的末尾。定义label:开始,最多可以有7个字符。
[root@benxintuzi shell]# cat data3 This is the line 1 This is the line 2 This is the line 3 This is the line 4 [root@benxintuzi shell]# sed '{2,3b; s/line/number/g}' data3 This is the number 1 This is the line 2 This is the line 3 This is the number 4 [root@benxintuzi shell]# sed '{ 2,3b s/line/number/g :label s/the/a/g }' data3 This is a number 1 This is the line 2 This is the line 3 This is a number 4
3 测试命令
[address]t [label]
测试命令的跳转不是基于地址,而是基于替换命令是否成功。如下程序每次去除一个逗号,直到一个逗号都没有时结束循环跳转。
[root@benxintuzi shell]# echo "This,is,a,test,to,remove,commas." | sed -n '{ :start s/,/ /p t start }' This is,a,test,to,remove,commas. This is a,test,to,remove,commas. This is a test,to,remove,commas. This is a test to,remove,commas. This is a test to remove,commas. This is a test to remove commas.
4 模式替换
在行The cat sleeps in a hat中,如果想将cat和hat都加上引号,如何做呢?sed中可以使用&表示找到的匹配模式:
[root@benxintuzi shell]# echo "The cat sleeps in a hat" | sed 's/.at/".at"/g' The ".at" sleeps in a ".at" [root@benxintuzi shell]# echo "The cat sleeps in a hat" | sed 's/.at/"&"/g' The "cat" sleeps in a "hat"
5 sed实例工具
# 加倍行间距G:$!G表示最后一行不加倍 [root@benxintuzi shell]# cat data3 This is the line 1 This is the line 2 This is the line 3 This is the line 4 [root@benxintuzi shell]# sed '$!G' data3 This is the line 1 This is the line 2 This is the line 3 This is the line 4 [root@benxintuzi shell]#
# 行编号工具: [root@benxintuzi shell]# cat data3 This is the line 1 This is the line 2 This is the line 3 This is the line 4 [root@benxintuzi shell]# sed '=' data3 | sed 'N; s/\n/\t/' 1 This is the line 1 2 This is the line 2 3 This is the line 3 4 This is the line 4
# 删除多余的空白行: # 关键在于创建一个包含非空白行和一个紧挨空白行的地址区间,匹配该区间则不执行删除。 # /./,/^$/!d,/./表示至少包含一个字符的行,/^$/表示一个空行: [root@benxintuzi shell]# cat data4 This is the line 1 This is the line 2 This is the line 3 This is the line 4 [root@benxintuzi shell]# sed '/./,/^$/!d' data4 This is the line 1 This is the line 2 This is the line 3 This is the line 4
# 删除开头的空行: [root@benxintuzi shell]# cat data5 This is the line 1 This is the line 2 This is the line 3 This is the line 4 [root@benxintuzi shell]# sed '/./,$!d' data5 This is the line 1 This is the line 2 This is the line 3 This is the line 4
# 删除结尾的空行: [root@benxintuzi shell]# cat data6 This is the line 1 This is the line 2 This is the line 3 This is the line 4 [root@benxintuzi shell]# sed '{ > :start > /^\n*$/{$d; N; b start} > }' data6 This is the line 1 This is the line 2 This is the line 3 This is the line 4 [root@benxintuzi shell]#
shell工具之二:gawk
gawk基础
gawk程序是unix中原始awk程序的GNU版本。gawk提供了一种编程环境,允许修改和重新组织文件中的数据。格式如下:
gawk options program file
选项 |
说明 |
-F fs |
指定行中分隔数据字段的分隔符 |
-f file |
指定gawk程序的文件名 |
-v var=value |
定义gawk程序中的一个变量及其默认值 |
-mf N |
指定要处理的数据文件中的最大字段数 |
-mr N |
指定要处理的数据文件中的最大行数 |
-W keyword |
指定gawk的兼容模式或警告等级 |
gawk程序使用的脚本命令必须放在用单引号括起来的花括号中:
gawk '{print "hello benxintui"}'
gawk在处理文本文件时,自动为行中的每个数据字段分配一个变量:
"$0"表示整行;
"$1"表示行中第1个字段;
"$2"表示行中第2个字段;
...
"$n"表示行中第n个字段。
gawk还可以指定程序何时运行。
在处理数据前运行脚本,可以使用BEGIN关键字:
[root@benxintuzi shell]# cat data3 This is the line 1 This is the line 2 This is the line 3 This is the line 4 [root@benxintuzi shell]# gawk 'BEGIN{print "The data3 File Contents:"} {print $0}' data3 The data3 File Contents: This is the line 1 This is the line 2 This is the line 3 This is the line 4
在处理数据后运行脚本,可以使用END关键字:
[root@benxintuzi shell]# gawk 'BEGIN{print "The data3 File Contents:"} {print $0} END{print "End of File"}' data3 The data3 File Contents: This is the line 1 This is the line 2 This is the line 3 This is the line 4 End of File
gawk进阶
1 使用变量
内置变量:
1 字段和行分隔变量
变量 |
说明 |
FS |
输入字段分隔符(默认为空格) |
RS |
输入行分隔符(默认为换行符) |
OFS |
输出字段分隔符(默认为空格) |
ORS |
输出行分隔符(默认为换行符) |
[root@benxintuzi shell]# cat data7 This,is,the,line,1 This,is,the,line,2 This,is,the,line,3 This,is,the,line,4 [root@benxintuzi shell]# gawk 'BEGIN{FS=","; OFS="<-->"; RS="\n"; ORS="|\n"} {print $1,$2,$3,$4,$5}' data7 This<-->is<-->the<-->line<-->1| This<-->is<-->the<-->line<-->2| This<-->is<-->the<-->line<-->3| This<-->is<-->the<-->line<-->4| [root@benxintuzi shell]# gawk 'BEGIN{FS=","; OFS="<-->"; RS="\n"; ORS="|\n"} {print $0}' data7 This,is,the,line,1| This,is,the,line,2| This,is,the,line,3| This,is,the,line,4|
2 数据变量
变量 |
说明 |
ARGC |
当前命令行参数个数 |
ARGV |
当前命令行参数数组 |
ARGIND |
当前文件在ARGV中的位置 |
CONVFMT |
数字的转换格式,默认为%.6g |
OFMT |
数字的输出格式,默认为%.6g |
ENVIRON |
当前shell环境变量及其值组成的关联数组 |
ERRNO |
错误号 |
FILENAME |
数据文件名 |
FNR |
数据文件中的当前行数 |
NF |
数据文件中每行的字段数 |
NR |
已处理的数据行数(如果同时处理多个文件,则该值会不断累加) |
IGNORECASE |
当其值非0时,忽略gawk命令中字符串的大小写 |
RLENGTH |
由match匹配的子字符串的长度 |
RSTART |
由match匹配的子字符串的起始位置 |
[root@benxintuzi shell]# gawk 'BEGIN{print ARGC, ARGV[0], ARGV[1]}' data7 2 gawk data7 [root@benxintuzi shell]# gawk 'BEGIN{print ENVIRON["HOME"]; print ENVIRON["PATH"]}' /root /usr/lib/qt-3.3/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin:/usr/java/jdk1.7.0_75/bin:/usr/java/jdk1.7.0_75/jre/bin:/bigdata/hadoop-2.6.0/bin:/bigdata/hadoop-2.6.0/sbin:/usr/maven/apache-maven-3.3.3/bin:/home/benxintuzi/bin [root@benxintuzi shell]# gawk ' BEGIN{FS=","} {print "FILENAME="FILENAME, "ARGIND="ARGIND, "NF="NF", FNR="FNR, "NR="NR}' data7 data7 FILENAME=data7 ARGIND=1 NF=5, FNR=1 NR=1 FILENAME=data7 ARGIND=1 NF=5, FNR=2 NR=2 FILENAME=data7 ARGIND=1 NF=5, FNR=3 NR=3 FILENAME=data7 ARGIND=1 NF=5, FNR=4 NR=4 FILENAME=data7 ARGIND=2 NF=5, FNR=1 NR=5 FILENAME=data7 ARGIND=2 NF=5, FNR=2 NR=6 FILENAME=data7 ARGIND=2 NF=5, FNR=3 NR=7 FILENAME=data7 ARGIND=2 NF=5, FNR=4 NR=8
自定义变量:
gawk变量名区分大小写。
[root@benxintuzi shell]# gawk ' BEGIN{ testing="This is a test" print testing testing=45 print testing }' This is a test 45 # 在命令行中给gawk变量赋值: [root@benxintuzi shell]# cat script1 BEGIN{FS=","} {print $n} [root@benxintuzi shell]# gawk -f script1 n=2 data7 is is is is [root@benxintuzi shell]# gawk -f script1 n=5 data7 1 2 3 4
2 操作数组
# 数组的定义 [root@benxintuzi shell]# gawk ' > BEGIN{ > capital["China"]="beijing" > print capital["China"] > var[1]=20 > var[2]=10 > total=var[1] + var[2] > print total > }' beijing 30 # 数组的遍历 [root@benxintuzi shell]# gawk ' BEGIN{ var["a"]=1 var["b"]=2 var["c"]=3 for (dex in var) { print "Index:" dex " --- " "Value:" var[dex] } }' Index:a --- Value:1 Index:b --- Value:2 Index:c --- Value:3 # 数组元素的删除 [root@benxintuzi shell]# gawk ' BEGIN{ var["a"]=1 var["b"]=2 var["c"]=3 for (dex in var) { print "Index:" dex " --- " "Value:" var[dex] } delete var["b"] print "-------------------" for (dex in var) { print "Index:" dex " --- " "Value:" var[dex] } }' Index:a --- Value:1 Index:b --- Value:2 Index:c --- Value:3 ------------------- Index:a --- Value:1 Index:c --- Value:3
3 使用模式
使用正则表达式来匹配时,表达式必须放到作用脚本的花括号左边:
[root@benxintuzi shell]# cat data7 This,is,the,line,1 This,is,the,line,2 This,is,the,line,3 This,is,the,line,4 [root@benxintuzi shell]# gawk 'BEGIN{FS=","} /,2/{print $0}' data7 This,is,the,line,2
使用匹配操作符˜来匹配数据行中的特定字段,如找出以1开头的第二个字段的行,如下所示:
[root@benxintuzi shell]# cat out1 This is a test program 2684 Loop 1 Loop 2 Loop 3 Loop 4 Loop 5 Loop 6 Loop 7 Loop 8 Loop 9 Loop 10 This is the end of program [root@benxintuzi shell]# gawk '$2 ~ /^1/{print $0}' out1 Loop 1 Loop 10
还可以使用比较表达式(== / <= / < / >= / >)来匹配,如显示所有属于root用户组的系统用户(组ID==0):
[root@benxintuzi shell]# cat /etc/passwd root:x:0:0:root:/root:/bin/bash bin:x:1:1:bin:/bin:/sbin/nologin daemon:x:2:2:daemon:/sbin:/sbin/nologin adm:x:3:4:adm:/var/adm:/sbin/nologin lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin sync:x:5:0:sync:/sbin:/bin/sync shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown halt:x:7:0:halt:/sbin:/sbin/halt mail:x:8:12:mail:/var/spool/mail:/sbin/nologin uucp:x:10:14:uucp:/var/spool/uucp:/sbin/nologin operator:x:11:0:operator:/root:/sbin/nologin games:x:12:100:games:/usr/games:/sbin/nologin gopher:x:13:30:gopher:/var/gopher:/sbin/nologin ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin nobody:x:99:99:Nobody:/:/sbin/nologin dbus:x:81:81:System message bus:/:/sbin/nologin usbmuxd:x:113:113:usbmuxd user:/:/sbin/nologin vcsa:x:69:69:virtual console memory owner:/dev:/sbin/nologin rtkit:x:499:497:RealtimeKit:/proc:/sbin/nologin avahi-autoipd:x:170:170:Avahi IPv4LL Stack:/var/lib/avahi-autoipd:/sbin/nologin pulse:x:498:496:PulseAudio System Daemon:/var/run/pulse:/sbin/nologin haldaemon:x:68:68:HAL daemon:/:/sbin/nologin ntp:x:38:38::/etc/ntp:/sbin/nologin apache:x:48:48:Apache:/var/www:/sbin/nologin saslauth:x:497:76:"Saslauthd user":/var/empty/saslauth:/sbin/nologin postfix:x:89:89::/var/spool/postfix:/sbin/nologin abrt:x:173:173::/etc/abrt:/sbin/nologin gdm:x:42:42::/var/lib/gdm:/sbin/nologin sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin tcpdump:x:72:72::/:/sbin/nologin benxintuzi:x:500:500:CentOS:/home/benxintuzi:/bin/bash [root@benxintuzi shell]# gawk -F: '$4 == 0{print $1}' /etc/passwd root sync shutdown halt operator
4 结构化命令
if格式: if (condition) { statements } else { statements } |
if (condition) statements; else statements
|
while格式: while (condition) { statements } |
do { statements } while (condition) |
for格式: for (assignments; condition; iteration) |
|
5 内置函数
数学函数 |
说明 (采用弧度为单位) |
atan2(x, y) |
x/y的反正切 |
cos(x) |
x的余弦值 |
exp(x) |
x的指数函数 |
int(x) |
x的整数部分 |
rand() |
0~1的随机浮点数 |
sin(x) |
x的正弦值 |
sqrt(x) |
x的平方根 |
srand(x) |
种子函数 |
位操作函数 |
说明 |
and(v1, v2) |
v1与v2按位与 |
or(v1, v2) |
v1与v2按位或 |
compl(val) |
对val求补 |
lshift(val, count) |
将val左移count位 |
rshift(val, count) |
将val右移count位 |
xor(v1, v2) |
v1与v2按位异或 |
字符串函数 |
说明 |
asort(s [, d]) |
对数组s按数据元素排序。排序后数组的索引值变为表示顺序的数字。如果指定了d,则将排序后的数组存储到d中 |
asorti(s [, d]) |
对数组s按索引值进行排序。排序后数组的元素为表示顺序的索引值。如果指定了d,则将排序后的数组存储到d中 |
gensub(r, s, h [, t]) |
查找变量$0或者目标字符串t来匹配正则表达式r。如果h以g/G开头,则用s替换掉匹配的文本;如果h为数字,表示要替换掉第几处匹配的文本 |
gsub(r, s [, t]) |
查找变量$0或者目标字符串t来匹配正则表达式r。如果找到了,就全部替换为字符串s |
index(s, t) |
返回t在s中的索引值。如果没找到,则返回0 |
length([s]) |
返回字符串s的长度。如果没有指定s,则返回$0的长度 |
match(s, r, [, a]) |
返回字符串s中正则表达式r位置的索引值。如果指定了a,则将s中匹配r的部分存储到a中 |
split(s, a [, r]) |
将s用FS字符或者r分隔开存储到a中,返回被分割的行数 |
sprintf(format, variables) |
类似于printf |
sub(r, s [, t]) |
在变量$0或者目标字符串t中查找正则表达式r的匹配,如果找到了,就用s替换掉第一处匹配 |
substr(s, i [, n]) |
返回s中从索引值i开始的n个字符构成的字符串。如果未指定n,则返回i开始的所有部分 |
tolower(s) |
将s中的所有字符转换为小写 |
toupper(s) |
将s中的所有字符转换为大写 |
# 按数组元素排序 [root@benxintuzi shell]# gawk ' BEGIN{ var["d"]=4 var["b"]=3 var["a"]=2 var["c"]=1 asort(var, result) for (i in result) print "index:" i " <---> " "value:" result[i] }' index:4 <---> value:4 index:1 <---> value:1 index:2 <---> value:2 index:3 <---> value:3 # 按数组索引排序 [root@benxintuzi shell]# gawk ' BEGIN{ var["d"]=4 var["b"]=3 var["a"]=2 var["c"]=1 asorti(var, result) for (i in result) print "index:" i " <---> " "value:" result[i] }' index:4 <---> value:d index:1 <---> value:a index:2 <---> value:b index:3 <---> value:c [root@benxintuzi shell]# cat data7 This,is,the,line,1 This,is,the,line,2 This,is,the,line,3 This,is,the,line,4 [root@benxintuzi shell]# gawk ' BEGIN{FS=","} { number=split($0, var) print($number, var[1], var[2], var[3], var[4], var[5]) }' data7 1 This is the line 1 2 This is the line 2 3 This is the line 3 4 This is the line 4 |
|
时间函数 |
说明 |
mktime(datespec) |
将一个YYYY MM DD HH MM SS [DST]格式的日期转换成时间戳 |
strftime(format [, timestamp]) |
将当前时间的时间戳转换为shell函数date()的格式 |
systime() |
返回当前时间的时间戳 |
[root@benxintuzi shell]# gawk ' > BEGIN{ > date=systime() > day=strftime("%A %B %d %Y", date) > print day > }' Sunday August 16 2015 |
6 自定义函数
格式:
function name ([variables])
{
statements
}
函数的定义必须出现在所有代码块之前,包括BEGIN块。如果将函数放在库文件中定义,则在gawk中使用时,利用-f选项指定库文件。但是如果需要同时通过-f指定脚本文件名,则利用多次-f即可:
# 在命令行中定义函数 [root@benxintuzi shell]# gawk ' function myfunc() { print($1, $2, $5) } BEGIN{FS=","} { myfunc() }' data7 This is 1 This is 2 This is 3 This is 4 # 在库文件中定义函数 [root@benxintuzi shell]# cat funclib function myfunc() { print($1, $2, $5) } [root@benxintuzi shell]# cat script2 BEGIN{FS=","; RS="\n"} { myfunc() } [root@benxintuzi shell]# gawk -f funclib -f script2 data7 This is 1 This is 2 This is 3 This is 4
来源:https://www.cnblogs.com/benxintuzi/p/4734867.html