一个同学问我一个问题,说有以下文件内容,要求输出为特定的格式。这里就献丑给出一个处理的方法吧,由于时间关系可能我的答案并不是最好的,但是我尽量将我的答案讲解明白,让你理解处理的方法。如果您有简单明了的处理方法请不啬赐教!
题目
文件内容如下:
2016-12-08 00:09 血战钢锯岭
2016-12-08 03:01 你的名字
2016-12-08 04:00 长城
2016-12-08 04:01 萨利机长
2016-12-09 07:35 神奇动物在
2016-12-09 09:24 湄公河行动
2016-12-09 10:59 我不是潘金莲
2016-12-09 12:43 海洋奇缘
2016-12-09 14:29 神奇四侠2015
2016-12-10 16:30 死侍
2016-12-10 16:31 加勒比海盗5:死
2016-12-10 16:36 三体
2016-12-10 18:04 阿凡达2
2016-12-10 19:40 日落七次
要求输出结果为:
2016-12-08
00:09 血战钢锯岭
03:01 你的名字
04:00 长城
04:01 萨利机长
2016-12-09
07:35 神奇动物在
09:24 湄公河行动
10:59 我不是潘金莲
12:43 海洋奇缘
14:29 神奇四侠2015
2016-12-10
16:30 死侍
16:31 加勒比海盗5:死
16:36 三体
18:04 阿凡达2
19:40 日落七次
参考答案
看到题目后,发现文件内容中的规律,源文件主要由年月日、时分和电影名称组成三列,而目标文件年月日主要是去重,每个日期只出现了一次后换行,将当日要上映电影的时分和电影名称按行显示出来。
因为是文件处理,首先我想到了sed来处理,使用sed加正则表达式将文件内容进行分组处理,然后去除重复的“年月日”,实现代码如下:
# sed -r "s/([0-9]{4}-[0-9]{2}-[0-9]{2})(\t)(.*)(\t)(.*)/\1\n\3\t\5/" file |sed -r ':1;N;s/^(\S+)((\n.*)*)\n\1$/\1\2/M;$!b1'
2016-12-08
00:09 血战钢锯岭
03:01 你的名字
04:00 长城
04:01 萨利机长
2016-12-09
07:35 神奇动物在
09:24 湄公河行动
10:59 我不是潘金莲
12:43 海洋奇缘
14:29 神奇四侠2015
2016-12-10
16:30 死侍
16:31 加勒比海盗5:死
16:36 三体
18:04 阿凡达2
19:40 日落七次
#
答案剖析
首先,我们先看管道前的代码,主要是将“年月日”和“时分,电影名称”分成了两部分。我们看到sed的-r选项的意思是支持扩展的这规则表达式,类似grep的-P。其中([0-9]{4}-[0-9]{2}-[0-9]{2})(\t)(.*)(\t)(.*)是正则表达式和分组。
正则表达式是指用来描述或者匹配一系列符合某个句法规则的字符串的单个字符串。就是用某种模式去匹配一类字符串的一个公式。Sed中的正则如下表所示:
^
行的开始
$
行的结尾
.
一个字符
*
匹配0个或多个*前面的字符
[]
方括号中的所有字符
\
转义
{}
重复次数
()
分组,将匹配这个表达式的字符保存到一个临时区域(最多保存9个),它们可以用\1到\9来引用。
/./
匹配至少有一个字符
/../
匹配至少有两个字符的行
/^#/
匹配用#开头的行
/^$/
匹配空行
/}$/
匹配用}结尾的行(没有空格在后面)
/} *$/
匹配用}结尾的行(可以有空格在后面)
/[abc]/
匹配小写的a或b或c
/^[^abc]/
匹配开头不是小写的a或b或c
匹配该题目的正则表达式的含义如下:
·([0-9]{4}-[0-9]{2}-[0-9]{2}):匹配数字0-9重复4次,匹配年;匹配数字0-9重复2次匹配月和日,并使用()分组
·(\t):匹配制表符,如果你使用的空格分隔,直接匹配空格,可以使用(\ |\t)匹配空格或者制表符
·(.*):匹配任意字符
接下来解释演示/\1\n\3\t\5/的含义,为了减少篇幅,我复制一份文件名为file1,内容如下:
# cat file1
2016-12-08 00:09 血战钢锯岭
2016-12-08 03:01 你的名字
2016-12-09 10:59 我不是潘金莲
#
执行如下命令,结果表明\1代表分组1,自然\3和\5代表分组3和5
# sed -r "s/([0-9]{4}-[0-9]{2}-[0-9]{2})(\ |\t)(.*)(\ |\t)(.*)/\1/" file1
2016-12-08
2016-12-08
2016-12-09
#
那么\n代表什么呢?我们在现有命令上加上/n,结果表明\n代表换行,如下所示:
# sed -r "s/([0-9]{4}-[0-9]{2}-[0-9]{2})(\ |\t)(.*)(\ |\t)(.*)/\1\n/" file1
2016-12-08
2016-12-08
2016-12-09
#
\t不用解释了吧,完整的执行以下如下所示:
# sed -r "s/([0-9]{4}-[0-9]{2}-[0-9]{2})(\ |\t)(.*)(\ |\t)(.*)/\1\n\3\t\5/" file1
2016-12-08
00:09 血战钢锯岭
2016-12-08
03:01 你的名字
2016-12-09
10:59 我不是潘金莲
# sed -r "s/([0-9]{4}-[0-9]{2}-[0-9]{2})(\ |\t)(.*)(\ |\t)(.*)/\1\n\3\t\5/" file1 >>file2
继续看管道后的命令的含义,之后的命令信息量有点稍微大。
# sed -r ':1;N;s/^(\S+)((\n.*)*)\n\1$/\1\2/M;$!b1' file2
2016-12-08
00:09 血战钢锯岭
03:01 你的名字
2016-12-09
10:59 我不是潘金莲
#
主要有多行模式空间的操作命令N、D、P和sed脚本流程控制命令b、t。
多行模式空间(MultilinePattern Space):就是在模式空间中放置输入文件的多个行内容,操作多行模式空间的有N、D、P含义如下:
·N命令:是将下一行也输入到模式空间中,当前行与下一行之间插入一个’\n’,以下为示意图
·D命令:仅删除Multiline Space中第一个’\n’之前的内容,如上图,即删除“The UnixOperating System”,而“Is A interestingSystem”仍然存在。同时,它使得脚本的控制流转到脚本文件的第一行,跳过该命令的后续命令。
·P命令:仅打印Multiline Space中第一个’\n’之前的内容,如上图,即仅打印“The UnixOperating System”。
我们看下N的范例,还是使用file1文件中的内容,命令执行结果如下:
# sed ‘N’ file1
2016-12-08 00:09 血战钢锯岭
2016-12-08 03:01 你的名字
2016-12-09 10:59 我不是潘金莲
#
看到结果后,有同学会说,这和Sed不加任何的选项和命令,执行的结果相同。如下所示:
# sed '' file1
2016-12-08 00:09 血战钢锯岭
2016-12-08 03:01 你的名字
2016-12-09 10:59 我不是潘金莲
#
使用肉眼咋一看真发现不了区别,首先我们回顾下sed的工作过程:
sed会先读取文本中的第一行,到模式空间,然后执行sed命令,处理完成后,将结果发送到屏幕上。sed每处理完一行就将其从模式空间中删除,接着会读取文本中的第二行,到模式空间,然后执行sed命令,处理完成后,将结果发送到屏幕上。重复此过程,直到文本中的最后一行,sed便结束运行。
了解sed的工作原理,我们发现没有使用N命令时候,sed依次将文本中的行读取到模式空间中,sed没有做任何的命令操作,他就直接显示到屏幕上了。
当使用N命令后,sed执行过程是sed会先读取文本中的第一行 “2016-12-08 00:09 血战钢锯岭$”到模式空间,然后执行sed命令N,模式空间中的第一行内容后追加第二行内容生成多行模式空间的第一行内容,多行模式空间变为“2016-12-08 00:09 血战钢锯岭\n2016-12-08 03:01 你的名字$”,处理完成后,将结果发送到屏幕上。sed继续向模式空间读取下一行内容,本例中就是第三行,然后再次追加下一行内容,生成多行模式空间中的第二行内容,以此类推。由于本例中第三行下没有内容,这时候执行N命令后就不会生成多行模式空间的第二行内容,所以模式空间中有“2016-12-09 10:59 我不是潘金莲”。处理完成后,将结果发送到屏幕上。
结果表明,我们使用N命令后,前两行输出的是多行模式空间的内容,最后一行是模式空间的内容。由于多行模式空间合并的第一行和第二行之间有\n,所以看到输出的格式没有变化,为了证明这个说法,我们将\n 替换成空格,如下所示:
# sed 'N;s/\n/ /' file1
2016-12-08 00:09 血战钢锯岭 2016-12-08 03:01 你的名字
2016-12-09 10:59 我不是潘金莲
#
通常,sed是将编辑命令从上到下依次应用到读入的行上,N命令能够在一定程序上改变默认的执行流程,甚至利用N命令可以形成一个强大的循环处理流程。除此之外,其实sed还提供了分支命令(b)和测试(test)两个命令来控制流程,这两个命令可以跳转到指定的标签(label)位置继续执行命令。标签是以冒号开头的标记,标签名称可以自定义。例如:定义一个名称为:label标签,如下所示:
:label
command1
/pattern/b label
command2
当执行到/pattern/b top时,如果匹配pattern,则跳转到:label标签所在的位置,继续执行下一个命令command1。
上面的例子用到了分支命令,分支命令的跳转是无条件的。而与之相对的是测试命令,测试命令的跳转是有条件的,当且仅当当前行发生成功的替换时才跳转。
为了明白测试命令的用法,我们用它来实现file1中的内容:
# sed -e ':1;s/2016/2017/;t1;' file1
2017-12-08 00:09 血战钢锯岭
2017-12-08 03:01 你的名字
2017-12-09 10:59 我不是潘金莲
#
我们定义了一个标签为:1,然后在最后利用测试命令跳转到该标签。可能,你会觉得这里也可以使用分支命令,但是事实上分支命令会导致死循环,因为在它里他没有结束的条件。
但是测试命令就不同了,这一点直到最后才体现出来。当最后一行被s/2016/2017/命令读入之后,2016替换成2017,此时ta继续跳转到最开头,因为模式空间中的2016已经全部被替换成2017,所以替换也不会发生。之前我们说过,当且仅当当前行发生成功的替换时测试命令才跳转。所以此时跳转不会发生,退出sed命令。
到此,你能看明白后半句的意思吗?欢迎留言!
欢迎使用微信关注“云运维联盟”公众号,第一时间了解本博客动态!