正则表达式学习笔记
1. 引言
以前我们用grep
在一个文件中找出包含某些字符串的行,比如在头文件中找出一个宏定义。其实grep
还可以找出符合某个模式(Pattern)的一类字符串。例如找出所有符合xxxxx@xxxx.xxx
模式的字符串(也就是email地址),要求x字符可以是字母、数字、下划线、小数点或减号,email地址的每一部分可以有一个或多个x字符,例如abc.d@ef.com
、1_2@987-6.54
,当然符合这个模式的不全是合法的email地址,但至少可以做一次初步筛选,筛掉a.b
、c@d
等肯定不是email地址的字符串。再比如,找出所有符合yyy.yyy.yyy.yyy
模式的字符串(也就是IP地址),要求y是0-9的数字,IP地址的每一部分可以有1-3个y字符。
如果要用grep
查找一个模式,如何表示这个模式,这一类字符串,而不是一个特定的字符串呢?从这两个简单的例子可以看出,要表示一个模式至少应该包含以下信息:
规定一些特殊语法表示字符类、数量限定符和位置关系,然后用这些特殊语法和普通字符一起表示一个模式,这就是正则表达式(Regular Expression)。例如email地址的正则表达式可以写成[a-zA-Z0-9_.-]+@[a-zA-Z0-9_.-]+\.[a-zA-Z0-9_.-]+
,IP地址的正则表达式可以写成[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}
。下一节介绍正则表达式的语法,我们先看看正则表达式在grep
中怎么用。例如有这样一个文本文件testfile
:
192.168.1.1 1234.234.04.5678 123.4234.045.678 abcde
查找其中包含IP地址的行:
$ egrep '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' testfile 192.168.1.1 1234.234.04.5678
egrep
相当于grep -E
,表示采用Extended正则表达式语法。grep
的正则表达式有Basic和Extended两种规范,它们之间的区别下一节再解释。另外还有fgrep
命令,相当于grep -F
,表示只搜索固定字符串而不搜索正则表达式模式,不会按正则表达式的语法解释后面的参数。
注意正则表达式参数用单引号括起来了,因为正则表达式中用到的很多特殊字符在Shell中也有特殊含义(例如\),只有用单引号括起来才能保证这些字符原封不动地传给grep
命令,而不会被Shell解释掉。
192.168.1.1
符合上述模式,由三个.
隔开的四段组成,每段都是1到3个数字,所以这一行被找出来了,可为什么1234.234.04.5678
也被找出来了呢?因为grep
找的是包含某一模式的行,这一行包含一个符合模式的字符串234.234.04.567
。相反,123.4234.045.678
这一行不包含符合模式的字符串,所以不会被找出来。
grep
是一种查找过滤工具,正则表达式在grep
中用来查找符合模式的字符串。其实正则表达式还有一个重要的应用是验证用户输入是否合法,例如用户通过网页表单提交自己的email地址,就需要用程序验证一下是不是合法的email地址,这个工作可以在网页的Javascript中做,也可以在网站后台的程序中做,例如PHP、Perl、Python、Ruby、Java或C,所有这些语言都支持正则表达式,可以说,目前不支持正则表达式的编程语言实在很少见。除了编程语言之外,很多UNIX命令和工具也都支持正则表达式,例如grep、vi、sed、awk、emacs等等。“正则表达式”就像“变量”一样,它是一个广泛的概念,而不是某一种工具或编程语言的特性。
2. 基本语法
我们知道C的变量和Shell脚本变量的定义和使用方法很不相同,表达能力也不相同,C的变量有各种类型,而Shell脚本变量都是字符串。同样道理,各种工具和编程语言所使用的正则表达式规范的语法并不相同,表达能力也各不相同,有的正则表达式规范引入很多扩展,能表达更复杂的模式,但各种正则表达式规范的基本概念都是相通的。本节介绍egrep(1)
所使用的正则表达式,它大致上符合POSIX正则表达式规范,详见regex(7)
(看这个man page对你的英文绝对是很好的锻炼)。希望读者仿照上一节的例子,一边学习语法,一边用egrep
命令做实验。
表 32.1. 字符类
字符 | 含义 | 举例 |
---|---|---|
. |
匹配任意一个字符 | abc. 可以匹配abcd 、abc9 等 |
[] |
匹配括号中的任意一个字符 | [abc]d 可以匹配ad 、bd 或cd |
- |
在[] 括号内表示字符范围 |
[0-9a-fA-F] 可以匹配一位十六进制数字 |
^ |
位于[] 括号内的开头,匹配除括号中的字符之外的任意一个字符 |
[^xy] 匹配除xy 之外的任一字符,因此[^xy]1 可以匹配a1 、b1 但不匹配x1 、y1 |
[[:xxx:]] |
grep 工具预定义的一些命名字符类 |
[[:alpha:]] 匹配一个字母,[[:digit:]] 匹配一个数字 |
表 32.2. 数量限定符
字符 | 含义 | 举例 |
---|---|---|
? |
紧跟在它前面的单元应匹配零次或一次 | [0-9]?\.[0-9] 匹配0.0 、2.3 、.5 等,由于. 在正则表达式中是一个特殊字符,所以需要用\ 转义一下,取字面值 |
+ |
紧跟在它前面的单元应匹配一次或多次 | [a-zA-Z0-9_.-]+@[a-zA-Z0-9_.-]+\.[a-zA-Z0-9_.-]+ 匹配email地址 |
* |
紧跟在它前面的单元应匹配零次或多次 | [0-9][0-9]* 匹配至少一位数字,等价于[0-9]+ ,[a-zA-Z_]+[a-zA-Z_0-9]* 匹配C语言的标识符 |
{N} |
紧跟在它前面的单元应精确匹配N次 |
[1-9][0-9]{2} 匹配从100 到999 的整数 |
{N,} |
紧跟在它前面的单元应匹配至少N 次 |
[1-9][0-9]{2,} 匹配三位以上(含三位)的整数 |
{,M} |
紧跟在它前面的单元应匹配最多M 次 |
[0-9]{,1} 相当于[0-9]? |
{N,M} |
紧跟在它前面的单元应匹配至少N 次,最多M 次 |
[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3} 匹配IP地址 |
再次注意grep
找的是包含某一模式的行,而不是完全匹配某一模式的行。再举个例子,如果文本文件的内容是
aaabc aad efg
查找a*
这个模式的结果是三行都被找出来了
$ egrep 'a*' testfile aabc aad efg
a*
匹配0个或多个a
,而第三行包含0个a
,所以也包含了这一模式。单独用a*
这样的正则表达式做查找没什么意义,一般是把a*
作为正则表达式的一部分来用。
表 32.3. 位置限定符
字符 | 含义 | 举例 |
---|---|---|
^ |
匹配行首的位置 | ^Content 匹配位于一行开头的Content |
$ |
匹配行末的位置 | ;$ 匹配位于一行结尾的; 号,^$ 匹配空行 |
\< |
匹配单词开头的位置 | \<th 匹配... this ,但不匹配ethernet 、tenth |
\> |
匹配单词结尾的位置 | p\> 匹配leap ... ,但不匹配parent 、sleepy |
\b |
匹配单词开头或结尾的位置 | \bat\b 匹配... at ... ,但不匹配cat 、atexit 、batch |
\B |
匹配非单词开头和结尾的位置 | \Bat\B 匹配battery ,但不匹配... attend 、hat ... |
位置限定符可以帮助grep
更准确地查找,例如上一节我们用[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}
查找IP地址,找到这两行
192.168.1.1 1234.234.04.5678
如果用^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$
查找,就可以把1234.234.04.5678
这一行过滤掉了。
表 32.4. 其它特殊字符
字符 | 含义 | 举例 |
---|---|---|
\ |
转义字符,普通字符转义为特殊字符,特殊字符转义为普通字符 | 普通字符< 写成\< 表示单词开头的位置,特殊字符. 写成\. 以及\ 写成\\ 就当作普通字符来匹配 |
() |
将正则表达式的一部分括起来组成一个单元,可以对整个单元使用数量限定符 | ([0-9]{1,3}\.){3}[0-9]{1,3} 匹配IP地址 |
| |
连接两个子表达式,表示或的关系 | n(o|either) 匹配no 或neither |
以上介绍的是grep
正则表达式的Extended规范,Basic规范也有这些语法,只是字符?+{}|()
应解释为普通字符,要表示上述特殊含义则需要加\
转义。如果用grep
而不是egrep
,并且不加-E
参数,则应该遵照Basic规范来写正则表达式。
3. sed
sed
意为流编辑器(Stream Editor),在Shell脚本和Makefile中作为过滤器使用非常普遍,也就是把前一个程序的输出引入sed的输入,经过一系列编辑命令转换为另一种格式输出。sed
和vi
都源于早期UNIX的ed
工具,所以很多sed
命令和vi
的末行命令是相同的。
sed
命令行的基本格式为
sed option 'script' file1 file2 ... sed option -f scriptfile file1 file2 ...
sed
处理的文件既可以由标准输入重定向得到,也可以当命令行参数传入,命令行参数可以一次传入多个文件,sed
会依次处理。sed
的编辑命令可以直接当命令行参数传入,也可以写成一个脚本文件然后用-f
参数指定,编辑命令的格式为
/pattern/action
其中pattern
是正则表达式,action
是编辑操作。sed
程序一行一行读出待处理文件,如果某一行与pattern
匹配,则执行相应的action
,如果一条命令没有pattern
而只有action
,这个action
将作用于待处理文件的每一行。
表 32.5. 常用的sed命令
/pattern/p |
打印匹配pattern 的行 |
/pattern/d |
删除匹配pattern 的行 |
/pattern/s/pattern1/pattern2/ |
查找符合pattern 的行,将该行第一个匹配pattern1 的字符串替换为pattern2 |
/pattern/s/pattern1/pattern2/g |
查找符合pattern 的行,将该行所有匹配pattern1 的字符串替换为pattern2 |
使用p
命令需要注意,sed
是把待处理文件的内容连同处理结果一起输出到标准输出的,因此p
命令表示除了把文件内容打印出来之外还额外打印一遍匹配pattern
的行。比如一个文件testfile
的内容是
123 abc 456
打印其中包含abc
的行
$ sed '/abc/p' testfile 123 abc abc 456
要想只输出处理结果,应加上-n
选项,这种用法相当于grep
命令
$ sed -n '/abc/p' testfile abc
使用d
命令就不需要-n
参数了,比如删除含有abc
的行
$ sed '/abc/d' testfile 123 456
注意,sed
命令不会修改原文件,删除命令只表示某些行不打印输出,而不是从原文件中删去。
使用查找替换命令时,可以把匹配pattern1
的字符串复制到pattern2
中,比如:
$ sed 's/bc/-&-/' testfile 123 a-bc- 456
pattern2
中的&
表示原文件的当前行中与pattern1
相匹配的字符串,再比如:
$ sed 's/\([0-9]\)\([0-9]\)/-\1-~\2~/' testfile -1-~2~3 abc -4-~5~6
pattern2
中的\1
表示与pattern1
的第一个()
括号相匹配的内容,\2
表示与pattern1
的第二个()
括号相匹配的内容。sed
默认使用Basic正则表达式规范,如果指定了-r
选项则使用Extended规范,那么()
括号就不必转义了。
如果testfile
的内容是
<html><head><title>Hello World</title>
<body>Welcome to the world of regexp!</body></html>
现在要去掉所有的HTML标签,使输出结果为
Hello World
Welcome to the world of regexp!
怎么做呢?如果用下面的命令
$ sed 's/<.*>//g' testfile
结果是两个空行,把所有字符都过滤掉了。这是因为,正则表达式中的数量限定符会匹配尽可能长的字符串,这称为贪心的(Greedy)[39]。比如sed
在处理第一行时,<.*>
匹配的并不是<html>
或<head>
这样的标签,而是
<html><head><title>Hello World</title>
4. awk
sed
以行为单位处理文件,awk
比sed
强的地方在于不仅能以行为单位还能以列为单位处理文件。awk
缺省的行分隔符是换行,缺省的列分隔符是连续的空格和Tab,但是行分隔符和列分隔符都可以自定义,比如/etc/passwd
文件的每一行有若干个字段,字段之间以:
分隔,就可以重新定义awk
的列分隔符为:
并以列为单位处理这个文件。awk
实际上是一门很复杂的脚本语言,还有像C语言一样的分支和循环结构,但是基本用法和sed
类似,awk
命令行的基本形式为:
awk option 'script' file1 file2 ... awk option -f scriptfile file1 file2 ...
和sed
一样,awk
处理的文件既可以由标准输入重定向得到,也可以当命令行参数传入,编辑命令可以直接当命令行参数传入,也可以用-f
参数指定一个脚本文件,编辑命令的格式为:
/pattern/{actions}
condition{actions}
和sed
类似,pattern
是正则表达式,actions
是一系列操作。awk
程序一行一行读出待处理文件,如果某一行与pattern
匹配,或者满足condition
条件,则执行相应的actions
,如果一条awk
命令只有actions
部分,则actions
作用于待处理文件的每一行。比如文件testfile
的内容表示某商店的库存量:
ProductA 30 ProductB 76 ProductC 55
打印每一行的第二列:
$ awk '{print $2;}' testfile 30 76 55
自动变量$1
、$2
分别表示第一列、第二列等,类似于Shell脚本的位置参数,而$0
表示整个当前行。再比如,如果某种产品的库存量低于75则在行末标注需要订货:
$ awk '$2<75 {printf "%s\t%s\n", $0, "REORDER";} $2>=75 {print $0;}' testfile ProductA 30 REORDER ProductB 76 ProductC 55 REORDER
可见awk
也有和C语言非常相似的printf
函数。awk
命令的condition
部分还可以是两个特殊的condition
-BEGIN
和END
,对于每个待处理文件,BEGIN
后面的actions
在处理整个文件之前执行一次,END
后面的actions
在整个文件处理完之后执行一次。
awk
命令可以像C语言一样使用变量(但不需要定义变量),比如统计一个文件中的空行数
$ awk '/^ *$/ {x=x+1;} END {print x;}' testfile
就像Shell的环境变量一样,有些awk
变量是预定义的有特殊含义的:
表 32.6. awk常用的内建变量
FILENAME | 当前输入文件的文件名,该变量是只读的 |
NR | 当前行的行号,该变量是只读的,R 代表record |
NF | 当前行所拥有的列数,该变量是只读的,F 代表field |
OFS | 输出格式的列分隔符,缺省是空格 |
FS | 输入文件的列分融符,缺省是连续的空格和Tab |
ORS | 输出格式的行分隔符,缺省是换行符 |
RS | 输入文件的行分隔符,缺省是换行符 |
例如打印系统中的用户帐号列表
$ awk 'BEGIN {FS=":"} {print $1;}' /etc/passwd
awk
还可以像C语言一样使用if
/else
、while
、for
控制结构,此处从略。
5. awk用法补充
1. awk 中的 if 语句
逐行读取文件,每行以空格分开的字符串当做参数,从 $1 开始。
(1) 语法
awk '{if (condition) {statement} }' [input_file] awk '{ if (condition) {command1} else {command2} }' [input_file] awk '{ if(condition1){ command1 } else if(condition2) { command2 } else if(condition3) { command3 } . . . else{ commandN } }' [input_file]
(2) 例子
awk '{ if ($1=="100") { print "Name : " ,$2; print "Age : ",$3; print "Department : " ,$4; } }' linuxmi.txt ----------执行结果----------------- Name : Robert Age : 23 Department : Commerce
写成1行:
awk '{ if ($1=="100") { print "............... \n"; print "Name : " ,$2; print "Age : ",$3; print "Department : " ,$4; } }' linuxmi.txt
if..else的例子:
//脚本中只能用空格,不能用tab键 awk '{ if ($3<20) { print "Student "$2,"of department", $4, "is less than 20 years old" } else { print "Student "$2,"of department", $4, "is more than 20 years old" } }' linuxmi.txt ----------执行结果----------------- is more than 20 years old Subject is less than 20 years oldent DevOps is more than 20 years old SecOps is more than 20 years old IT is more than 20 years oldnt Commerce is more than 20 years oldment linuxmi is more than 20 years oldMaths is less than 20 years oldnt Arts
这个导致第一行"ID Name Age Subject" 也被显示出来了。下面使用正则表达式进行过滤。正则表达式 /[0-9]+$/ 表示数值。~表示参数有一个数字。在它们前面的反运算符意味着字段不应该有数字。
awk '{ if (! ($3 ~ /[0-9]+$/)) { print "Age is just a number but you do not have a number" } else if ($3<20) { print "Student "$2,"of department", $4, "is less than 20 years old" } else { print "Student "$2,"of department", $4, "is more than 20 years old" } }' linuxmi.txt ----------------------------执行结果----------------------------- Age is just a number but you do not have a number is less than 20 years oldent DevOps is more than 20 years old SecOps is more than 20 years old IT is more than 20 years oldnt Commerce is more than 20 years oldment linuxmi is more than 20 years oldMaths is less than 20 years oldnt Arts
2. 使用 awk 程序文件
# cat linuxmi.awk { if (! ($3 ~ /[0-9]+$/)) { print "Age is just a number but you do not have a number" } else if ($3<20) { print "Student "$2,"of department", $4, "is less than 20 years old" } else { print "Student "$2,"of department", $4, "is more than 20 years old" } } # awk -f linuxmi.awk linuxmi.txt Age is just a number but you do not have a number is less than 20 years oldent DevOps is more than 20 years old SecOps is more than 20 years old IT is more than 20 years oldnt Commerce is more than 20 years oldment linuxmi is more than 20 years oldMaths is less than 20 years oldnt Arts
3. 使用三元操作符代替
//语法: (condition) ? Command1 : Command2 $ awk '{print ($3 <=20)? "Age less than 20: " $2 : "Age over 20: " $2}' linuxmi.txt Age over 20: Name Age less than 20: Farhaan Age over 20: Alex Age over 20: Ronn Age over 20: Robert Age less than 20: Samantha Age over 20: Bob Age less than 20: Rihaan
4. 使用awk进行过滤操作
(1) 查看kworker线程的绑核情况
# for P in `ps -elf | grep -v grep | grep kworker | awk '{print $2}'`; do echo $P; cat /proc/$P/comm; cat /proc/$P/status | grep Cpus_allowed_list; done;
(2) 查看rcu相关线程的优先级
# for P in `ps -AT | grep rcu | awk '{print $2}'`; do echo $P; cat /proc/$P/comm; cat /proc/$P/sched | grep prio; done;
参考:
1. 正则表达式:https://akaedu.github.io/book/ch32.html
2. Linux 下强大的 awk 命令: https://mp.weixin.qq.com/s/WEE702IgzgUbRI7u3K39gg
posted on 2020-02-06 23:12 Hello-World3 阅读(246) 评论(0) 编辑 收藏 举报