学会正则表达式,玩弄文本于股掌之中
1950 年, 一位叫 斯蒂芬·科尔·克莱尼的数学家发表了一篇标题为《神经网事件的表示法》的论文,引入了正则表达式的概念。正则表达式就是用来描述他称为"正则集的代数"的表达式,因此采用"正则表达式"这个术语。
随后,肯·汤普逊将这一符号系统引入 Unix 中的 qed 编辑器 ,肯·汤普逊也是 Unix 的主要发明人。正则表达式的第一个实用应用程序诞生。
目前,正则表达式已经在很多软件中得到广泛的应用,包括 *nix(Linux, Unix等)、HP 等操作系统,PHP、C#、Java、 Python、javascript 等编程语言,以及很多的文本处理软件中,都可以看到正则表达式的影子。
今天,无论你是否从事 IT 工作,你都应该学习正则表达式,因为它不仅能让你处理文本信息时事半功倍,更能为你提供一种思维方式,更重要的是,它是通用的知识,不因具体的文本编辑软件而不同,也不因具体的编程语言而不同。
大多数的 IT 青年都知道正则表达式,也能通过 grep 来查找含有相应字符串的文本信息,但是能使用正则表达式的高级功能的,却是少数,一个重要的原因就是正则表达式的符号有点难以记忆,也很不直观。看到别人写的正则表达式,就像看天书一般。虽然正则表达式是有点丑陋,但却是最优秀的文本处理工具。学会使用正则表达式,就算你不会编程,你也轻松高效地处理文本。
假如这样的需求:有一个近上万行内容的文本文件,内容是中英文混合,毫无规律,现在要求把所有的中文全部删除,你会怎么做呢?
如果不会正则表达式,你只能一行一行地删除,会不会觉得很累?但是如果会用正则表达式,只要几秒的时间的时间即可完成。下次如果有人有类似这样的问题请你帮忙,你可以使用正则表达式,弹指间,不需要的字符串已灰飞烟灭,从此,你在别人眼里深藏功与名。(正则表达式是装逼利器 _)。
下面我尝试让你入门正则表达式,如有疑问可加微信 somenzz 交流。
1、要匹配什么
相信你肯定用过 windows 里的文件搜索功能吧,在搜索栏输入"*.doc",然后所有后缀为 doc 的文件都查找了出来,这里的 * 就是通配符。在正则表达式也是一样,* 表示通配符,表示任意数目的字符,这点相信大家都不陌生。正则表达式也有一些通配符,我们叫它元字符,这是需要记忆的,不过很容易记忆 ,如下所示:
常用的元字符
代码 | 说明 |
---|---|
. | 匹配除换行符以外的任意字符 |
\w | 匹配字母或数字或下划线或汉字 |
\s | 匹配任意的空格 |
\d 或 [0-9] | 匹配一个数字 |
^ | 匹配字符串的开始位置 |
$ | 匹配字符串的结束位置 |
比如
- .* 代表匹配任意一行
- \d\d 匹配连续的两个数字
- ^[0-9] 匹配字符串开始位置是数字的字符串
- \s$ 匹配字符串结尾是空格的字符串
- ^$ 匹配不含空格的空行
- ^\s*$ 匹配含空格空行
2、要匹配多少次
有时要匹配很多次数,比如11位的手机号码,可以简单地这样写
\d\d\d\d\d\d\d\d\d\d\d
或
[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]
这样写显然是非常麻烦的,正则表达式提供了匹配次数简洁语法,很容易记忆,如下所示:
重复
代码/语法 | 说明 |
---|---|
* | 重复零次或更多次 |
+ | 重复一次或更多次 |
? | 重复零次或一次 |
{n} | 重复n次 |
{n,} | 重复n次或更多次 |
{n,m} | 重复n到m次 |
11位的手机号码的正则表达式可以简单地这样写
\d{11}
或
[0-9]{11}
假如你要匹配 5位 至 8 位的 QQ 号则可以使用:
\d{5,12}
或
[0-9]{5,12}
可能你会问了,如果要匹配*,?,+ 这些本身属于正则表达式里的字符呢? 也很简单,使用\来转义即可。要查找 * 就使用 * ,要查找 \ 就使用 \。例如:github.com 匹配 github.com,C:\Windows 匹配 C:\Windows。
3、反义
有时需要匹配不是某些字符的字符,如匹配非数字字符串,查找不含 aeiou 这 5 个字符的字符串,这时需要用到反义。
常用的反义代码
代码/语法 | 说明 |
---|---|
\W | 匹配任意不是字母,数字,下划线,汉字的字符 |
\S | 匹配任意不是空白符的字符 |
\D | 匹配任意非数字的字符 |
\B | 匹配不是单词开头或结束的位置 |
[^x] | 匹配除了x以外的任意字符 |
[^aeiou] | 匹配除了aeiou这几个字母以外的任意字符 |
例子:\S+ 匹配不包含空白符的字符串,[^aeiou] 匹配不包含a,e,i,o,u 这五个字符的字符串
4 、括号表达式,或
(TEMP|TMP|TEST)+.*\d$ 表示匹配含有 TEMP 或 TMP 或 TEST ,并且以数字结尾的字符串,可用于运维中查询一些命名不规范的表或一些垃圾表,从而进行处理。如下所示:
SELECT TABNAME
FROM syscat.tables
where REGEXP_LIKE(tabname,'(TEMP|TMP|TEST)+.*\d$','i')>0
查询结果如下:
F_DEP_DGLS_TMP_2
F_NIN_TB_TAX_BANK_KIND_TEMP2
TEST20180828
TMP_CLTNBR_18
TMP_CZKH1
TMP_RPT_RMBGRDKLLSPB_01
TMP_RPT_RMBTXLLSPB_01
TMP_ZH1
TMP_ZH2
VT_TMP_JJK_ZJHM_15
TMP_1
TMP_SX500
TMP_ZFMMKXH2
这里我们用到了小括号(),小括号可以指定子表达式,本例中 (TEMP|TMP|TEST) 就是一个表达式,里面的 | 连接多个选项,是或的关系。后面跟 + 表示这个子表达式代表的字符至少出现 1 次。后续会详细介绍如何在 db2 中添加自定义的正则表达式函数 REGEXP_LIKE,请关注。
5、使用零宽断言
零宽断言有点不太好理解,我以一个实用的例子来说明。
实例获取本机 IP 地址
通过一个获取本机 IP 地址例子,对正则表达式有个更深入的认识,不需记忆,理解即可。
IP 地址是这样的一种格式,xxx.xxx.xxx.xxx
我们分成两部分
第一部分 xxx.xxx.xxx. 相当于是 3 个 xxx. 应该写成 (xxx.){3} , xxx. 代表一至三位的数字,可以是一位、两位或三位。因此 xxx. 的正则表达式为 [0-9]{1-3}. ,因此合起来就是 ([0-9]{1,3}.){3}
第二部分 xxx,非常简单,就是 [0-9]{1,3}
这两部分加起来,完整的正则表达式就是:
([0-9]{1,3}\.){3}[0-9]{1,3}
下面在一台 linux 机器上验证下:
[aaron@ubuntu]$ ifconfig -a | grep -E -o "([0-9]{1,3}\.){3}[0-9]{1,3}"
192.168.167.40
255.255.255.0
192.168.167.255
127.0.0.1
255.0.0.0
192.168.122.1
255.255.255.0
192.168.122.255
可以看出所有的 IP 地址都打印了出来。假如果要获取某一块网卡的 IP 地址,可以这样
[aaron@ubuntu]$ ifconfig eth0 | grep -oP "([0-9]{1,3}\.){3}.*(?= netmask)"
192.168.167.40
[aaron@ubuntu]$ ifconfig eth0 | grep -oP "([0-9]{1,3}\.){3}.*(?= broadcast)"
192.168.167.40 netmask 255.255.255.0
这里使用先行断言 (?=exp) 来匹配表达式前面的位置 ,即“ netmask”,前的位置,这样就打印出了 eth0 真正的 IP 地址,可以做为参数传递给程序使用。
零宽断言用于查找在某些内容(但并不包括这些内容)之前或之后的东西,也就是说它们像 \b ^ $ < > 这样的定位作用,用于指定一个位置,这个位置应该满足一定的条件(即断言),因此它们也被称为零宽断言。有以下 4 种断言方式:
-
先行断言 (?=exp)//表示匹配表达式 exp 前面的位置
-
后发断言 (?<=exp) //表示匹配表达式 exp 后面的位置
-
负向零宽断言 (?!exp) // 匹配一个不含 exp 前面的位置,这个有点不太好理解,举个例子吧:有以下字符串:
baidu.com
sina.com.cn
那么正则:^(?!baidu).*$ 匹配结果就是第 2 行,也就是第 1 行被排除了,意思就是查找不以 baidu 开头的字符串。 -
负向零宽后发断言为 (?<!exp) // 匹配一个不含 exp 后面的位置,举个例子,有以下字符串
www.sina.com.cn
www.educ.org
www.hao.cc
www.baidu.com
www.123.com
那么正则 ^.*(?<!(com))$ 表示匹配不以 com 结尾的字符串,也就是前三个,使用 grep 时注意 表达式要加小括号,执行结果如下所示:
[aaron@ubuntu]$ cat t.txt
ww.sina.com.cn
www.educ.org
www.hao.cc
www.baidu.com
www.123.com
[aaron@ubuntu]$ grep -oP "^.*(?<!(com))$" t.txt
ww.sina.com.cn
www.educ.org
www.hao.cc
[aaron@ubuntu]$
匹配不含某个字符串的文本
比如,匹配不含 baidu 的字符串
[aaron@ubuntu]$ grep -oP "^(?!(.*baidu)).*$" t.txt
ww.sina.com.cn
www.educ.org
www.hao.cc
www.123.com
[aaron@ubuntu]$
比如,匹配不含 baidu 或 hao 的字符串
[aaron@ubuntu]$ grep -oP "^(?!(.*(baidu|hao))).*$" t.txt
ww.sina.com.cn
www.educ.org
www.123.com
[aaron@ubuntu]$
这些正则表达式的知识是通用的,无论你用 grep 或是 ue,还是 vim,他们都天然支持正则表达式,很多编程语言也支持,因此正则表达式的知识是通用的,作为程序员一定要知道。
现在回答本文开头提到的问题,如何在文本中删除中文字符。这里我使用的是文本编辑工具是 vim,你可以使用其他文本编辑工具,只要它支持正则表达式即可。
假如文本内容如下:
1 数字:^[0-9]*$
2 n位的数字:^\d{n}$
3 至少n位的数字:^\d{n,}$
4 m-n位的数字:^\d{m,n}$
5 零和非零开头的数字:^(0|[1-9][0-9]*)$
6 非零开头的最多带两位小数的数字:^([1-9][0-9]*)+(.[0-9]{1,2})?$
7 带1-2位小数的正数或负数:^(\-)?\d+(\.\d{1,2})?$
8 正数、负数、和小数:^(\-|\+)?\d+(\.\d+)?$
9 有两位小数的正实数:^[0-9]+(.[0-9]{2})?$
10 有1~3位小数的正实数:^[0-9]+(.[0-9]{1,3})?$
11 非零的正整数:^[1-9]\d*$ 或 ^([1-9][0-9]*){1,3}$ 或 ^\+?[1-9][0-9]*$
12 非零的负整数:^\-[1-9][]0-9"*$ 或 ^-[1-9]\d*$
13 非负整数:^\d+$ 或 ^[1-9]\d*|0$
14 非正整数:^-[1-9]\d*|0$ 或 ^((-\d+)|(0+))$
15 非负浮点数:^\d+(\.\d+)?$ 或 ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$
16 非正浮点数:^((-\d+(\.\d+)?)|(0+(\.0+)?))$ 或 ^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$
17 正浮点数:^[1-9]\d*\.\d*|0\.\d*[1-9]\d*$ 或 ^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$
18 负浮点数:^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$ 或 ^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$
19 浮点数:^(-?\d+)(\.\d+)?$ 或 ^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$
若要去除中文信息,首先我在网上查到匹配中文的正则表达式为 [\u4e00-\u9fa5],于是在 vim 中执行命令
:%s/[\u4e00-\u9fa5]//g
其实就是查找字符串 [\u4e00-\u9fa5]
将其替换为 空即可。执行前后对比如下所示:
这里 [\u4e00-\u9fa5] 不需要记忆,一些常用的复杂的正则表达式,网上都是可以搜索到的,在做稍复杂的文本处理时,首先要想到通过正则表达式怎么解决,如果写不出相应的正则表达式,可以查询 google 或 bing 寻求帮助。
资源分享:
(完)