README
# 正则表达式
# 克制
那到底该怎么“克制”呢?我的经验有以下三点。
**第⼀,能用普通字符串处理的,坚决⽤普通字符串处理。**字符串处理的速度不见得差,可读性却好上很多。如果要在大段文本中定位所有的 today 或者 tomorrow,用最简单的字符串查找,直接找两遍,明显比 to(day|morrow) 看起来更清楚。
**第⼆,能写注释的正则表达式,⼀定要写注释。**正则表达式的语法非常古老,不够直观,为了便于阅读和维护,如今大部分语言里都可以通过 x 打开注释模式。有了注释,复杂正则表达式的结构也能一目了然。
**第三,能用多个简单正则表达式解决的,⼀定不要苛求用一个复杂的正则表达式。**这里最明显的例子就是输入条件的验证。比如说,常见的密码要求“必须包含数字、小写字母、大写字母、特殊符号中的至少两种,且长度在 8 到 16 之间”。
你当然可以绞尽脑汁用一个正则表达式来验证,但如果放下执念,⽤多个正则表达式分别验证“包含数字”“包含小写字母”“包含大写字母”“包含特殊符号”这四个条件,要求验证成功结果数大于等于 2,再配合一个正则表达式验证长度,这样做也是可行的。虽然看起来繁琐,但可维护性绝对远远强于单个正则表达式。
# 元字符的分类
所谓元字符就是指 那些 在正则表达式中具有特殊意义的专用字符,正则就是由一系列的元字符组成
# 特殊单字符
.——任意字符(换行除外)\d——任意数字\D——任意非数字\w—— 任意字母,数字,下划线\W——任意非字母,数字,下划线\s——任意空白符\S——任意非空白字符

# 空白符
\n——换行符(常用)\——里面有一个空格,用来匹配空格\s——可以匹配各种空白字符——(常用)

# 量词
英文的星号(*)——0 到多次
加号(+)——1 到多次
问号(?)——0 到 1 次
{m,n}——m 到 n 次
{m}——出现m次
{m,}——至少出现m次(注意后面的逗号)
.*——会匹配出长度最长的结果——默认贪婪匹配.*?——会匹配出最短的结果——非贪婪匹配

# 范围
|——用来隔开多个正则,表示满足其中任意一个就行(类似于或)[]——代表多选一,可以表示里面的任意单个元素- [aeiou]——任意元音字符
- [a-z]——所有小写字符
- [^a-z]——
^表示非,表达的是不能是里面的任何单个字符

# 断言
正则中提供了一些结构,只用于匹配位置,而不是文本内容本身,这种结构就是断言

单词边界(Word Bounday)
\b——表示单词边界

\btom\b——保证只有一个tom
行的开始/结束(^,$)
回车(\r)和换行(\n)是两个概念
回车&换行

开始和结束
- 要求6位数字——
^\d{6}$
- 要求6位数字——

多行模式下,
^$可以匹配每一行的开头或者结尾- 所以对于输入数据的开头或者结尾使用
\A和\z(Python)
- 所以对于输入数据的开头或者结尾使用
解决这个问题还有一种做法,我们可以在使用正则校验前,先判断一下字符串的长度,如果不满足长度要求,那就不需要再用正则去判断了。
- 相当于你用正则解决主要的问题,而不是所有问题,这也是前面说的使用正则要克制.
环视(Look Around)
- 环视就是要求匹配部分的前面或后面要满足(或不满足)某种规则,有些地方也称环视为零宽断言
- 左尖括号代表看左边,没有尖括号是看右边,感叹号是非的意思。

- 环视与子组
- 环视中虽然也有括号,但不会保存成子组
- 保存成子组的一般是匹配到的文本内容,后续用于替换等操作,而环视是表示对文本左右环境的要求,即环视只匹配位置,不匹配文本内容
- 六位邮编
- 中间6位数字,左边不是数字,右边不是数字
(?<!\d)\d{6}(?!\d)
- 单词边界
\b\w+\b——一个带边界的单词(?<!w)\w+(?!\w)或者(?<W)\w+(?!\w)
# 示例
某个资源,https://,http://,ftp://
(https?|ftp):\/\/s?——表示有一个或者没有s——匹配 http和https\/——\表示转义字符,\/——表示正常的/
正则——手机号——第一版
- 第 1 位固定为数字 1;
- 第 2 位可能是 3,4,5,6,7,8,9;
- 第 3 位到第 11 位我们认为可能是 0-9 任意数字
1[3-9][0-9]{9}
替换重复出现的单词
以前的做法
the little cat cat is in the hat hat, we like it.- 查找——
(\w+)\s\1 - 替换——
\1 - 这种做法无法满足
现在的做法
the little cat cat2 is in the hat hat2, we like it.cat与cat2属于不同的单词,但是(\w+)\s\1达不到预计的效果
查找——
(\w+)( \1\b)+- 严谨点——
(\w+)(?:\s+\1\b)+ ?:——不保存子组\s+——可以匹配多个空白字符串
- 严谨点——
替换——
\1
# 正则三种模式
贪婪匹配,非贪婪匹配,独占模式
# 贪婪匹配
在正则中,表示次数的量词默认是贪婪的,在贪婪模式下,会尝试尽可能最大长度去匹配
- 在字符串 aaabb 中使用正则 a* 的匹配过程

- a* 在匹配开头的 a 时,会尝试尽量匹配更多的 a,直到第一个字母 b 不满足要求为止,匹配上三个 a,后面每次匹配时都得到了空字符串。
- 贪婪模式的特点就是尽可能进行最大长度匹配
# 非贪婪匹配
那么如何将贪婪模式变成非贪婪模式呢?我们可以在量词后面加上英文的问号 (?)——a?*

- 这次匹配到的结果都是单个的 a,就连每个 a 左边的空字符串也匹配上了。
- 非贪婪模式会尽可能短地去匹配
- 两种模式对比——查找引号字符

# 独占模式
不管是贪婪模式,还是非贪婪模式,都需要发生回溯才能完成相应的功能。
独占模式,它类似贪婪匹配,但匹配过程不会发生回溯。
- 贪婪模式(默认)

- 非贪婪模式(量词?)

- 独占模式(量词+)

对比

案例
we found “the little cat” is in the hat, we like “the little cat”- 要求,提取出所有单词,引号中的单词(the little cat)看做一个
\w+|“.+?”——双引号内部 非贪婪模式\w+|“[\w\s]+”——双引号内部 单词,空白符,一个或多个\w+|“[^”]+”——双引号内部,不包含引号的一个或多个字符
# 分组与引用
案例1
\d{18}|\d{15}- 能识别18位或者15位数字
- 但是当每行数字超过18位后,只能识别18位的
\d{15}|\d{18}- 只能识别15位
- 因为18位中包含15位,且在大多数正则实现中,多分支选择都是左边的优先
\d{15}(\d{3})?- 前面表示15位,后面括号作为一个整体,加问号表示0个或者1个
- 由多个元字符组成某个部分,应该被看成一个整体的时候,可以用括号括起来表示一个整体,这是括号的一个重要功能
- 其实用括号括起来还有另外一个作用,那就是“复用”
# 分组与编号
子组
保存子组(默认)
- 用括号括起来的部分 子表达式 会被保存成一个子组
- 默认情况下,子组会被保存,并分配编号

不保存子组
你可能只想用括号将某些部分看成一个整体,后续不用再用它。这时我们可以使用
(?:)不保存子组。不保存子组可以理解为,括号只用于归组,把某个部分当成“单个元素”,不分配编号,后面不会再进行这部分的引用。
不保存子组,性能会比较高。
区别

括号嵌套查找编号
- 对嵌套的括号查询-编号——数左边开括号的个数即可

- 对嵌套的括号查询-编号——数左边开括号的个数即可
命名分组
- 一些编程语言提供了命名分组(named grouping),这样和数字相比更容易辨识,不容易出错。命名分组的格式为
(?P<分组名>正则)。
- 一些编程语言提供了命名分组(named grouping),这样和数字相比更容易辨识,不容易出错。命名分组的格式为
# 分组引用
各编程语言
\num——\1(1为分组编号)——$1

分组引用在查找和替换中使用
(\w+) \1——(中间有空格)\w+表示一个单词,(\w+)\1表示引用分组的第一个(\w+)\s\1——空格尽量用\s
\1表示第一个分组,即((\d{4})-(\d{2})-(\d{2}))=2020-05-10\2表示第二个分组,即(\d{4})=2020在替换的时候,引用
\1,表示引用2020-05-10
# 子组引用(*重要*)
\d(\d){2}——\d(\d)+——120- 此时正则保存了一个子组,即
\1=0 - 这种
()量词的情况 只保留最后一个作为子组
- 此时正则保存了一个子组,即
\d(\d)(\d)——120- 此时正则保存了两个子组,即
\1=2&&\2=0
- 此时正则保存了两个子组,即
\d(\d{2})——\d(\d+)——120- 此时正则保存了一个子组,即
\1=20 - 这种
(量词),量词在括号里面的情况,整个括号表示一个整体,即一个子组
- 此时正则保存了一个子组,即
(\w+)\s\1——cat cat- 第一个cat为子组1,后面的
\1表示引用之前的子组cat - 这样即可表示 前后两个单词相同,即可表示 重复单词
- 第一个cat为子组1,后面的
((\w+)\s\2)——cat cat- 第一个子组为 cat cat ,第二个子组为第一个 cat
\2表示引用 第二个子组即第一个cat,这样也可以匹配上 重复单词
([a-z]+)(\d)\s\2——cat2 2- 第一个子组为 cat, 因为
+在括号里面 - 第二个子组是2,
\2表示引用第二个子组,即2,所有可以匹配上重复的字符2
- 第一个子组为 cat, 因为
(\w+)(\s\1)+——cat cat或者cat cat cat或者cat cat cat cat第一个子组为第一个 cat
\1表示引用第一个子组,即引用 cat+表示一个或多个,再次引用第一个子组 cat,这样就可以表示 匹配到多个重复的单词示例
查找
(\w+)(\s\1)+
替换
\1
# 匹配模式
模式--mode
模式修饰符——
(?模式标识)- 不区分大小写——
(?i)——Insensitive首字母的小写 - 点号通配模式——
(?s)——Single - 多行匹配模式——
(?m)——Multiline - 注释模式——
(?#)

- 不区分大小写——
# 不区分大小写模式(case-Insensitive)
找出所有cat,不区分大小写
- CAT=CAt=Cat=cat=cAt=cAT=cAt=caT
[Cc][Aa][Tt]——笨方法(?i)cat
匹配两个连续出现的cat,不区分大小写
(?i)(cat)\s\1

不区分大小写,匹配两个连续出现的cat,且第一次和第二次大小写一致
((?i)cat) \1- 用一个大括号包裹起来,表示不区分大小写只作用于这个括号里的内容

部分区分大小写——the cat 中 the不区分,cat区分
((?i)the) cat- 让模式修饰符
(?i)只作用于the,而不作用于cat - 如果不加括号,默认是两个都会被作用(即作用于整个正则),加了括号,相当于加了限定条件,只作用于
the
总结
- 不区分大小写模式的指定方式,使用模式修饰符 (?i);
- 修饰符如果在括号内,作用范围是这个括号内的正则,而不是整个正则;
- 使用编程语言时可以使用预定义好的常量来指定匹配模式。
# 点号通配模式(Single)
也叫单行匹配模式,但是和多行匹配模式没有关系
.——可以匹配除了换行之外的任何字符(?s).——可以匹配包括换行在内的任何字符
# 多行匹配模式(Multiline)
默认
^——匹配整个字符串的开头$——匹配整个字符串的结尾

(?m)——多行匹配模式^——匹配每行的开头$——匹配每行的结尾

# 注释模式(Comment)
- 正则表达式很复杂,可以在正则表达式内部加入注释
(?#)(\w+)(?#word)\s\1(?#word repeat again)
# 示例
HTML 标签是不区分大小写的,比如我们要提取网页中的 head 标签中的内容,用正则如何实现呢?
<meta charset="utf-8">(?si)<head.*?>.*<\/head>
# 转义

常用转义字符

# 字符串转义和正则转义
正则中:
\d——表示单个数字\\d——表示\d——\\表示真正的反斜杠\\|d——表示\或者d
程序中:
\\\\——\四个反斜杠表示一个反斜杠
因为先经过字符串转义
\\\\——\\,真正被读入正则的只有\\然后,正则转义
\\——\
python中,可以使用原生字符串的方式来避免出现上面的情况
>>> import re >>> re.findall('\\\\', 'a*b+c?\\d123d\\') ['\\', '\\']// 第一个\表示转义字符,第二个反斜杠表示真正的反斜杠 >>> import re >>> re.findall(r'\\', 'a*b+c?\\d123d\\') //r表示读入原生字符 ['\\', '\\']
# 元字符的转义
一般转义(直接在前面加
\即可)\*——*\+——+\?——?\-——-``\^——^\$——$\|——|
括号的转义
- 方括号 [] 和 花括号 {} 只需转义开括号
- 圆括号 () 两个都要转义
>>> import re >>> re.findall('\(\)\[]\{}', '()[]{}') ['()[]{}'] >>> re.findall('\(\)\[\]\{\}', '()[]{}') # 方括号和花括号都转义也可以 ['()[]{}']转义函数
- 使用转义函数可以将整个文本转义,将整个文本看做是一个正常的字符串,将其中的特殊字符加上转义。
>>> import re >>> re.escape('\d') # 反斜杠和字母d转义 '\\\\d' >>> re.findall(re.escape('\d'), '\d') ['\\d'] >>> re.escape('[+]') # 中括号和加号 '\\[\\+\\]' >>> re.findall(re.escape('[+]'), '[+]') ['[+]']- 其他编程语言的函数

# 字符组中的转义
在字符组里只有三种情况需要转义
- 脱字符
^在中括号中,且在第一个位置需要转义- 转义前代表非
- 转义后代表普通字符
>>> import re
>>> re.findall(r'[^ab]', '^ab') # 转义前代表"非"
['^']
>>> re.findall(r'[\^ab]', '^ab') # 转义后代表普通字符
['^', 'a', 'b']
- 中划线
-在中括号中,且不在首尾位置- 在开头或者结尾不需要转义
- 在中间
- 转义前表示范围
- 转义后表示普通字符
>>> import re
>>> re.findall(r'[a-c]', 'abc-') # 中划线在中间,代表"范围"
['a', 'b', 'c']
>>> re.findall(r'[a\-c]', 'abc-') # 中划线在中间,转义后的
['a', 'c', '-']
>>> re.findall(r'[-ac]', 'abc-') # 在开头,不需要转义
['a', 'c', '-']
>>> re.findall(r'[ac-]', 'abc-') # 在结尾,不需要转义
['a', 'c', '-']
- 右括号
]在中括号中,且不在首位- 在首位不需要转义,表示普通字符
- 不在首位,需要转义,不然
[]会被认为提前结束
>>> import re
>>> re.findall(r'[]ab]', ']ab') # 右括号不转义,在首位
[']', 'a', 'b']
>>> re.findall(r'[a]b]', ']ab') # 右括号不转义,不在首位
[] # 匹配不上,因为含义是 a后面跟上b]
>>> re.findall(r'[a\]b]', ']ab') # 转义后代表普通字符
[']', 'a', 'b']
其他字符,无需转义
- []内部,
+,*,?,.,()等不需要转义,就代表原来的字符. - 但如果在中括号中出现
\d或\w等符号时,他们还是元字符本身的含义。- \d=数字——\w=字符
>>> import re
>>> re.findall(r'[.*+?()]', '[.*+?()]') # 单个长度的元字符
['.', '*', '+', '?', '(', ')']
>>> re.findall(r'[\d]', 'd12\\') # \w,\d等在中括号中还是元字符的功能
['1', '2'] # 匹配上了数字,而不是反斜杠\和字母d
# 示例
\\n\n\\——换行符\n,用△表示- 即
\\ n \n \\——>字符串转义\n△\ - 输入字符串——字符串转义——正则转义
\n——>△——>△=换行符\\n——>\n——>△=换行符\\\n——>\△——>△=换行符\\\\n——>\\n——>\n=反斜杠+n
>>> import re
>>> re.findall('\n', '\\n\n\\')
['\n'] # 找到了换行符
>>> re.findall('\\n', '\\n\n\\') //输入`\n`,匹配到`\n`
['\n'] # 找到了换行符
>>> re.findall('\\\n', '\\n\n\\') //输入`\△`,
['\n'] # 找到了换行符
>>> re.findall('\\\\n', '\\n\n\\')
['\\n'] # 找到了反斜杠和字母n
# 正则流派

# POSIX 流派
- BRE
- GNU BRE 只有一个 E,使用时“花圆问管加”(
{}()?|+)时都要转义 - 早期标准,BRE不支持
?+| - GNU BRE 支持,但是需要加转义字符,即,
\?\+\|
- GNU BRE 只有一个 E,使用时“花圆问管加”(
- ERE
- GNU ERE 名称中有两个 E,不需要再转义

# PCRE 流派
- 来源于
Prel分支- 这个流派显著特征是有\d、\w、\s 这类字符fin组简记方式
- 现在大部分编程语言支持的都是这个流派
# Linux中使用正则
- 按照 BRE 标准 实现的有 grep、sed 和 vi/vim 等
- 按照 ERE 标准 实现的有 egrep、awk 等

可以使用
man grep来查看支持那些标准
grep默认是 BRE流派egrep=grep -E,是ERE流派grep -P是PCRE 流派
# 正则应用
# 正则处理Unicode编码

\w——不能匹配汉字量词正常使用
'极客{3}'- 匹配
极客客客 - 表示的是客这个汉字重复3次,而不是客这个汉字对应的编码最后一个字节重复3次
- 如果重复是最后一个字节,应该
极(?:客){3}——分组的形式
- 匹配
# 在编辑器中使用正则(Sublime Text)
- Sublime 的一些快捷键
shift+alt+1-9(非小键盘)- 使屏幕显示相等数字的小窗口
ctrl+h——替换Ctrl+L——选择整行


光标移动和文本选择
- 按住
shift键选中,然后按左右键可以左右选择文本块 - 按住
shift+alt,光标可以按块移动,快速移动到下一个单词。
- 按住
多焦点编辑
查找——>快速查找全部
例子:提取JSON中的姓名和手机号
{ "error_code": 0, "result": { "data": [ { "name": "朱小明", "tel": "138xx138000" }, { "name": "王五", "tel": "139xx139000" } ] } } 朱小明 138xx138000 王五 139xx139000- 选中
": ",快速查找全部 - 按右方向键,将光标移动到引号右边
- 按住
shift+alt,快速选择整个引号内的内容 - 复制,粘贴即可
- 选中
竖向编辑
shift+鼠标右键,可同时操作处于同一列的文本,同时编辑ctrl+鼠标左键,可以选中多处文本,同时编辑。
内容提取
\S+@\S+\.\S+(?=;)——简单邮箱提取\S——任意非空白字符(?=;)——环视;
小李: jkmkqhvrc@265.com; 小王: atvl@sogou@.com; 小红: vtoispm@tom.com; 小真: olncckkerlikb@citiz.com; 小爱: mddbatlosa@msn.com; jkmkqhvrc@265.com atvl@sogou@.com vtoispm@tom.com olncckkerlikb@citiz.com mddbatlosa@msn.com
内容替换
(\S+@(\S+)\.\S+)==查找- 两个括号,两个分组,均可以用来引用
\1=jkmkqhvrc@265.com\2=265
- 两个括号,两个分组,均可以用来引用
\2邮箱 ===\1==替换jkmkqhvrc@265.com atvl@sogou@.com vtoispm@tom.com olncckkerlikb@citiz.com mddbatlosa@msn.com 265邮箱 ===jkmkqhvrc@265.com sogou@邮箱 ===atvl@sogou@.com tom邮箱 ===vtoispm@tom.com citiz邮箱 ===olncckkerlikb@citiz.com msn邮箱 ===mddbatlosa@msn.com
统计一篇英文文章中每个单词出现的次数
处理成一行一个单词
\W——\n将非字符转换成空格
(?<!\w)\s——``- 将左边不是字符的空格去掉
I have a cat, cat is a dog, dog is I cat. I have a cat cat is a dog dog is I cat I have a cat cat is a dog dog is I cat
使用sort 命令排序,uniq -c统计次数
uniq -c: 统计每行出现次数 sort : -n 按数字排序 -r 逆序排序 -k1 根据-t的分割,分成几域,取第1个域排序 -t 指定分隔符,默认的分隔符为空白字符和非空白字符之间的空字符 head -n10: 取前10行数据$ sort word.txt | uniq -c 2 I 2 a 3 cat 2 dog 1 have 2 is $ sort word.txt | uniq -c | sort -nr 3 cat 2 is 2 dog 2 a 2 I 1 have
# 在语言中用正则

校验
验证日期 2022-06-01
python
# 测试环境 Python3 >>> import re >>> re.match(r'\d{4}-\d{2}-\d{2}', '2020-06-01') <re.Match object; span=(0, 10), match='2020-06-01'> # 这个输出是匹配到了,范围是从下标0到下标10,匹配结果是2020-06-01 # re.search 输出结果也是类似的\A\Z表示文本的开头和结尾- 不建议使用
^和$,因为在多行模式下,可以表示每行的开头和结尾
# 测试环境 Python3 >>> import re >>> reg = re.compile(r'\A\d{4}-\d{2}-\d{2}\Z') # 建议先编译,提高效率 >>> reg.search('2020-06-01') is not None True >>> reg.match('2020-06-01') is not None # 使用match时\A可省略 Truejava
\A\z表示文本的开头和结尾
import java.util.regex.Matcher; import java.util.regex.Pattern; class Main { public static void main(String[] args) { //方法1,可以不加 \A 和 \z System.out.println(Pattern.matches("\\d{4}-\\d{2}-\\d{2}", "2020-06-01")); // true //方法2,可以不加 \A 和 \z System.out.println("2020-06-01".matches("\\d{4}-\\d{2}-\\d{2}")); // true //方法3,必须加上 \A 和 \z Pattern pattern = Pattern.compile("\\A\\d{4}-\\d{2}-\\d{2}\\z"); System.out.println(pattern.matcher("2020-06-01").find()); // true } }
提取
日志时间提取
- python
# 没有子组时 >>> import re >>> reg = re.compile(r'\d{4}-\d{2}') >>> reg.findall('2020-05 2020-06') ['2020-05', '2020-06'] # 有子组时 >>> reg = re.compile(r'(\d{4})-(\d{2})') >>> reg.findall('2020-05 2020-06') [('2020', '05'), ('2020', '06')]- 节约内存,使用迭代器
>>> import re >>> reg = re.compile(r'(\d{4})-(\d{2})') >>> for match in reg.finditer('2020-05 2020-06'): ... print('date: ', match[0]) # 整个正则匹配到的内容 ... print('year: ', match[1]) # 第一个子组 ... print('month:', match[2]) # 第二个子组 ... date: 2020-05 year: 2020 month: 05 date: 2020-06 year: 2020 month: 06java
import java.util.regex.Matcher; import java.util.regex.Pattern; class Main { public static void main(String[] args) { Pattern pattern = Pattern.compile("\\d{4}-\\d{2}"); Matcher match = pattern.matcher("2020-06 2020-07"); while (match.find()) { System.out.println(match.group()); } } }
替换
(02-20-2022)月日年——年月日(2022年02月20日)
- python
>>> import re >>> reg = re.compile(r'(\d{2})-(\d{2})-(\d{4})') >>> reg.sub(r'\3年\1月\2日', '02-20-2020 05-21-2020') '2020年02月20日 2020年05月21日' # 可以在替换中使用 \g<数字>,如果分组多于10个时避免歧义 >>> reg.sub(r'\g<3>年\g<1>月\g<2>日', '02-20-2020 05-21-2020') '2020年02月20日 2020年05月21日' # 返回替换次数 >>> reg.subn(r'\3年\1月\2日', '02-20-2020 05-21-2020') ('2020年02月20日 2020年05月21日', 2)- java
- 替换时 引用子组要用
$
- 替换时 引用子组要用
import java.util.regex.Matcher; import java.util.regex.Pattern; class Main { public static void main(String[] args) { //方法1,输出 2020年02月20日 2020年05月21日 System.out.println("02-20-2020 05-21-2020".replaceAll("(\\d{2})-(\\d{2})-(\\d{4})", "$3年$1月$2日")); //方法2,输出 2020年02月20日 2020年05月21日 final Pattern pattern = Pattern.compile("(\\d{2})-(\\d{2})-(\\d{4})"); Matcher match = pattern.matcher("02-20-2020 05-21-2020"); System.out.println(match.replaceAll("$3年$1月$2日")); } }切割
切割得到单词
- python
>>> import re >>> reg = re.compile(r'\W+') >>> reg.split("apple, pear! orange; tea") ['apple', 'pear', 'orange', 'tea'] # 限制切割次数,比如切一刀,变成两部分 >>> reg.split("apple, pear! orange; tea", 1) ['apple', 'pear! orange; tea']- java
import java.util.regex.Matcher; import java.util.regex.Pattern; class Main { public static void main(String[] args) { Pattern pattern = Pattern.compile("\\W+"); for(String s : pattern.split("apple, pear! orange; tea")) { System.out.println(s); } } }示例
xxx#163.com (请把#换成@)
- python
// 替换——在用正则提取邮箱 >>> reg=re.compile(r'#') >>> reg.sub(r'@','xxx#163.com') 'xxx@163.com' //提取 >>> reg=re.compile(r'\w+[#@]\w+.\w+') >>> reg.findall('联系邮箱:xxx#163.com (请把#换成@) 联系邮箱:xxx@163.com') ['xxx#163.com', 'xxx@163.com']- java
import java.util.regex.Matcher;
import java.util.regex.Pattern;
//替换
class Main {
public static void main(String[] args) {
Pattern compile = Pattern.compile("#");
Matcher matcher = compile.matcher("xxx#163.com");
System.out.println(matcher.replaceAll("@"));
}
}
//提取
class Main {
public static void main(String[] args){
//提取 带#或者带@的邮箱
Pattern pattern=Pattern.compile("\\w+[@#]\\w+.\\w+");
Matcher matcher = pattern.matcher("联系邮箱:xxx#163.com (请把#换成@) 联系邮箱:xxx@163.com");
while (matcher.find())
{
System.out.println(matcher.group());
}
}
}
# 原理

# 正则匹配原理
有穷状态自动机
- 正则之所以能够处理复杂文本,就是因为采用了有穷状态自动机
- 有穷自动机的具体实现称为正则引擎,主要有 DFA 和 NFA 两种
- DFA:确定性有穷自动机(Deterministic finite automaton) NFA:非确定性有穷自动机(Non-deterministic finite automaton)
正则匹配过程
编译**(compile)**的过程,其实就是生成自动机的过程
NFA工作机制
NFA 引擎的工作方式是,先看正则,再看文本,而且以正则为主导
字符串:we study on jikeshijian app 正则:jike(zhushou|shijian|shixi)
正则中的第一个字符是 j,NFA 引擎在字符串中查找 j,接着匹配其后是否为 i ,如果是 i 则继续,这样一直找到 jike
regex: jike(zhushou|shijian|shixi) ^ text: we study on jikeshijian app ^
我们再根据正则看文本后面是不是 z,发现不是,此时 zhushou 分支淘汰
regex: jike(zhushou|shijian|shixi) ^ 淘汰此分支(zhushou) text: we study on jikeshijian app ^
我们接着看其它的分支,看文本部分是不是 s,直到 shijian 整个匹配上。
shijian 在匹配过程中如果不失败,就不会看后面的 shixi 分支。
假设这里文本改一下,把 jikeshijian 变成 jikeshixi,正则 shijian 的 j 匹配不上时 shixi 的 x,会接着使用正则 shixi 来进行匹配,重新从 s 开始(NFA 引擎会记住这里)。
第二个分支匹配失败 regex: jike(zhushou|shijian|shixi) ^ 淘汰此分支(正则j匹配不上文本x) text: we study on jikeshixi app ^
再次尝试第三个分支 regex: jike(zhushou|shijian|shixi) ^ text: we study on jikeshixi app ^
DFA工作机制
DFA 会先看文本,再看正则表达式,是以文本为主导的
DFA 会从 we 中的 w 开始依次查找 j,定位到 j ,这个字符后面是 i。所以我们接着看正则部分是否有 i ,如果正则后面是个 i ,那就以同样的方式,匹配到后面的 ke
text: we study on jikeshijian app ^ regex: jike(zhushou|shijian|shixi) ^
继续进行匹配,文本 e 后面是字符 s ,DFA 接着看正则表达式部分,此时 zhushou 分支被淘汰,开头是 s 的分支 shijian 和 shixi 符合要求。
text: we study on jikeshijian app ^ regex: jike(zhushou|shijian|shixi) ^ ^ ^ 淘汰 符合 符合
然后 DFA 依次检查字符串,检测到 shijian 中的 j 时,只有 shijian 分支符合,淘汰 shixi,接着看分别文本后面的 ian,和正则比较,匹配成功。
text: we study on jikeshijian app ^ regex: jike(zhushou|shijian|shixi) ^ ^ 符合 淘汰
DFA与NFA
- 一般来说,DFA 引擎会更快一些,因为整个匹配过程中,字符串只看一遍,不会发生回溯,相同的字符不会被测试两次。也就是说 DFA 引擎执行的时间一般是线性的。DFA 引擎可以确保匹配到可能的最长字符串。但由于 DFA 引擎只包含有限的状态,所以它没有反向引用功能;并且因为它不构造显示扩展,它也不支持捕获子组。
- NFA 以表达式为主导,它的引擎是使用贪心匹配回溯算法实现。NFA 通过构造特定扩展,支持子组和反向引用。但由于 NFA 引擎会发生回溯,即它会对字符串中的同一部分,进行很多次对比。因此,在最坏情况下,它的执行速度可能非常慢。
POSIX NFA
- POSIX NFA 引擎与传统的 NFA 引擎类似,但不同之处在于,POSIX NFA 在找到可能的最长匹配之前会继续回溯,也就是说它会尽可能找最长的,如果分支一样长,以最左边的为准
- 比如使用正则 pos|posix 在文本 posix 中进行匹配,传统的 NFA 从文本中找到的是 pos,而不是 posix,而 POSIX NFA 找到的是 posix
三者比较

# 回溯
回溯是 NFA 引擎才有的,并且只有在正则中出现量词或多选分支结构时,才可能会发生回溯。
+比如我们使用正则 a+ab 来匹配 文本 aab 的时候,过程是这样的,a+ 是贪婪匹配,会占用掉文本中的两个 a,但正则接着又是 a,文本部分只剩下 b,只能通过回溯,让 a+ 吐出一个 a,再次尝试
.*如果正则是使用 .*ab 去匹配一个比较长的字符串就更糟糕了,因为 .* 会吃掉整个字符串(不考虑换行,假设文本中没有换行),然后,你会发现正则中还有 ab 没匹配到内容,只能将 .* 匹配上的字符串吐出一个字符,再尝试,还不行,再吐出一个,不断尝试

- 所以在工作中,我们要尽量不用 .*,可以用其他方式来替换
- 比如要提取引号中的内容时
".+?"——非贪婪模式"[^"]+"——双引号内部,非双引号的字符
- 所以在工作中,我们要尽量不用 .*,可以用其他方式来替换
# 优化原则
测试性能的方法
使用ipyhon来测试正则表达式
- win+R——输入 ipython
- ipython使用技巧 (opens new window)
In [1]: import re In [2]: x = '-' * 1000000 + 'abc' In [3]: timeit re.search('abc', x) 480 µs ± 8.06 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)可以通过前面 regex101.com 查看正则和文本匹配的次数
提前编译好正则
- compile
尽量准确表示匹配范围
- 我们可以写成
“[^"]+”。使用[^"]要比使用点号好很多
- 我们可以写成
提取出公共部分
- 因为 NFA 以正则为主导,会导致字符串中的某些部分重复匹配多次,影响效率。
(abcd|abxy)这样的表达式,可以优化成ab(cd|xy)(^this|^that) is——^th(is|at) is——锚点部分独立处理this|that——th(?:is|at)
出现可能性大的放左边
- 由于正则是从左到右看的,所有在分支中,把可能性大的放到左边
在必要时才使用子组
- 在正则中,括号可以用于归组,但如果某部分后续不会再用到,就不需要保存成子组。
(?:)——不保存子组
警惕嵌套的子组重复
(.*)*这个正则,匹配的次数会呈指数级增长
避免不同分支重复匹配
# 示例
针对这个示例,NFA 引擎的匹配过程
文本:a12----开始部分用△表示,结束部分用○表示
即:△a12○
正则:^(?=[a-z])[a-z0-9]+$
^——△
(?=[a-z])——a
[a-z0-9]——△——a12
$——○
# 正则解决常见问题

# 正则处理问题的基本思路
比如将问题分解成多个小问题,每个小问题见招拆招。
某个位置上可能有多个字符的话,就⽤字符组。
某个位置上有多个字符串的话,就⽤多选结构。
出现的次数不确定的话,就⽤量词。
对出现的位置有要求的话,就⽤锚点锁定位置。
在正则中比较难的是某些字符不能出现,这个情况又可以进一步分为以下两种
- 组成中不能出现
- 要查找的内容中不能出现某些字符——
[^aeiou]——不能出现元音 - 例如 要求密码6位,但是不能有两个连续数字出现
^((?!\d\d)\w){6}$(?!\d\d)——代表右边不能是两个数字,但是左边没有正则,即为空字符串
- 要查找的内容中不能出现某些字符——
- 要查找的内容前后不能出现——环视
- 组成中不能出现
# 常见问题
匹配数字
- 数字——
\d或[0-9] - 连续的多个数字——
\d+或[0-9]+ - n 位数字——
\d{n}。 - 至少 n 位数据——
\d{n,}。 - m-n 位数字——
\d{m,n}。
- 数字——
匹配正数、负数、小数(浮点数)
- 3,3.14,-3,+2.7,20.12
[+-]?\d+[.]?\d*[+-]?\d+(?:\.\d+)?- ——
(?:)表示不保留子组——\.表示转义.
- ——
- .5,+.5——考虑这种极端情况(负号的时候整数部分不能没有,而正数的时候整数部分可以没有)
- 正数:
+?(?:\d+(?:\.\d+)?|\.\d+)\d+(?:\.\d+)?和\.\d+两个分支
- 负数:
-\d+(?:\.\d+)? - 组合:
-\d+(?:\.\d+)?|\+?(?:\d+(?:\.\d+)?|\.\d+)——正数和负数两个分支
- 正数:
- 3,3.14,-3,+2.7,20.12
十六进制数
[0-9a-fA-F]+
手机号码

- 限制前两位
1[3-9]\d{9}
- 限制前三位
- 13:
13[0-9]\d{8} - 14:
14[5-9]\d{8} - 15:
15[0-35-9]\d{8} - 16:
16[2567]\d{8} - 17:
17[0-8]\d{8} - 18:
18[0-9]\d{8} - 19:
19[1389]\d{8} - 组合:
1(?:3[0-9]|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[1389])\d{8}
- 13:
- 限制前两位
身份证号码
- 第一代是 15 位,第二代是 18 位;如果是 18 位,最后一位可以是 X(或 x),两代开头都不能是 0
- 15:
[1-9]\d{14} - 18:
[1-9]\d{16}[\dXx] - 组合:
[1-9]\d{14}(\d{2}[\dXx])?
邮政编码
- 邮编一般为 6 位数字,需要加断言
(?<!\d)\d{6}(?!\d)——左边不是数字,右边不是数字
QQ号码
- QQ 号不能以 0 开头,最长的有 10 位,最短的从 10000(5 位)开始
[1-9]\d{4,9}?——\d{4,9},表示数字出现4次到9次
中文字符
- 中文属于多字节 Unicode 字符,通过 Unicode 属性,但有一些语言是不支持这种属性的
- 另外一个办法,就是码值的范围,中文的范围是 4E00 - 9FFF 之间
- Python,Java,JavaScript 中,Unicode 可以写成 \u码值 来表示,即匹配中文的正则可以写成 [\u4E00-\u9FFF],在 PHP 中使用,Unicode 就需要写成 \u{码值} 的样式。
# 测试环境,Python3 >>> import re >>> reg = re.compile(r'[\u4E00-\u9FFF]') >>> reg.findall("和伟忠一起学正则regex") ['和', '伟', '忠', '一', '起', '学', '正', '则']IPV4地址
- IPv4 地址通常表示成 27.86.1.226 的样式,4 个数字用点隔开,每一位范围是 0-255
- 简单版:
\d{1,3}(?:\.\d{1,3}){3} - 按位数考虑
- 1位——
1.--01.--001.0{0,2}\d
- 2位——
10.--010.0?\d\d
- 3位——
100.--255.1\d\d|2[0-5][0-5]
- 组合——
0{0,2}\d|0?\d\d|1\d\d|2[0-5][0-5]=x - 组合改进——长的分支放到左边——
1\d\d|2[0-5][0-5]|0?\d\d|0{0,2}\d=x
- 1位——
- 加上点——x.x.x.x
(?:x)(?:\.(?:x)){3}(?:1\d\d|2[0-5][0-5]|0?\d\d|0{0,2}\d)(?:\.(?:1\d\d|2[0-5][0-5]|0?\d\d|0{0,2}\d)){3}
日期
- 日期格式 yyyy-mm-dd
- 简单:
\d{4}-\d{2}-\d{2} - 复杂:考虑月份 1-12,一位时0可带可不带;日期 1-31(像 30,29,28这种应该交给程序来完成,不要用正则完成所有的事情)
- 年:
\d{4} - 月:
1[0-2]|0?[1-9]——两位的写在前面 - 日:
[12]\d|3[01]|0?[1-9] - 组合:
\d{4}-(?:1[0-2]|0?[1-9])-(?:[12]\d|3[01]|0?[1-9])
- 年:
时间
- 24小时制——23:24
- 小时:0-23;分钟:0-59
(?:2[0-3]|1[0-9]|0?[0-9]):(?:[1-5][0-9|0?[0-9]])- 12小时制——11:59
- 小时:0-11;分钟:0-59
(?:1[01]|0?[0-9]):(?:[1-5][0-9|0?[0-9]])
邮箱
- 格式是 用户名 @主机名,用户名部分通常可以有英文字母,数字,下划线,点等组成,但其中点不能在开头,也不能重复出现
- 简单版:
[\w.]+@[\w]+\.[\w]+
网页标签
- 配对出现的标签,不区分大小写,title
(?si)<title.*?>.*<\/title>
- 提取引号里面的内容
".+""[^"]+"——带""(?<=")[^"]+(?=")——不带""
- 提取尖括号<>里面的内容
<[^>]+>——带<>(?<=<)[^>]+(?=>)——不带<>
- 配对出现的标签,不区分大小写,title
# 从编程语言的角度来理解正则表达式

使用第 4 代语言来描述问题,而无需花费大量时间,去考虑具体的处理逻辑和算法实现,处理逻辑和算法实现是由编译器(Compiler)或解释器(Interpreter)这样的语言解析引擎来负责。
- 正则表达式属于声明式编程范式
声明式编程范式,主要是模拟人脑思维的过程。声明式重目标、轻过程,专注问题的分析和表达,而不是算法实现。它不用指明执行顺序,属于目标导向,强调的是定义问题的描述——即“做什么”,因而目标是显性而算法是隐性的。
- 因此,从编程范式的角度来看:
- 声明式编程的世界观是:程序是由若干目标任务组成的有序列表;
- 声明式编程的方法论是:用语法元素来描述任务,由解析引擎转化为指令并执行。
正则表达式的语法元素本质上就是程序逻辑和算法
正则表达式中的星号量词“*”这一元字符,就是高级语言的处理逻辑“循环结构”的体现。具体来说,星号量词“*”代表的是不定次数循环结构,而前后多个星号量词的嵌套就是多层不定次数循环结构的嵌套;
或运算符,也就是竖线“|”这个元字符,就是高级语言的处理逻辑“分支结构”的体现;
而用于分组的圆括号“()”,就相当于高级语言的作用域。
而当或运算符“|”出现在由星号量词“*”所限定的分组圆括号“()”中时,其实就是在“循环结构”中嵌套了“分支结构”;
而如果进一步地,“循环结构”所嵌套的“分支结构”中的某个分支,又被某个星号量词“*”所限定,那么则相当于“循环结构”所嵌套的“分支结构”又嵌套了“循环结构”。
(张三|李四 *)*
