前言时刻
又是一星期没写总结,家里有事,嗯……。没事,现在开始学,也是可以的,觉得最晚的是时候,恰好是最好的开始。
来来总结一波,今天学习的是最重要的re正则。 包括:字符组、元字符、量词。 贪婪匹配和惰性匹配 findall和search用法
1、正则表达式
正则(regex)是一种匹配字符串的语法,就像是 xpath 一样。注意:正则表达式不是哪种语言特有的,而是编程语言为了支持正则而开发相应的模块或库。比如正则表达式对应的 Python 库就是 re ,同样的Java、js等都是支持正则表达式的。
接下来,就先写正则表达式的语法,然后再写 Python 的 re 模块,re 支持正则表达式中的所有语法。所以学好正则表达式,以后换哪个编程语言都可。
推荐一个在线练习正则表达式的网站,很好用。https://tool.chinaz.com/regex
1. 字符组
字符组:指的是在括号[]
中的字符,一个括号只匹配一个字符。字符串:xiyuangongzi666
[abcd]
:匹配abcd中任一字符,结果:a[0-9]
:表示匹配任意 Unicode 十进制数。相当于是[0123456789][a-z]
:表示匹配 a 到 z 中的任意一位字母。[A-Z]
:表示匹配 A 到 Z 中的任意一位字母。- [0-9a-zA-Z]:表示匹配任意一位数字、字母。
- 注意:[]中的字符均不含转义功能
import re
st = "xiyuangongzi666"
re.findall("[a-z][0-9]", st) # ['i6']
re.findall("[azi][569]", st) # ['i6']
re.findall("[a-z]\d", st) # ['i6']
re.findall("[a-z][\d]", st) # ['i6']
2. 元字符
如果你觉得用上面的字符组匹配有些麻烦了,那么接下来介绍的将更佳简洁
\d
相当于是[0-9],表示匹配任意 Unicode 十进制数。\D
相当于是[^0-9]
,对 \d 取非,匹配除了十进制数以外的字符。\w
相当于是[0-9a-zA-Z_],匹配数字、字母、下划线。\W
相当于是[^0-9a-zA-Z_]
,匹配除了数字、字母、下划线以外的字符。- 空白字符:空格、tab键(\t)、回车(\n),可统一使用
\s
表示。 \S
:是匹配除了空白字符以外的字符。.
是匹配除了换行符之外的任意一个字符[^]
:表示 非 功能,例如:[^\d]
匹配所有非数字字符^
:匹配一个字符串的开头$
:匹配一个字符串的结尾|
或的意思,如ab|cd,表示ab或者cd()
表示分组,可以限定区域以及获取分组结果。
例子:
# 2、非^和或|
re.findall("[^0-9]\d", st) # ['i6']
re.findall("[a-z]\d|\d\d", st) # ['i6', '66']
# 3、简洁版
re.findall("\w\d", st) # ['i6', '66']
# ^和$
re.findall("^[a-z]\d$", st) # []
re.findall("^[a-z][a-z]", st) # ['xi']
re.findall("\d\d$", st) # ['66']
# .
st2 = """我喜欢1python
学习吧~
"""
re.findall("python.学习", st2) # []
re.findall("python.学习", st2, re.DOTALL) # ['python\n学习']
# re.DOTALL 让 . 可以匹配任何的字符,包括换行符
3. 量词
{n}
表示对前面的正则式匹配 n 个。{n,}
表示对前面的正则式至少匹配 n 个。{n, m}
表示对前面的正则式至少匹配 n 个,最多匹配 m 个。?
表示对前面的正则式匹配 0 个或 1 个。+
表示对前面的正则式匹配 1 个或 无穷 个。*
表示对前面的正则式匹配 0 个或 无穷 个
# 量词
import re
st = "xiyuangongzi666"
re.findall("[a-z]{2,4}[0-9]", st) # ['ngzi6'] 贪婪模式
re.findall("[a-z]{2,}[0-9]", st) # ['xiyuangongzi6']
re.findall("\d?\d{3}", st) # ['666']
re.findall("[a-z]{2,}[0-9]", st) # ['xiyuangongzi6']
re.findall("[a-z]+[0-9]", st) # ['xiyuangongzi6']
re.findall("6*[a-z]*[0-9]", st) # ['xiyuangongzi6', '66']
2、re正则
查找:
2.1 re.findall
re.findall(pattern, string, flags=0)
:返回所有匹配的结果组成的列表。
- pattern:正则表达式
- flags:标志位,用于控制正则表达式的匹配模式,比如:忽略大小写,等
st = "xiyuangongzi666xiyuangongzi666"
# 1.findall
re.findall('[a-z]\d', st, flags=2) # ['i6', 'i6']
re.findall('xi(.+?)gong(.+?)\d', st) # [('yuan', 'zi'), ('yuan', 'zi')]
1)如果你想用前一个分组匹配的 结果 匹配后面字符,可以用(?P=name)
或者(\index)
:
st2 = "gongzigongzi"
re.findall(r'gong(.+?)gong(\1)', st2) # [('zi', 'zi')]
re.findall(r'gong(?P<name>.+?)gong(?P=name)', st2) # ['zi']
# 解释:请看文章最底部的,其他
2.2 re.search
re.search(pattern, string, flags=0)
:只匹配字符串中的第一个结果,返回匹配到的内容的位置和内容等,取值需要使用 group ,用法啥的和 findall 一样。
import re
st = "xiyuangongzi666xiyuangongzi666"
# 2. re.search
res = re.search('[a-z]\d', st) # <re.Match object; span=(11, 13), match='i6'>
res.group() # i6
res = re.search('xi(.+?)gong(.+?)\d', st) # <re.Match object; span=(0, 13), match='xiyuangongzi6'>
res.group(0) # xiyuangongzi6
res.group(1) # yuan
res.group(2) # zi
注意:.group(index)
中,若无 index 默认取0,表示取全部匹配到的结果,index 取的是第 index 个分组的匹配结果。
2.3 re.match
re.match(pattern, string, flags=0)
:match 函数和 search 的用法几乎一样,唯一不同在于 match 只匹配字符串开头部分,若匹配到会返回结果,若无则返回 None 。
import re
# 3、re.match 函数
st = "xiyuangongzi666xiyuangongzi666"
res = re.match('xi[a-z]', st) # <re.Match object; span=(0, 3), match='xiy'>
res.group() # xiy
re.match('zi\d', st) # None
re.search('^zi\d', st) # None
# 在search中正则表达式中加上 ^ 就相当于match函数。
问题1?
当我们使用 findall 时,返回的是匹配的所有结果组成的列表。如果老板让你匹配了几百万个数据,你的内存会瞬间爆满。那该怎么办?
还记得之前学习的迭代器或生成器,是典型的时间换空间,每次仅仅返回其中的一个数据,这样就可节省大量内存。
那么请出接下来的re.finditer
,返回所有匹配到的结果的迭代器。
2.4 re.finditer
用法和 findall 一样,re.finditer
强大之处在于它返回一个迭代器,节省空间,性能好。
# 4、re.finditer
st = "xiyuangongzi666xiyuangongzi666"
res = re.finditer('xi(.*?)gong', st)
print(res) # <callable_iterator object at 0x7fa0e636a3d0> # 迭代器
for each in res:
print(each.group())
# xiyuangong
# xiyuangong
问题2?
老板又给你布置了个爬虫任务,对 20 万个豆瓣电影网页,进行数据分析,需要提取出里面的标题、内容、标签等?
分析:每个网页的结构都一样,仅需要编写出一个正则表达式,就可对每个网页提取出目标数据。如果你采用 re.seach
方法, 每提取一个网页都要编译一次正则表达式,然而每个网页的正则表达式都是一样的,要是只可以编译一次就好了?
那你需要re.compile
,预先编译正则表达式,后续调用不在重复编译,妥妥的效率,详细用法看下面。
2.5 re.compile
re.compile(pattern, flags=0)
:将一个正则表达式编译成一个正则表达式对象,该对象可调用 search、findall、match、sub、split等方法。
# 5、re.compile
import re
st1 = "likepython"
st2 = "likepython666"
pattern = re.compile(r'like(.*)\d*')
pattern.search(st1).group(1) # python
pattern.search(st2).group(1) # python666
pattern.findall(st2) # ['python666']
pattern.match(st2).group(1) # python666
替换和分割
问题3?
今天老板让你写个脚本,替换一个 text 里面的广告文字,这些广告文字有个特点,就是内容随机不固定,你的解决办法是?
分析:首先排除直接使用文本编辑软件 一键替换 功能,因为广告内容不固定。或者使用 python 中的 replace 替换,也行不通,原因如上。
解决:使用正则(re.sub)匹配广告文字并替换,so 酸爽。具体请看下方
2.6 re.sub()
re.sub(pattern, repl, string, count=0, flags=0)
,用正则表达式匹配字符串并替换,强大而实用。
- pattern: 正则表达式,用于匹配要替换的字符串
- repl:要替换成的内容
- string:要处理的字符串
- count:要替换多少个
# 6、re.sub函数
text = """
我喜欢Python
人生苦短,我广告1用Python
我用广2告Python,因为3广告人生苦广4告短
"""
re.sub('\d*广\d*告\d*', 'hi', text, count=6)
# '\n我喜欢Python\n人生苦短,我hi用Python\n我用hiPython,因为hi人生苦hi短\n'
—-2021-7-14更新—-
1)使用 re 正则在指定位置插入字符串:
这个功能超级强大,请看例子。
import re
st = "我喜欢Python666"
res = re.sub('(喜欢)', r'真\1', st)
print(res) # 我真喜欢Python666
res2 = re.sub(r'(喜欢)\w{3,6}', r'\1*', st)
print(res2) # 我喜欢*666
解释:上方的(喜欢),是添加了一个分组,后面的 \1 表示的是,前面第一个参数匹配到的内容中第一个分组内容。所以就是把喜欢换成了真喜欢。
2.7 re.split()
re.split(pattern, string, maxsplit=0, flags=0)
,对正则表达式匹配到的结果进行分割,返回列表。
- pattern: 正则表达式,用于匹配要分割的字符串
- string:要处理的字符串
- maxsplit:最大要分割的次数。
- flags:
# 7、re.split
st = "我用广2告Python,因为3广告人生苦广4告短"
re.split('\d*广\d*告\d*', st) # ['我用', 'Python,因为', '人生苦', '短']
re.split('(\d*广\d*告\d*)', st) # ['我用', '广2告', 'Python,因为', '3广告', '人生苦', '广4告', '短']
# 使用分组的话,被分割的字符串也会显示在列表中,可用于查看。
flag
- re.I == re.IGNORECASE:忽略大小写匹配,比如 [a-z] 匹配的是小写字母,加上 re.I 之后,大写小写均匹配。
- re.A == re.ASCII:让
\w
,\W
,\b
,\B
,\d
,\D
,\s
和\S
只匹配ASCII,而不是Unicode。 - re.S == re.DOTALL:让 . 可以匹配到所有字符,包括换行符。
- re.M == re.MUTILINE:多行模式,使用 ^ 和 $ 时候,会匹配每一行的开头或者结尾,而不是之前的只匹配字符串的开头和结尾。
re.X == re.VERBOSE:允许在正则表达式中添加注释(#+注释),可在每一行后面添加注释。
a = re.compile(r"""\d + # 匹配整数
\. # 匹配小数点
\d * # 匹配至少 0 个数字""", re.X)
b = re.compile(r"\d+\.\d*")
其他不常用用法
(?#…)
:注释功能,#后面的内容会被忽略\number
:匹配数字代表的组合。(?P=name)
:反向引用一个命名组合的内容,可用于后续匹配st2 = "gongzigongzi"
re.findall(r'gong(.+?)gong(\1)', st2) # [('zi', 'zi')]
re.findall(r'gong(?P<name>.+?)gong(?P=name)', st2) # ['zi']
(?!…)
:匹配…
不符合的情况。这个叫 negative lookahead assertion (前视取反)。比如说,Isaac (?!Asimov)
只有后面 不 是'Asimov'
的时候才匹配'Isaac '
。相当是 if 判断(?!…)
:匹配…
不符合的情况。这个叫 lookahead assertion (前视取反)。比如说,Isaac (?=Asimov)
只有后面 是'Asimov'
的时候才匹配'Isaac '
。相当是 if 判断(?:…)
:取消该分组。
总结:
今天学习的 re 正则,非常的重要,建议重点掌握,多多练习。多看多练多总结。
参考文章: https://docs.python.org/zh-cn/3.8/library/re.html https://www.runoob.com/python/python-reg-expressions.html