正则表达式
用正确的语言,描述问题的模式
问题:
- 需求:列举全部JPEG图片 ls *.jpg
- 描述:小数点之前任意字符均可、一小数点接jpg结尾。
- 需求:列举测试输入和答案文件ll *_test_?.(inlans)
- 描述:文件名以任意数量、任意字符开始,后接_test_,后接1个任意字符,然后是小数点,以in或ans结尾。
- 需求:文件批量改名 rename ‘s/LC/lc/’ LC-*.cpp
- 描述:将LC-开头的cpp文件替换成小写lc并去掉连字符。
- 需求:标识符 _stdcall valueOf bindlst
- 描述:以字母或者下划线开头,后续字符还可以是数字。
- 地址:1号楼2单元301室
- 描述:数字、“号楼”、数字、“单元”、数字、“室”
- 需求:URL的http://localhost:9090/search?id=1
- 描述:单词开头、“:”、双斜线、、接字母或连字符、可选的冒号接数字、斜线之后有任意字符、如果有?则后面有些&分开的等式。
抽象建模:
- 匹配(match):验证文本是否完全符合模式。
- 案例:验证字符串是合理的IPv4地址
- 搜索(search):从文本中找目标模式。可以出现多次。
- 案例:Web爬虫需要从HTML中找出所有的URL递归爬取。
- 案例:从病毒DNA搜索基因片段。生物信息学、医疗、考古、刑侦…
- 替换(replace):将与模式匹配的部分替换成其他内容。文本编辑、文本处理…
- 案例:将“email, name”替换成“name<email>”制作通讯录。
用正确的语言,描述问题的模式
概念
字母表
- 问题:怎么确定那些符号可用?
- 例如二进制数字只能由0和1字符组成。
- 字母表(alphabet):有限的符号集合。
- 示例:
- 二进制字母表{0,1};
- 十六进制字母表{0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F};(字母还有小写)
- 因为字母(26)、ASCII(128)、Unicode(144697)
- IPv4字母表{0,1,2,3,4,5,6,7,8,9,.};
- C++标识符字母表{大写字母、小写字母、下划线、数字}
- DNA由四种碱基组成,可视为{A,T,G,C}
串
- 串(string):基于某个字母表中符号的有穷序列。
- 示例:
- 空串
- 十六进制串DEAD表示数值57005
- 英文字母串DEAD在英语中表示死亡。
- 标识符字母表上的串3D不是合理的标识符
- 基于IPv4字母表的串01.999…“不是合理的IPv4语言”
- 英语字母串You can you up不是合理的英语。
- 这个串不属于规范的英语。//属于 ~ 集合 ~ 语言
- 这个串不符合英语语法。//语法 ~ 模式 ~ 语言
语言
- 语言(language):给定字母表上一个任意的可数的串集合。
- 语言是个集合
- 可数:可能无穷(与自然数集等势,一一对应)。
- 示例:
- 空集∅是语言。
- 只有空串s的集合{ s }是语言。
- 字母表上的美中字母都是一种语言。
- 0和1是二进制字母表上的两种语言。
- A,T,G,C是DNA字母表上的四种语言。
应用:匹配字符
- 案例:某国想取缔字母Z,要从所有文件中搜索Z并删除。
- 方法:字符是基础的正则表达式,直接写就行。
- 示例:Z被某些国家认为是种邪恶的语言。
- 说明:两边斜线是分割符,用于区分,不属于正则表达式。
运算
连接 XY
- 串的连接(concatenation):串X与串Y的连接就是X后接Y。
- 示例:jpg是j、p、g的连接。
- 空串是连接运算的单位元。
- 示例:A连接空串等于A
- 示例:空串连接A等于A
- 连接看做乘法,多次连接看做指数运算。
- 示例:A0是空串
- 示例:A3是AAA
应用:匹配序列
- 问题:怎样找特定序列?例如jpg
- 语言的连接:语言L和语言M的连接是L中的任意串与M中的任意串连接的集合。
- 示例:语言j、语言p、语言g的连接是语言jpg
- 示例:语言p、语言n、语言g的连接是语言png
- 通过连接运算可以构造新语言。
- 示例:语言jpg正确描述了问题的模式。
并 |
- 问题:想找出扩展名是jpg或png的图片。
- 思路:既然语言是集合,那么就可以求并集。
- 方法:运算符 | 表示两种语言的并。
- 示例:语言 jpg|png正确描述了上述问题的模式。
闭包 *
- 问题:描述不含0的二进制串。
- Kleene闭包(closure):语言L连接0次或多次得到的串集合,记为L*
- 示例:1*描述了问题的模式。
- 示例:L0是只含空串的集合{ };
- 正闭包:语言L连接正整数次得到的串集合,记为L+
- 示例:除非L包含空串,否则L+不含空串;
正则表达式
- 正则表达式r代表一种语言L®
- 空串是正则表达式。
- 字母表中的每个符号都是正则表达式。
- 递归构造:
- ® 语言L®本身,括号不影响含义。
- ®* 闭包
- ®(s) 连接
- ®|(s) 并
结合性与优先级
- 闭包、连接、并运算都是左结合,优先级依次降低。
- 示例:
- jpg == (jp)g == j(pg)
- 10* == 1(0*) != (10)*
- jpg | png == (jpg) | (png)!= jp(g|p)ng
- 规定优先级后,可以省略部分括号:
- (a)|((b)*©)
- (a)|(b*c)
- a|b*c
扩展语法
-
字符
- 任意字符 .
- 列举字符 [aeiou]
- 限定范围 [0-9]
- 排除范围 [^0-9]
- 常用类别 数字\d 非数字\D 单词\w 非单词\W 空白\s 非空白\S
-
频度
- 零次或一次 ?
- 零次或多次 *
- 一次或多次 +
- 固定n次{n} m到n次{m,n} 至少m次{m,} 最多n次{,n};
-
定位:开头^ 结尾$ 边界\b
-
聚组:()任选模式(A|B) 替换\0 \1 \2 … 或 $0 $1 $2 …
匹配任意字符 .
-
需求:想搜索歼灭机型号,类似J-后接两个任意字符。
-
方法:元字符 . 匹配任意字符。
-
解释: . 是(a|b| … )列举字母表所有字符的简写。
-
示例: /J-…/ 匹配J-7A J-20 …
-
需求:怎样匹配数点本身?比如搜索带数点的条目。
-
方法 :反斜线转义 \.
-
示例:/.\./ 匹配 1. A. 等等
匹配部分字符 [ ]
-
问题:怎样限定字符?比如只允许元音字母?
-
方法:在方括号中列举可接受的字符。
-
解释:[abc]是(a|b|c)的简写。
-
示例:匹配元音 [aeiou]
-
示例:匹配数字 [0123456789]
-
提示:字符顺序无关 [ab]与[ba]等效
指定字符范围 [A-Z]
- 问题:匹配字母难道要列出52个字符?
- 方法:用起-止字符指定连续的字符范围。
- 解释:[a-z]是(a|b|…|z)的简写。
- 示例:[0-9]数字
- 示例:[-]连字符本身
- 示例:标识符由字母或下划线开头[A-Za-z_]
排除字符集 [^]
- 问题:匹配辅音[b-df-hj-np-tv-z]的写法太啰嗦啦!
- 方法:异或^开头的字符集只匹配范围以外的字符。
- 示例:匹配辅音 [^aeiou]
- 示例:匹配除空格以外的字符[^ ]
频度元字符 ? * +
- 零次或多次 * 可空也可重复
- 一次或多次 + 必要也可重复
- 零次或一次 ?可空但不重复
- 示例:图片文件名不能为空,扩展名为jpg或jpeg
- /.+.jpe?g/
- 示例:匹配C++标识符
- /[a-zA-Z_][a-zA-Z_0-9]*/
头文件是<regex>
除了性能差,别的啥都好。
例子:
#include<iostring>
#include<regex>
using namespace std;
int main(){
const regex ptn{"^[A-Za-z_]\\w*$"};
for(string s; getline(cin, s);){
cout << regex_match(s, ptn) << endl;
}
return 0;
}
//c++标识符的判断;
固定重复次数 { n }
- 问题:匹配手机号要写11遍吗?身份证号岂不要写18遍?
- 方法:大括号包围的数字表示之前的重复次数。
- 示例:手机号是1开头的11位数字 1[0-9]{10}
- 示例:身份证号的前17位必须是数字,最后1位是数字或X [0-9]{17}[0-9X]
可变重复次数 {m,n}
- 问题:重复次数不是定值而是范围,怎么办?
- 比如要求邮箱用户名最少2个,最多20个字符。
- 方法:{m,n}重复最少m次、最多n次。
- 方法:{m,}重复最少m次。
- 方法:{,n}重复最多n次。
- 示例:用户名字母开头,后面也可以是数字,总长2到20。
/[a-zA-Z][a-zA-Z0-9]{1,19}/
预设字符类
- 需求:ISBN书号要反复写同样字符串集很麻烦。能简化吗?
- 预设字符类:
- \d \D 数字/非数(digit) \d相当于[0-9]
- \w \W 构词/非词(word) \w成词意思是符合标识符的标准 \W是一些符号
- \s \S 空白/非空(space) \s空白不只是空白,Tab啥的可能也算,但是不同环境下的正则表达式的预设字符不同
- 上例:\d{3}-\d-\d{4}-\d{4}-\d
- 示例:标识符 [A-Za-z_]\w*
模式定位
- 需求:想要匹配完整的行或者单词
- 方法:使用定位元字符。
- 匹配开头 ^
- 匹配结尾 $
- 匹配单词边界 \b bound边界
- 示例:/\b([A-Za-z_][A-Za-z_0-9])\b/
子模式()
- 需求:怎样表达模式中重复的子模式?
- 比如域名 aaa.bbb.cc@sdu.edu.cn
- 比如地址 123.45.6.7
- 方法:子表达式就是子模式。子模式也可以指定频度。
- 示例:域名\w+(\.\w+)*@sdu\.edu\.cn
- 示例:地址\d{1,3}(\.\d{1,3}){3}
应用:IPv4的各节
- 需求:匹配0到255的整数但不能有前缀0.
- 策略:分而治之。
- 模式:/\d|[0-9]\d|1\d{2}|2[0-4]\d|25[0-5]/
- 分组:
- 一位数:接受
- 两位数:1-9开头的接受
- 三位数:1开头的任意、20x到24x、250到255。
- 拓展:能挣钱描述一节,描述IP地址也就不难了。
惰性匹配
- 问题:想分出龙新型号“3A3000”的主机号、数字。
- 试解:/^(\w*)(\d+)$/ ->3A3000
- 正解:/^(\w***?**)(\d+)$/ ->3A3000
- 贪婪匹配:尽量多向前匹配。试解
- 惰性匹配:尽量少向前匹配。正解
替换 $n \n
- 引用组号:
- $1 … $9
- \1 … \9
- 引用整体:$0或者 \0
- TODO
特殊字符
- 移植性 TODO
- \n 换行(newline)
- \r 回车(carriage return)
- \p
- \t 水平制表
- \f
- \v 垂直制表
- \b
看具体环境,有的支持有的不支持
匹配选项
- g 全局搜索(global search)
- i 忽略大小写(case insensitive)
- m 多行匹配(multiline)
局限性
- 问题:判断串是否是回文。例如:101
- 问题:判断身份证号是否有效。考虑校验位和日期。
- 问题:判断是否质数序列。例如:2_7_5
代价
- 可能包含递归、产生大量运行时开销,有安全风险(DDoS)
- 程序编译时间延长
- 运行时创建regex对象开销较大。
- 尽量事先创建,重复使用。
- 性能不如精心编写的专用函数。