正则表达式详解
当前位置:点晴教程→知识管理交流
→『 技术文档交流 』
1 正则是个啥维基百科说:
使用 Linux 比较多的朋友应该有使用过 说这么多,正则表达式可能是一个可意会难言传的名词,通常用来做字符串查找或者替换。如果还觉得难理解,那把它当隔壁老王也是可以的——虽然不知道为啥这个邻居叫老王,但大家说老王的时候知道是这个邻居就行了,反正正则表达式就这么个东西。 2 正则的理论这一节用来提升一下高度,期望通过这一节可以更透彻地理解正则表达式(不理解也没关系的,不懂发动机原理并不妨碍咱们开车)。 正则表达式可以用形式化语言理论的方式来表达。正则表达式由常量和算子组成,它们分别表示字符串的集合和在这些集合上的运算。给定有限字母表 , 定义了下列常量:
定义了下列运算:
上述常量和算子形成了克莱尼代数。 也有文档使用对选择使用符号 、 或 替代竖线。 为了避免括号,假定 Kleene 星号有最高优先级,接着是串接,接着是并集。如果没有歧义则可以省略括号。例如:(ab)c 可以写为 abc 而 a|(b(c*)) 可以写为 a|bc*。 例子:
为了使表达式更简洁,正则表达式也定义了 3 正则的语法好了,长篇大论那么多,终于进入“正题”。
正则语法应该是通用的,至少我用 3.1 基本概念在介绍语法之前,先介绍几个基本概念。 3.1.1 模式(pattern)朋友们在查资料的时候可能会经常碰到这个词语,模式(pattern)其实指的就是某个正则表达式。 3.1.2 选择( |
\ | |
()(?:) 、 (?=) 、 [] | |
*+ 、 ? 、 {n} 、 {n,} 、 {n,m} | |
^$ 、3.2.2 里的其他特殊字符 | |
| |
Python 内置一个正则库 re,使用的时候直接引入即可:
import re
re 中的标志(flags)re.ASCII | \w\W, \b, \B, \d, \D, \s 和 \S 只匹配 ASCII 字符 |
re.IGNORECASE | |
re.LOCALE | \w\W, \b, \B 和大小写遵循当前 locale |
re.MULTILINE | ^$ 匹配每行的行末 |
re.DOTALL | . |
re.VERBOSE |
re 中的特殊字符(组合)这里给出一张表,可以扫一眼,后面用到记不准的时候再查就可以了。不好理解的看后面示例。
^ | |
$ | |
. | DOTALL 标志则匹配包含换行的所有字符。 |
* | ab* 会匹配 a,ab,或者 a 后面跟随任意个 b |
+ | ab+ 会匹配 a 后面跟随 1 个以上到任意个 b,它不会匹配 a |
? | ab? 会匹配 a 或者 ab |
*?+?, ?? | |
{m} | m 次重复,a{6} 将匹配 6 个 a |
{m,} | m 次重复,a{4,}b 将匹配 aaaab 或者 1000 个 a 尾随一个 b |
{,n} | n 次重复,a{,2}b 将匹配 b,ab,aab |
{m,n} | m~n 次重复,a{3,5} 将匹配 3 到 5 个 a |
{m,n}? | aaaaaa, a{3,5} 匹配 5 个 a ,而 a{3,5}? 只匹配 3 个 a |
| | A|BA 和 B 可以是任意正则表达式,创建一个正则表达式,匹配 A 或者 B。注意,这有“短路”特性,即匹配 A 之后就不再匹配 B |
\ | |
\数字 | (.+) \1 匹配 the the 或者 55 55, 但不会匹配 thethe (注意组合后面的空格)。这个特殊序列只能用于匹配前面 99 个组合。 |
\A | MULTILINE 标志时)下与 ^ 一样。 |
\b | \b 定义为 \w 和 \W 字符之间,或者 \w 和字符串开始 / 结尾的边界, 意思就是 r'\bfoo\b' 匹配 foo, foo., (foo), bar foo baz 但不匹配 foobar 或者 foo3。 |
\B | r'py\B' 匹配 python, py3, py2, 但不匹配 py, py., 或者 py!。 \B 是 \b 的取非 |
\d | [0-9] |
\D | \d 取非。 如果设置了 ASCII 标志,就相当于 [^0-9] |
\s | [ \t\n\r\f\v](注意第一个为空格) |
\S | [^ \t\n\r\f\v] |
\w | [a-zA-Z0-9_] |
\W | \w 正相反。如果使用了 ASCII 旗标,这就等价于 [^a-zA-Z0-9_]。 |
\Z | |
[] | 1. 字符可以单独列出,比如 [amk] 匹配 a, m, 或者 k2. 可以表示字符范围,通过用 - 将两个字符连起来。比如 [a-z] 将匹配任何小写 ASCII 字符, [0-5][0-9] 将匹配从 00 到 59 的两位数字3. 特殊字符在集合中,失去它的特殊含义。比如 [(+*)] 只会匹配这几个文法字符 (, +, *, 或者 )4. 字符类如 \w 或者 \S 可以匹配的字符由 ASCII 或者 LOCALE 标志决定5. 不在集合范围内的字符可以通过取反来进行匹配。如果集合首字符是 ^ ,所有不在集合内的字符将会被匹配,比如 [^9] 将匹配所有字符,除了 9, [^^] 将匹配所有字符,除了 ^, ^ 如果不在集合首位,则没有特殊含义。 |
(...) | \数字 转义序列进行再次匹配。要匹配字符 ( 或者 ), 用 \( 或 \), 或者把它们包含在字符集合里:[(], [)]。 |
(?…) | ? 跟随 ( 并无含义),? 后面的第一个字符决定了这个构建采用什么样的语法,下面列出所支持的扩展。 |
(?:…) | |
(?aiLmsux) | a, i, L, m, s, u, x 中的一个或多个) 这个组合匹配一个空字符串;这些字符对正则表达式设置以下标记 re.A (只匹配 ASCII 字符), re.I (忽略大小写), re.L (语言依赖), re.M (多行模式), re.S ( . 匹配全部字符), re.U (Unicode 匹配), 和 re.X (冗长模式)。 这个方法免去了在 re.compile() 中传递 flag 参数。标记应该放在正则表达式开始。 |
(?aiLmsux-imsx:…) | |
(?P<name>…) | |
(?P=name) | name 的命名组中匹配到的串一样的字串。 |
(?#…) | |
(?=…) | … 的内容,但是并不消费模式的内容。这个叫做 Positive Lookahead Assertion。 |
(?!…) | … 不符合的情况。这个叫 Negative Lookahead Assertion。 |
(?<=…) | … 的内容到当前位置。这叫 Positive Lookbehind Assertion。 |
(?<!…) | … 的模式。这个叫 Negative Lookbehind Assertion。 |
(?(id/name)yes-pattern|no-pattern) |
这里介绍了 re 正则里的特殊字符(组合),大部分都是比较好理解的,有些不好理解的也可以不使用。但几个 Assertion 可能是重要但又不好理解的。之所以没写中文,是因为流行的翻译更难理解,索性直接用英文好了。
Assertion 通常译为 断言,用于测试一个假设是否成立。上面表格看着有四种,但可以分为两类:Lookahead 和 Lookbehind。
这两个词好理解,就是 向前看 和 向后看 嘛,但这里的前后需要结合阅读顺序。现代阅读方向基本都是从左往右的(如果还有从右往左的请告诉我!),前后 是按照阅读方向来说的,即:从左向右为前;从右向左为后。
剩下两个词是 Positive 和 Negative,分别是 正的/阳性的/肯定 和 负的/阴性的/否定,代表了 包含 和 不包含。
于是这四类断言便可分为:向前看包含,向前看 不 包含,向后看包含,向后看 不 包含。但刚才也说了,这里的前后容易误会,所以不如引入树的两个概念,前驱 和 后继,更好理解,比如:前驱包含,前驱不包含,后继包含,后继不包含,感觉要清晰得多。或者干脆用 左右 代替:左右包含断言 、 左右不包含断言 、 右左包含断言 、 右左包含断言 😂
Anyway,理解了本质,叫什么其实不重要了。现在朋友们应该清楚这四个断言了。如果还有疑惑,就看后面的例子吧。
TL;DR:大部分时候,认准 findall 就可以了;有其他需求,再看其他的。
将正则表达式的模式编译为一个正则表达式对象(正则对象),然后可以通过这个对象的方法,如 match(), search() 等,进行匹配。其主要目的,是让多次使用某个模式(正则表达式)的时候更加高效。
扫描整个 字符串 找到匹配模式的第一个位置,并返回一个相应的 匹配对象。如果没有匹配,就返回一个 None。
如果 string 开始的 0 或者多个字符匹配到了正则表达式模式,就返回一个相应的匹配对象。如果没有匹配,就返回 None。
如果整个 string 匹配到正则表达式模式,就返回一个相应的匹配对象。否则就返回一个 None。
用 pattern 分开 string。如果在 pattern 中捕获到括号,那么所有的组里的文字也会包含在列表里。如果 maxsplit 非零,最多进行 maxsplit 次分隔,剩下的字符全部返回到列表的最后一个元素。
这两个方法类似,都是查找所有符合模式的字符串,区别在于,findall 返回一个列表;finditer 返回一个迭代器。
返回通过使用 repl 替换在 string 最左边非重叠出现的 pattern 而获得的字符串。 如果模式没有找到,则返回原 string。 repl 可以是字符串或函数;如为字符串,则其中任何反斜杠转义序列都会被处理。 也就是说,\n 会被转换为一个换行符,\r 会被转换为一个回车符,依此类推。 未知的 ASCII 字符转义序列保留在未来使用,会被当作错误来处理。 其他未知转义序列例如 \& 会保持原样。 向后引用,如 \6, 会用模式中第 6 组所匹配到的子字符串来替换。
作用与 sub() 相同,但是返回一个元组 (字符串,替换次数)
转义 pattern 中的特殊字符。
清除正则表达式的缓存。
这部分可能是多数朋友想看的部分了。这里就演示 一招鲜,只使用 findall,如果不能满足需求,再去研究其他方法吧。
注:跑本节示例需要引入 re 包:
import re
3.2.2 示例先把 3.2.2 里介绍的特殊字符(组合)示例一下。
# ^
re.findall(r"^From", "From Here to Eternity")
# ['From']
re.findall(r"^From", "Reciting From Memory")
# []
# $
re.findall(r"}$", "{block}")
# ['}']
re.findall(r"}$", "{block}\n")
# ['}']
re.findall(r"}$", "{block} ")
# []
# .
re.findall(r"a.b", "axb")
# ['axb']
re.findall(r"a.b", "a\nb")
# []
re.findall(r"a.b", "a\nb', re.DOTAL")
# ['a\nb']
# *
re.findall(r"ab*", "a")
# ['a']
re.findall(r"ab*", "ab")
# ['ab']
re.findall(r"ab*", "abb")
# ['abb']
# +
re.findall(r"ab+", "a")
# []
re.findall(r"ab+", "ab")
# ['ab']
re.findall(r"ab+", "abb")
# ['abb']
# ?
re.findall(r"ab?", "a")
# ['a']
re.findall(r"ab?", "ab")
# ['ab']
re.findall(r"ab?", "abb")
# ['ab']
# *? +? ??
re.findall(r"ab*?", "abb")
# ['a']
re.findall(r"ab+?", "abb")
# ['ab']
re.findall(r"ab??", "abb")
# ['a']
# {m} {m,} {m,n} {,n} {m,n}?
re.findall(r"ab{1}", "abbbbbb")
# ['ab']
re.findall(r"ab{1,}", "abbbbbb")
# ['abbbbbb']
re.findall(r"ab{1,3}", "abbbbbb")
# ['abbb']
re.findall(r"ab{,3}", "abbbbbb")
# ['abbb']
re.findall(r"ab{3,5}?", "abbbbbb")
# ['abbb']
# |
re.findall(r"a|ab", "abbbbbb")
# ['a'] # 注意前面提到的“短路”特性,匹配 a 之后就不再匹配 ab 了
# \
re.findall(r"a\(b\)", "a(b)bbbbb")
# ['a(b)']
s = """a
b"""
re.findall(r"a\nb', ")
# ['a\nb']
# \数字
re.findall(r"(.+) \1", "the the")
# ['the'] # 对比下面,更多可查询:python raw string
re.findall("(.+) \1", "the the")
# []
# \A
re.findall(r"\Aabc", "abcdefg")
# ['abc']
re.findall(r"\Aabc", "1abcdefg")
# []
# \b
re.findall(r"\bclass\b", "no class at all")
# ['class']
re.findall(r"\bclass\b", "the declassified algorithm")
# []
# \B
re.findall(r"\Bclass\B", "the declassified algorithm")
# ['class']
re.findall(r"\Bclass\B", "no class at all")
# []
# \d 等效为 [0-9]
re.findall(r"\d", "123abc")
# ['1', '2', '3']
re.findall(r"\d", "abc")
# []
# \D 等效为 [^0-9]
re.findall(r"\D", "abc")
# ['a', 'b', 'c']
re.findall(r"\D", "123")
# []
# \s 等效为 [ \t\n\r\f\v]
re.findall(r"a\sb", "a b")
# ['a b']
re.findall(r"a\sb", "ab")
# []
# \S 等效为 [^ \t\n\r\f\v]
re.findall(r"a\Sb", "a&b")
# ['a&b']
re.findall(r"a\Sb", "a b")
# []
# \w 等效为 [a-zA-Z0-9_]
re.findall(r"a\wb", "a0b")
# ['a0b']
re.findall(r"a\wb", "a&b")
# []
# \W 等效为 [^a-zA-Z0-9_]
re.findall(r"a\Wb", "a&b")
# ['a&b']
re.findall(r"a\Wb", "a0b")
# []
# \Z
re.findall(r"abc\Z", "123abc")
# ['abc']
re.findall(r"abc\Z", "abc123")
# []
# (...)
re.findall(r"(ab)", "ababcdefg")
# ['ab', 'ab']
re.findall(r"(ab)*", "ababcdefg")
# ['ab', '', '', '', '', '', '']
# (?:…)
re.findall(r"(?:ab)ab", "ababcdefg")
# ['abab']
# (?P<name>…) 与 (?P=name)
re.findall(r"(?P<word>.+) (?P=word)", "the the")
# ['the'] # 对比前面 \数字
# (?#…)
re.findall(r"(?#这个表达式用于演示注释和组合命令)(?P<word>.+) (?P=word)", "the the")
# ['the']
# 下面演示四种断言
# 左右包含断言 (?=…)
re.findall(r"Isaac(?=\sAsimov)", "Isaac Asimov")
# ['Isaac']
re.findall(r"Isaac(?=\sAsimov)", "Isaac simov")
# []
# 左右不包含断言 (?!…)
re.findall(r"Isaac(?!\sAsimov)", "Isaac asimov")
# ['Isaac']
re.findall(r"Isaac(?!\sAsimov)", "Isaac Asimov")
# []
# 右左包含断言 (?<=…)
re.findall(r"(?<=-)\w+", "spam-egg")
# ['egg']
re.findall(r"(?<=-)\w+", "spamegg")
# []
# 右左不包含断言 (?<!…)
re.findall(r"(?<!-)\w+", "spamegg")
# ['spamegg']
re.findall(r"(?<!-)^\w+$", "spam-egg")
# []
下面举一些常用的例子。需要注意的是,大部分事情都是八仙过海各显神通,方法并不唯一。
为了方便,使用同一串字符串:
string = "我的身份证号是110010200012200036,我的私人邮箱是chuck@outlook.com,公司邮箱是12345678@qq.org,邮编是520040,出生日期是2000-12-20,入职时间2021/12/20,手机号是18811882288,办公室电话010-88888888,服务器地址有:192.168.1.1 192.168.1.85 192.168.1.86 127.0.0.1 256.1.1.1 192.256.256.256 192.255.255.255 aa.bb.cc.dd"
中文的编码从 \u4e00 到 \u9fa5。
pattern = r"[\u4e00-\u9fa5]+"
re.findall(pattern, string)
['我的身份证号是', '我的私人邮箱是', '公司邮箱是', '邮编是', '出生日期是', '入职时间', '手机号是', '办公室电话', '服务器地址有']
用户名为 4~20 个字符,包含大小写字母,下划线,阿拉伯数字,点号,中划线;二域名为 1~10 个字符;顶级域名为 1~10 个字符。
pattern = r"[a-zA-Z0-9_-]{4,20}@[a-zA-Z0-9_-]{1,10}[.][a-zA-Z0-9_-]{1,10}"
re.findall(pattern, string)
# ['chuck@outlook.com', '123456@qq.org']
身份证十八位,前六位是地区,然后是六位生日,然后是三位顺序,然后是一位校验。
pattern = r"[1-9]\d{5}(?:19|(?:2\d))\d{2}(?:(?:0[1-9])|(?:10|11|12))(?:(?:[0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]"
re.findall(pattern, string)
# ['110010200012200036']
邮编前两位数字表示省(直辖市、自治区),第三位数字表示邮区;第四位数字表示县(市);最后两位数字表示投递局(所)。
pattern = r"(?<!\d)[1-9]\d{5}(?!\d)"
re.findall(pattern, string)
# ['520040']
常见日期格式:yyyyMMdd、yyyy-MM-dd、yyyy/MM/dd、yyyy.MM.dd。
pattern = r"(?<!\d)\d{4}(?:-|\/|.)\d{2}(?:-|\/|.)\d{2}(?!\d)"
re.findall(pattern, string)
# ['2000-12-20', '2021/12/20']
IPv4 的定义 IPv4 地址是 32 位的二进制数字,可转换为 4 个 0~255 之间的数字。
pattern = r"(?<![\.\d])(?:25[0-5]\.|2[0-4]\d\.|[01]?\d\d?\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)(?![\.\d])"
re.findall(pattern, string)
# ['192.168.1.1', '192.168.1.85', '192.168.1.86', '127.0.0.1', '192.255.255.255']
区号 3~4 位,号码 7~8 位。
pattern = r"\d{3}-\d{8}|\d{4}-\d{7}"
re.findall(pattern, string)
# ['010-88888888']
手机号以 1 开头,第二位一般是 356789:
pattern = r"1[356789]\d{9}"
re.findall(pattern, string)
# ['18811882288']
根据微信的提示:微信帐号长度限 6-20 位,建议避免包含姓名、生日等涉及个人隐私的信息。实测发现,只能以字母开头,不能使用汉字:
微信号须以字母开头,仅支持 6~20 个字母、数字、下划线、减号自由组合。
pattern = r"([a-zA-Z][\w|-]{5,19})"
re.findall(pattern, string)
# 正常来说这样就可以了
但一般都是不正常的。比如前面的 寻找发言人,是从一个字节数组里找到微信号。
下面的几种情况都可以用同一个模式解决:
pattern = r"\\x.{2}(?:\\t|\\r|\\n)?([a-zA-Z][\w|-]{5,19})"
string = b'\n\x04\x08\x10\x10\x00\x1a\x0f\x08\x01\x12\x0bxiang090705\x1aw\x08\x07\x12s<msgsource>\n\t<silence>1</silence>\n\t<membercount>229</membercount>\n\t<signature>v1_/2pgK9XJ</signature>\n</msgsource>\n\x1a$\x08\x02\x12 c1c0a01f588dff1477906712b410dcb6'
re.findall(pattern, str(string))
# ['xiang090705']
string = b'\n\x04\x08\x10\x10\x00\x1a\x16\x08\x01\x12\rwxid_4311119412\x1aw\x08\x07\x12s<msgsource>\n\t<silence>1</silence>\n\t<membercount>229</membercount>\n\t<signature>v1_ur8vW3ou</signature>\n</msgsource>\n\x1a$\x08\x02\x12 1d0821bd376a7b800eaae34421641d75'
re.findall(pattern, str(string))
# ['wxid_4311119412']
string = b'\n\x04\x08\x10\x10\x00\x1a\r\x08\x01\x12\taaa850414\x1aw\x08\x07\x12s<msgsource>\n\t<silence>1</silence>\n\t<membercount>229</membercount>\n\t<signature>v1_esI/AIx0</signature>\n</msgsource>\n\x1a$\x08\x02\x12 e5ad5ed70899ad68ecc494e0020a85a4'
re.findall(pattern, str(string))
# ['aaa850414']