Python正则表达式
概述
- 正则表达式,Regular Expression,缩写为regex、regexp、RE等。
- 正则表达式是文本处理极为重要的技术,用它可以对字符串按照某种规则进行检索、替换。
- 1970年代,Unix之父Ken Thompson将正则表达式引入到Unix中文本编辑器ed和grep命令中,由此正则表达式普及开来。
- 1980年后,perl语言对Henry Spencer编写的库,扩展了很多新的特性。1997年开始,Philip Hazel开发出了PCRE (Perl Compatible Regular Expressions),它被PHP和HTTPD等工具采用。
- 正则表达式应用极其广泛,shell中处理文本的命令、各种高级编程语言都支持正则表达式。
分类
BRE
- 基本正则表达式,
grep、sed、vi
等软件支持。vim
有扩展。
- 基本正则表达式,
ERE
- 扩展正则表达式,
egrep(grep -E)、sed -r
等。
- 扩展正则表达式,
PCRE
- 几乎所有高级语言都是
PCRE
的方言或者变种。Python从1.6开始使用SRE正则表达式引擎,可以认为是PCRE
的子集,见模块re
。
- 几乎所有高级语言都是
基础语法
元字符 metacharacter
正则表达式 | 说明 | 示例 |
---|---|---|
. | 匹配除换行符外的任意一个字符 | a.b 匹配 “axb”, “aab”, 但不匹配 “a\nb” |
[abc] | 字符集合,匹配所包含的任意一个字符 | [abc] 匹配 “plain” 中的 ‘a’ |
abc | 字符集合,匹配除去集合内字符的任意一个字符 | [^abc] 匹配 “plain” 中的 ‘p’、’l’、’i’ 或 ‘n’ |
[a-z] | 字符范围,匹配所包含的任意一个字符 | [a-z] 匹配小写字母 |
\b | 匹配单词的边界 | \bb 在文本中找到以 ‘b’ 开头的 ‘b’ 字符 |
\B | 不匹配单词的边界 | t\B 匹配包含 ‘t’ 的单词但不以 ‘t’ 结尾的 ‘t’ 字符,例如 “write” |
\d | 匹配一个数字 | \d 匹配 “123” 中的 ‘1’ |
\D | 匹配一个非数字 | \D 匹配 “abc” 中的 ‘a’ |
\s | 匹配一个空白字符 | \s 匹配空格、制表符等 |
\S | 匹配一个非空白字符 | \S 匹配 “word” 中的 ‘w’ |
\w | 匹配一个单词字符 | \w 匹配 “word” 中的 ‘w’ |
\W | 匹配一个非单词字符 | \W 匹配 “123” 中的 ‘1’ |
- 在正则表达式中、取反的时候要慎用
转义
凡是在正则表达式中有特殊意义的符号,如果想使用它的本意,请使用\转义。 反斜杠自身,得使用\
\r、\n还是转义后代表回车、换行
重复
正则符号 | 说明 | 举例 |
---|---|---|
* | 表示前面的正则表达式会重复0次或多次 | e\w* 单词中e后面可以有非空白字符 |
? | 表示前面的正则表达式会重复0次或1次 | e\w+ 单词中e后面至少有一个非空白字符 |
+ | 表示前面的正则表达式重复至少1次 | e\w? 单词中e后面至多有一个非空白字符 |
{n} | 重复固定的n次 | e\w{1} 单词中e后面只能有一个非空白字符 |
{n,} | 重复至少n次 | e\w{1,} 等价 e\w+ e\w{0,} 等价 e\w* e\w{0,1} 等价 e\w? |
{n,m} | 重复n到m次 | e\w{1,10} 单词中e后面至少1个,至多10个非空 白字符 |
或
代码 | 说明 | 举例 |
---|---|---|
x|y | 匹配x或者y | wood took foot food使用 w|food 或者 (w|f)ood |
捕获
代码 | 说明 | 举例 |
---|---|---|
(pattern) |
使用小括号指定一个子表达式,也叫分组 捕获后会自动分配组号从1开始 |
可以改变优先级 | |
| \数字
| 匹配对应的分组 | (very) \1 匹配very very,但捕获的组group是very |
| (?:pattern)
| 如果仅仅为了改变优先级,就不需要捕获分组 | (?:w|f)ood ‘industr(?:y|ies)’ 等价 ‘industry|industries’ |
| (?<name>exp)``(?'name'exp)
| 命名分组捕获,但是可以通过name访问分组。Python语法必须是(?P<name>exp)
| |
断言
零宽断言
- 测试字符串为wood took foot food
代码 | 说明 | 举例 |
---|---|---|
(?=exp) |
零宽度正预测先行断言 | f(?=oo) f后面一定有oo出现 |
(?<=exp) |
零宽度正回顾后发断言 | (?<=f)ood 、(?<=t)ook 分别匹配 ood 、ook ,ook 前一定有 t 出现 |
负向零宽断言
代码 | 说明 | 举例 |
---|---|---|
(?!exp) |
零宽度负预测先行断言断言exp一定不会出现在右侧,也就是说断言后面一定是exp | \d{3}(?!\d) 匹配3数字,断言3位数字后面一定不能是数字 foo(?!d) foo后面一定不是d |
(?<!exp) |
零宽度负回顾后发断言 断言exp一定不能出现在左侧,也就是说断言前面一定不能是exp | (?<!f)ood ood的左边一定不是f |
代码 | 说明 | 举例 |
---|---|---|
(?#comment) |
注释 | f(?=oo)(?#这个后断言不捕获) |
贪婪与非贪婪
默认是贪婪模式,也就是说尽量多匹配更长的字符串。
非贪婪很简单,在重复的符号后面加上一个?问号,就尽量的少匹配了。
代码 | 说明 | 举例 |
---|---|---|
*? |
匹配任意次,但尽可能少重复 | |
+? |
匹配至少1次,但尽可能少重复 | |
?? |
匹配0次或1次,但尽可能少重复 | |
{n,}? |
匹配至少n次,但尽可能少重复 | |
{n,m}? |
匹配至少n次,至多m次,但尽可能少重复 |
very very happy 使用v.*y和v.*?y
引擎选项
代码 | 说明 | Python |
---|---|---|
IgnoreCase |
匹配时忽略大小写 | re.I re.IGNORECASE |
Singleline |
单行模式,. 可以匹配所有字符,包括\n |
re.S re.DOTALL |
Multiline |
多行模式,^ 行首、$ 行尾 |
re.M re.MULTILINE |
IgnorePatternWhitespace |
忽略表达式中的空白字符,如果要使用空白字符用转义,# 可以用来做注释 |
re.X re.VERBOSE |
单行模式:
. 可以匹配所有字符,包括换行符
^ 表示整个字符串的开头,$整个字符串的结尾
多行模式:
. 可以匹配除了换行符之外的字符,多行不影响.点号
^ 表示行首,$行尾,只不过这里的行是每一个行
默认模式:可以看做待匹配的文本是一行,不能看做多行,.点号不能匹配换行符,^和$表示行首和行
尾,而行首行尾就是整个字符串的开头和结尾
单行模式:基本和默认模式一样,只是.点号终于可以匹配任意一个字符包括换行符,这时所有文本就是
一个长长的只有一行的字符串。^就是这一行字符串的行首,$就是这一行的行尾。
多行模式:重新定义了行的概念,但不影响.点号的行为,^和$还是行首行尾的意思,只不过因为多行模
式可以识别换行符了。”开始”指的是\n后紧接着下一个字符;”结束”指的是\n前的字符,注意最后一行
结尾可以没有\n
简单讲,单行模式只影响.点号行为,多行模式重新定义行影响了^和$
注意:注意字符串中看不见的换行符,\r\n会影响e$的测试,e$只能匹配e\n
- 举例
very very happy
my primary key
上面2行happy
之后,有可能是\r\n
结尾。y$
单行匹配key的y
,多行匹配happy和key的y
。
.$指的是此行的结尾,而默认模式和单行模式都是一行,指的是这个大字符串的最后一个字符,就是key的y
。
正则练习参考
IP地址
- 匹配合法的IP地址
192.168.1.150
0.0.0.0
255.255.255.255
17.16.52.100
172.16.0.100
400.400.999.888
001.022.003.000
257.257.255.256
(\d{1,3}\.){3}\d{1,3}
(?:\d{1,3}\.){3}\d{1,3} # 400.400.999.888
- 对于ip地址验证的问题
- 可以把数据提出来后,交给IP地址解析库 处理,如果解析异常,就说明有问题,正则的验证只是一个初步的筛选,把明显错误过滤掉。
- 可以使用复杂的正则表达式验证地址正确性
- 前导0是可以的
import socket
nw = socket.inet_aton('192.168.05.001') # 错了抛异常 print(nw, socket.inet_ntoa(nw))
# 分析: 每一段上可以写的数字有1、01、001、000、23、023、230、100,也就说1位就是0-9,2位每一位也是 0-9,3位第一位只能0-2,其余2位都可以0-9 (?:([0-2]\d{2}|\d{1,2})\.){3}([0-2]\d{2}|\d{1,2}) # 解决超出200的问题,但是256 呢?
200是特殊的,要再单独分情况处理
25[0-5]|2[0-4]\d|[01]?\d\d? 这就是每一段的逻辑 (?:(25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)
提取文件名
- 选出含有ftp的链接,且文件类型是gz或者xz的文件名
ftp://ftp.astron.com/pub/file/file-5.14.tar.gz
ftp://ftp.gmplib.org/pub/gmp-5.1.2/gmp-5.1.2.tar.xz
ftp://ftp.vim.org/pub/vim/unix/vim-7.3.tar.bz2
http://anduin.linuxfromscratch.org/sources/LFS/lfs-
packages/conglomeration//iana-etc/iana-etc-2.30.tar.bz2
http://anduin.linuxfromscratch.org/sources/other/udev-lfs-205-1.tar.bz2
http://download.savannah.gnu.org/releases/libpipeline/libpipeline-
1.2.4.tar.gz
http://download.savannah.gnu.org/releases/man-db/man-db-2.6.5.tar.xz
http://download.savannah.gnu.org/releases/sysvinit/sysvinit-2.88dsf.tar.bz2
http://ftp.altlinux.org/pub/people/legion/kbd/kbd-1.15.5.tar.gz
http://mirror.hust.edu.cn/gnu/autoconf/autoconf-2.69.tar.xz
http://mirror.hust.edu.cn/gnu/automake/automake-1.14.tar.xz
.*ftp.*\.(?:gz|xz)
.*ftp.*/([^/]+(?:.xz|.gz))$ # 后面的分组就是文件名 (?<=.*ftp.*/)[^/]*\.(?:gz|xz) # 断言文件名前一定还有ftp,python的re模块不支持断言中 使用*、+或{3,4}等不固定长度的匹配
匹配邮箱地址
test@hot-mail.com
v-ip@asjin.com
web.manager@asjin.com.cn
super.user@google.com
a@w-a-com
- 确定规则,邮箱名为字母、数字、下划线、减号、点号,域名一样
\w[\w.-]*@([\w-]+\.)+\w+
@前至少一个字符开头,可以有字母、数字、下划线、减号、点号
@后至少有一个字符后跟点,重复至少1次,最后以1个字符串结尾
匹配html标记
- 提取href中的链接url,提取文字“逝水锦遥”
<a href='http://www.asjin.com/index.html' target='_blank'>逝水锦遥</a>
- 有的时候可以这样写
<a href=http://www.asjin.com/index.html target='_blank'>
逝水锦遥</a>
属性attr不标准的写法不写单双引号,这个一般交给浏览器可以正常处理。
<a href='http://www.asjin.com/index.html' target='_blank'>逝水锦遥</a><a>逝水 锦遥1</a>
>(\w+)<
<a.*?>[^<>]*
a标签要到第一个右尖括号结束,不能贪婪。内容是其之后第一个左尖括号之前
匹配URL
http://www.asjin.com/index.html
https://login.asjin.com
file:///ect/sysconfig/network
(\w+)://(\S*)
匹配二代中国身份证ID
321105700101003
321105197001010030
11210020170101054X
17位数字+1位校验码组成 前6位地址码,8位出生年月,3位数字,1位校验位(0-9或X)
身份证验证 身份证验证需要使用公式计算,最严格的应该实名验证。 \d{17}[0-9xX]|\d{15}
python正则使用
- Python使用re模块提供了正则表达式处理的能力.
常用
常量 | 说明 |
---|---|
re.M re.MULTILINE |
多行模式 |
re.I re.IGNORECASE |
忽略大小写 |
re.S re.DOTALL |
单行模式 |
re.X re.VERBOSE |
忽略表达式中的空白字符 |
import re
re.M | re.S # 多行匹配,单行匹配共存
regex = re.compile('\d') # 编译正则表达式
方法、编译
re.compile(pattern, flags=0)
设定flags,编译模式,返回正则表达式对象regex。
pattern就是正则表达式字符串,flags是选项。正则表达式需要被编译,为了提高效率,这些编译后的 结果被保存,下次使用同样的pattern的时候,就不需要再次编译。
re的其它方法为了提高效率都调用了编译方法,就是为了提速。
单次匹配
re.match(pattern, string, flags=0)
regex.match(string[, pos[, endpos]])
- match匹配从字符串的开头匹配,regex对象match方法可以重设定开始位置和结束位置。返回match对象(默认必须从0处匹配上,或指定位置开始匹配)
re.search(pattern, string, flags=0)
regex.search(string[, pos[, endpos]])
- 从头搜索直到第一个匹配,regex对象search方法可以重设定开始位置和结束位置,返回match对象(search 在字符串中搜索,从0或者指定位置开始向后搜索)
re.fullmatch(pattern, string, flags=0)
regex.fullmatch(string[, pos[, endpos]])
- 整个字符串和正则表达式匹配
import re
s = '''bottle\nbag\nbig\napple'''
for i, c in enumerate(s ,1):
print((i-1,c), end='\n' if i % 5 == 0 else '\t')
print()
m = re.match('^a',s, re.M) # mach 只匹配单次
print(type(m),m)
# 打印结果 <class 'NoneType'> None
regex = re.compile('^a',re.M) # compile 编译正则表达式,可以重复使用
m = regex.match(s,15,16)
print(type(m),m)
# 打印结果 <class 'NoneType'> None
regex = re.compile('^b',re.M) # compile 编译正则表达式,可以重复使用
m = regex.match(s,1)
print(type(m),m)
# 因此,match 从头开始匹配,如果匹配不到,返回None,如果匹配到了,返回一个match对象,match对象有group方法,可以打印出匹配到的内容
# 因此 search 在字符串中搜索,从0或者指定位置开始向后搜索
# 因此 re.fullmatch 要求全长匹配,一个不落(如果指定子串,子串要全匹配)
m = re.search('^a',s) # search 匹配多次
print(type(m),m)
regex = re.compile('^b',re.M)
m = regex.search(s,1)
print(type(m),m)
m = re.fullmatch('b.*', s , re.M | re.S )
print(type(m),m)
全文搜索
re.findall(pattern, string, flags=0)
regex.findall(string[, pos[, endpos]])
对整个字符串,从左至右匹配,返回所有匹配项的列表(findall 在文本中,全文搜索、匹配多次、返回列表,元素是字符串)re.finditer(pattern, string, flags=0)
regex.finditer(string[, pos[, endpos]])
对整个字符串,从左至右匹配,返回所有匹配项,返回迭代器。 注意每次迭代返回的是match对象。(finditer 全文搜索、但是返回迭代器,元素是Match对象)
import re
s = '''bottle\nbag\nbig\nable'''
for i,c in enumerate(s, 1):
print((i-1, c), end='\n' if i%10==0 else ' ')
print()
(0, 'b') (1, 'o') (2, 't') (3, 't') (4, 'l') (5, 'e') (6, '\n') (7, 'b') (8,
'a') (9, 'g')
(10, '\n') (11, 'b') (12, 'i') (13, 'g') (14, '\n') (15, 'a') (16, 'b') (17,
'l') (18, 'e')
# findall方法
result = re.findall('b', s) print(1, result)
regex = re.compile('^b')
result = regex.findall(s) print(2, result)
regex = re.compile('^b', re.M) result = regex.findall(s, 7) print(3, result) # bag big
regex = re.compile('^b', re.S) result = regex.findall(s) print(4, result) # bottle
regex = re.compile('^b', re.M) result = regex.findall(s, 7, 10) print(5, result) # bag
# finditer方法
regex = re.compile('^b\w+', re.M)
result = regex.finditer(s)
print(type(result))
r = next(result)
print(type(r), r) # Match对象
print(r.start(), r.end(), s[r.start():r.end()]) r = next(result)
print(type(r), r)
print(r.start(), r.end(), s[r.start():r.end()])
匹配替换
re.sub(pattern, replacement, string, count=0, flags=0)
regex.sub(replacement, string, count=0)
使用pattern对字符串string进行匹配,对匹配项使用repl替换。
replacement可以是string、bytes、function
re.subn(pattern, replacement, string, count=0, flags=0)
regex.subn(replacement, string, count=0)
import re
s = '''bottle\nbag\nbig\napple'''
for i,c in enumerate(s, 1):
print((i-1, c), end='\n' if i%8==0 else ' ')
print()
(0, 'b') (1, 'o') (2, 't') (3, 't') (4, 'l') (5, 'e') (6, '\n') (7, 'b') (8,
'a') (9, 'g')
(10, '\n')(11, 'b')(12, 'i')(13, 'g')(14, '\n')(15, 'a')(16, 'p')(17, 'p')
(18, 'l')(19, 'e')
# 替换方法
regex = re.compile('b\wg')
result = regex.sub('asjin', s)
print(1, result) # 被替换后的字符串
result = regex.sub('asjin', s, 1) # 替换1次 print(2, result) # 被替换后的字符串
regex = re.compile('\s+')
result = regex.subn('\t', s)
print(3, result) # 被替换后的字符串及替换次数的元组
分组
使用小括号的pattern捕获的数据被放到了组group中。 match、search函数可以返回match对象;findall返回字符串列表;finditer返回一个个match对象
- 如果pattern中使用了分组,如果有匹配的结果,会在match对象中
- 使用group(N)方式返回对应分组,1到N是对应的分组,0返回整个匹配的字符串,N不写缺省为0 2. 如果使用了命名分组,可以使用group(‘name’)的方式取分组
- 也可以使用groups()返回所有组
- 使用groupdict() 返回所有命名的分组
import re
s = '''bottle\nbag\nbig\napple'''
for i,c in enumerate(s, 1):
print((i-1, c), end='\n' if i%10==0 else ' ')
print()
# 分组
regex = re.compile('(b\w+)')
result = regex.match(s) # 从头匹配一次
print(type(result))
print(1, 'match', result.group(), result.group(0), result[0], result.groups())
result = regex.search(s, 1) # 从指定位置向后匹配一次 print(2, 'search', result.groups()) #
# 命名分组
regex = re.compile('(b\w+)\n(?P<name2>b\w+)\n(?P<name3>b\w+)') result = regex.match(s)
print(3, 'match', result)
print(4, result.group(3), result.group(2), result.group(1)) print(5, result.group(0).encode()) # 0 返回整个匹配字符串,即match
print(6, result.group('name2'), result.group('name3'))
print(6, result.groups())
print(7, result.groupdict())
result = regex.findall(s) # 返回什么,有几项? for x in result: # 有分组里面放的东西不一样
print(type(x), x)
regex = re.compile('(?P<head>b\w+)')
result = regex.finditer(s)
for x in result:
print(type(x), x, x.group(), x.group('head'), x['head'], x[0])
如果有分组,findall返回的是分组的内容,而不是match匹配的字符串。
有没有分组,都可以使用Match对象的group(0),它总是为匹配的字符串。
分割字符串
字符串的分割函数split,太难用,不能指定多个字符进行分割。
re.split(pattern, string, maxsplit=0, flags=0)
re.split
分割字符串
import re
s = """
os.path.abspath(path)
normpath(join(os.getcwd(), path)).
"""
# 把每行单词提取出来
print(s.split()) # 做不到['os.path.abspath(path)', 'normpath(join(os.getcwd(),', 'path)).']
print(re.split('[\.()\s,]+', s))