python常用模块之re模块
引言:
re模块是python内置模块中重要模块之一,有必要单独做一次学习总结,以下是个人对re模块的使用与理解。
参考:
1. 正则表达式规则(摘自菜鸟教程)
| 模式 | 描述 |
|---|---|
| ^ | 匹配字符串的开头 |
| $ | 匹配字符串的末尾。 |
| . | 匹配任意字符,除了换行符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符。 |
| [...] | 用来表示一组字符,单独列出:[amk] 匹配 'a','m'或'k' |
| [^...] | 不在[]中的字符:[^abc] 匹配除了a,b,c之外的字符。 |
| re* | 匹配0个或多个的表达式。 |
| re+ | 匹配1个或多个的表达式。 |
| re? | 匹配0个或1个由前面的正则表达式定义的片段,非贪婪方式 |
| re{ n} | 匹配n个前面表达式。例如,"o{2}"不能匹配"Bob"中的"o",但是能匹配"food"中的两个o。 |
| re{ n,} | 精确匹配n个前面表达式。例如,"o{2,}"不能匹配"Bob"中的"o",但能匹配"foooood"中的所有o。"o{1,}"等价于"o+"。"o{0,}"则等价于"o*"。 |
| re{ n, m} | 匹配 n 到 m 次由前面的正则表达式定义的片段,贪婪方式 |
| a| b | 匹配a或b |
| (re) | 匹配括号内的表达式,也表示一个组 |
| (?imx) | 正则表达式包含三种可选标志:i, m, 或 x 。只影响括号中的区域。 |
| (?-imx) | 正则表达式关闭 i, m, 或 x 可选标志。只影响括号中的区域。 |
| (?: re) | 类似 (...), 但是不表示一个组 |
| (?imx: re) | 在括号中使用i, m, 或 x 可选标志 |
| (?-imx: re) | 在括号中不使用i, m, 或 x 可选标志 |
| (?#...) | 注释. |
| (?= re) | 前向肯定界定符。如果所含正则表达式,以 ... 表示,在当前位置成功匹配时成功,否则失败。但一旦所含表达式已经尝试,匹配引擎根本没有提高;模式的剩余部分还要尝试界定符的右边。 |
| (?! re) | 前向否定界定符。与肯定界定符相反;当所含表达式不能在字符串当前位置匹配时成功。 |
| (?> re) | 匹配的独立模式,省去回溯。 |
| \w | 匹配数字字母下划线 |
| \W | 匹配非数字字母下划线 |
| \s | 匹配任意空白字符,等价于 [\t\n\r\f]。 |
| \S | 匹配任意非空字符 |
| \d | 匹配任意数字,等价于 [0-9]。 |
| \D | 匹配任意非数字 |
| \A | 匹配字符串开始 |
| \Z | 匹配字符串结束,如果是存在换行,只匹配到换行前的结束字符串。 |
| \z | 匹配字符串结束 |
| \G | 匹配最后匹配完成的位置。 |
| \b | 匹配一个单词边界,也就是指单词和空格间的位置。例如, 'er\b' 可以匹配"never" 中的 'er',但不能匹配 "verb" 中的 'er'。 |
| \B | 匹配非单词边界。'er\B' 能匹配 "verb" 中的 'er',但不能匹配 "never" 中的 'er'。 |
| \n, \t | 匹配一个换行符,匹配一个制表符 |
| \1...\9 | 匹配第n个分组的内容。 |
| \10 | 匹配第n个分组的内容,如果它经匹配。否则指的是八进制字符码的表达式。 |

2. re模块常用方法
正则表达式修饰符
| re.I | IGNORECASE,使匹配对大小写不敏感 |
| re.L | LOCALE,做本地化识别(locale-aware)匹配 |
| re.M | MULTILINE,多行匹配,影响 ^ 和 $ |
| re.S | DOTALL,使 . 匹配包括换行在内的所有字符 |
| re.U | UNICODE,根据Unicode字符集解析字符。这个标志影响 \w, \W, \b, \B. |
| re.X | VERBOSE,该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。 |
re.findall(pattern, string, flags=0) pattern: 正则表达式 string:要匹配的字符串 flag: 正则表达式修饰符, 返回匹配的所有子串的列表,有分组则返回分组后的列表
In [1]: import re In [2]: s = '123 abc 456 efg ijk' In [3]: pattern = r'\d+' In [4]: re.findall(pattern, s) # 返回所有匹配的子串列表 Out[4]: ['123', '456'] In [5]: s = '{"1": 1, "2": 2, "3": 3}' In [6]: re.findall(r'"(\d)"', s) # 只返回匹配的分组后的字符串列表 Out[6]: ['1', '2', '3']
re.search(pattern, string, flags=0) 扫描整个字符串,找到第一个匹配后返回一个match对象
In [1]: import re In [2]: s = '123abc456def' In [3]: pattern = '[a-z]+' In [4]: re.search(pattern, s) Out[4]: <_sre.SRE_Match object; span=(3, 6), match='abc'> In [5]: m = re.search(pattern, s) In [6]: m.groups() # 没有分组,返回空元祖 Out[6]: () In [7]: m.group() # 返回第一个匹配字符串 Out[7]: 'abc'
re.match(pattern, string, flags=0) 从字符串的起始位置开始匹配,如果字符串不符合正则表达式,则匹配失败,函数返回None, 否则返回一个匹配对象match
In [1]: import re In [2]: s = '13512341234' In [3]: re.match('\d+', s) Out[3]: <_sre.SRE_Match object; span=(0, 11), match='13512341234'> In [4]: re.match('\d{5}', s) # 匹配成功后则不继续往后匹配, 返回匹配对象 Out[4]: <_sre.SRE_Match object; span=(0, 5), match='13512'> In [5]: re.match('\D+', s) # 匹配不成功, 返回空对象
re.split(pattern, string, maxsplit=0, flags=0) pattern: 正则表达式 string: 匹配字符串 maxsplit: 最大分割次数 默认不限制次数 flags: 正则表达式修饰符, 返回按匹配字符串分割后的字符串列表
>>> re.split(r'[a-d]+', '123a456B789c000', flags=re.I) # 忽略大小写的按照a-d分割 ['123', '456', '789', '000'] >>> re.split(r'\W+', 'hello, world, everyone') # 按照非数字字母下划线分割 ['hello', 'world', 'everyone'] >>> re.split(r'\W+', 'hello, world, everyone', 1) # 最大分割一次 ['hello', 'world, everyone'] >>> re.split(r'(\W+)', 'hello, world, everyone') # 有分组,则把分组的对象也一并返回给列表 ['hello', ', ', 'world', ', ', 'everyone']
re.sub(pattern, repl, string, count=0, flags=0) pattern: 正则表达式 repl: 要替换的字符串或函数, string: 要匹配的字符串, count: 替换的次数, flags: 正则表达式修饰符, 返回替换后的新字符串
>>> re.sub(r'{(.*?)}', r'[\1]', r'{1, 2, 3, 4}') # 将大括号替换为中括号 '[1, 2, 3, 4]' >>> re.sub(r'(www)', 'https://\1', 'www.baidu.com') # 分组后前要加r'' 'https://\x01.baidu.com' >>> re.sub(r'(www)', r'https://\1', 'www.baidu.com') # 添加https://前缀 'https://www.baidu.com'
要替换的为函数时:
def add_http(match_obj): return 'http://' + match_obj.group() string = "The website is www.baidu.com!!!" print(re.sub(r'(www\.)\w+', add_http, string)) out: The website is http://www.baidu.com!!!
re.subn(pattern, repl, string, count=0, flags=0): 参数同sub, 但是返回的是一个元组(替换后的字符串, 替换次数)
>>> re.subn('\d', '***', 'abc4abc4abc1abc') ('abc***abc***abc***abc', 3)
re.compile(pattern, flags=0) pattern: 正则表达式, 将正则表达式编译成一个正则表达式对象Pattern, 当正则表达式很长且要反复使用时用它
>>> pattern = re.compile('\d{2}') >>> pattern.search('abc243') <_sre.SRE_Match object at 0x7fe34ecb7718> >>> p = pattern.search('abc243') # 用法与上述类似 >>> p.group() '24'
re.finditer(pattern, string, flags=0) 同findall类似, 只不过返回匹配字符串的的迭代器,元素是每一个匹配对象match
>>> iterator = re.finditer(r'\d+', 'a123b123c45d67') >>> for i in iterator: ... print(i.group()) ... 123 123 45 67
3. re模块使用
1. re.findall()函数默认分组优先
>>> re.findall('www.(baidu|google).com', 'www.google.com') 只能取到分组内容 ['google'] >>> re.findall('www.(?:baidu|google).com', 'www.google.com') # 取消分组 ['www.google.com']
2. re.split()分组的特殊情况,在上面的split函数中已经指出,这里再重申一遍, 加入分组后会把分组内容也返回到列表中
>>> re.split(r'[a-d]', '1a2b3c4d5a6b7') ['1', '2', '3', '4', '5', '6', '7'] >>> re.split(r'([a-d])', '1a2b3c4d5a6b7') ['1', 'a', '2', 'b', '3', 'c', '4', 'd', '5', 'a', '6', 'b', '7']
3. 正则使用的一些细节
1)‘-’连接字符要在字符组元字符中当做普通字符处理要放在开头或末尾,转义字符'\'也要加上反斜杠才能当做普通字符, 其他的特殊字符.*?在里面都只是普通字符
>>> re.findall('[-.?*1-9]', 'a1.?*-9') ['1', '.', '?', '*', '-', '9'] >>> re.findall(r'[-.*?1-9\\]', '-.*?\\') ['-', '.', '*', '?', '\\']
2) 正则表达式并不是写的越复杂越好,越详细越好,而是需要在复杂和完整性求得平衡,而这都取决于带搜索的文本,能满足自己的要求即可。
4. re模块的环视功能
# 环视应用例子, 把match修改为matched s = 'A backreference match whatever text was match by the earlier group named name.' # 方法一, 直接修改效率高,最简单, 正则表达式'占用' 'match' print(re.sub(r'\bmatch\b', 'matched', s)) # 方法二, 使用分组保留, 没有好处,增加了复杂程度 print(re.sub(r'\b(match)\b', r'\1ed', s)) # 使用顺序环视, 正则表达式并未'占用' '\bmatch' print(re.sub(r'(?<=\bmatch)\b', r'ed', s)) # 同时使用顺序环视和逆序环视 print(re.sub(r'(?<=\bmatch)(?=\b)', r'ed', s)) # 顺序环视和逆序环视颠倒同上面一样,环视并不占用字符只代表位置,此处相当于在这个位置插入了'ed' print(re.sub(r'(?=\b)(?<=\bmatch)', r'ed', s)) # out: A backreference matched whatever text was matched by the earlier group named name.
5. re匹配规律, 以下例子均来自精通正则表达式第四章
import re
import time
# 测试正则引擎
s = "nfa not"
pattern = r'nfa|nfa not'
print(re.findall(pattern, s)) # 匹配了一项则不继续往下匹配了
pattern = r'nfa not|nfa'
print(re.findall(pattern, s))
pattern = r'X(.+)+X' # 这个正则匹配花了很长时间
s = '=XX=========================='
start = time.time()
res = re.findall(pattern, s)
print('%.2f s' % (time.time() - start))
print(res)
['nfa'] ['nfa not'] 25.64 s []
匹配原则1: 优先选择最左端的(最靠开头)的匹配结果。
s = 'The dragging belly indicates your cat is too fat' # 会按字符串从左到右的顺序寻找匹配的结果, 而不会按照正则表达式写的顺序寻找 print(re.findall(r'fat|cat|belly|your', s)) # out: ['belly', 'cat', 'your', 'cat', 'fat']
匹配原则2: 标准的匹配量词(. * ? {m, n})是匹配优先(greedy)的。
s = 'abc{hello}bcd{world}' print(re.findall(r'\{.*\}', s)) # 默认是贪婪模式的 print(re.findall(r'\{.*?\}', s)) # 关闭贪婪模式 # out: ['{hello}bcd{world}'] ['{hello}', '{world}']
6. 匹配效率问题
1: 要提取双引号里面的内容, 还要能够提取转义字符
1) 版本1和2
s = r'"very much \" \2 hello \" "' print(re.findall(r'"((?:\\.|[^\\"])*)"', s)) # 多选结构交换位置能够减少回溯次数,增加效率 # 这是因为一般来说引号内转移字符少, 所以要把普通字符写在前面
# 只有匹配成功才能提高速度, 所以把尽量正确的放在多选分支前可以提升效率
print(re.findall(r'"((?:[^\\"]|\\.)*)"', s)) # out # ['very much \\" \\2 hello \\" ']
2)版本3
# 上述可以继续优化 # 上面的*号每匹配一个新的字符都要迭代进入多选分支结构选择退出,这无疑会增加额外的开销, # 可以在匹配连续普通字符时, 可以一次性读入,也就是加上'+'号可以极大地减小*号迭代的次数 print(re.findall(r'"((?:[^\\"]+|\\.)*)"', s))
2:正则回溯理解
下面要匹配这行话'''The name "McDonald's" is said "makudonarudo" in Japannese'''
1) 简单用'(".*")'匹配,匹配过程如下图所示,正则引擎从T开始一个一个字符匹配,直到A处的引号才匹配成功,然后开始尝试匹配正则表达式剩余部分,一直到行末.号匹配不成功,开始回溯,在字符串末尾尝试匹配",但显然不行,然后一步步往后回溯,直到回溯到...rudo(C)处,开始尝试匹配" 成功则返回结果。

2) 用'(".*"!)'匹配上述字符串,显然不能匹配成功,匹配失败过程如下所示,匹配的过程类似上面成功的样例, 当匹配到末尾时,开始回溯,到C处显然不能匹配"!,然后继续回溯,一直到A处仍然不能匹配,然后正则引擎开始从A处后面M开始重新匹配字符串,如此往复直到Y处才宣告匹配失败。

3) 用'("[^"]*"!)'匹配上述字符串,显然也不能匹配成功,但是回溯次数远少于上面的次数,这也就达到了优化的目的。

3. 多选结构可以说是回溯的主要原因,提升效率的重要方法就是减少正则表达式的多选结构。例如能用字符组[abcd]代替的就不要用多选结构(a|b|c|d)。
4. re练习
1. 验证是否是手机号
# 验证手机号是否正确, 手机号是以13,14,15,17,18,19开头并且是11位数字 def is_phone(phone): regular = r'^1[345789]\d{9}$' return bool(re.match(regular, phone))
2. 简单验证身份证号码
# 身份证号开头不能为0,由15位数字, 或是18位由17位数字加数字或x组成 def is_identify_card(number): # pattern = r'^[1-9](\d{14}|\d{16}[\dx])$' # 不够简洁 pattern = r'^[1-9]\d{13}(\d|\d{3}[\dx])$' return bool(re.match(pattern, number))
3. 为数字字符串添加逗号 '123456789' -> '123,456,789'
# 实现在长整数中每3位插入','分隔符 # 方法一: 用sub的可以添加func参数实现 def add_dot(match_num): """对匹配的数字添加逗号""" # '1234567890' -> '1,234,567,890' string = "" for idx, item in enumerate(match_num.group('num')[::-1]): # type: int, str if idx % 3 == 0 and idx: string = ',' + string string = item + string if string.startswith(','): string = string[1:] return string s = '234567890 456543132 abc ac13423434 4564ab 456123' print(re.sub(r'\b(?P<num>\d+)\b', add_dot, s)) # 方法二, 用环视功能添加逗号,缺点 'ac1344314'也开始分割,逆序环视功能不支持?*+ print(re.sub(r'(?<=\d)(?=(\d\d\d)+\b)', ',', s)) # 方法三, 循环检测是否有需要替换的字符串,当没有替换则退出循环 while 1: # s, n = re.subn(r'(\b\d+?)(?=(\d\d\d)+\b)', r'\1,', s) #此处可以不用环视 s, n = re.subn(r'(\b\d+)(\d\d\d)+\b', r'\1,\2', s) if n == 0: print(s) break # 方法四: 综合方法一方法二 s = '234567890 456543132 abc ac13423434 4564ab 456123' print(re.sub(r'\b(\d+)\b', (lambda match: re.sub(r'(?<=\d)(?=(\d\d\d)+\b)', ',', match.group())), s))
234,567,890 456,543,132 abc ac13423434 4564ab 456,123 234,567,890 456,543,132 abc ac13,423,434 4564ab 456,123 # ac13...也分割了 234,567,890 456,543,132 abc ac13423434 4564ab 456,123 234,567,890 456,543,132 abc ac13423434 4564ab 456,123
4. 提取ip地址
s = 'The ip is from 192.168.1.100 to 192.168.1.254 ' # s = '25.254.254.99' pattern = r'\b(?:\d{1,3}\.){3}\d{1,3}\b' # 还要控制到000-255 print(re.findall(pattern, s)) pattern = r""" (?<![\w.]) # 否定逆向环视, 确保匹配对象的左侧是没有字符和. 解决0.0.0.0.0这种非ip (?:(?:[01]?\d\d?|2[0-4]\d|25[0-5])\.){3} # 控制ip每一个字节是0-255出现 并且xxx.出现3次 (?:[01]?\d\d?|2[0-4]\d|25[0-5]) # 规则同上 0-255 (?![\w.]) # 否定顺序环视, 确保匹配尾部不出现字符和. """ print(re.findall(pattern, s, re.VERBOSE)) # out # ['192.168.1.100', '192.168.1.254']
5. 正则版四则运算
1 # coding=utf-8 2 """ 3 任务: 输入一个四则运算字符串,支持括号,返回结果 4 例如: 1 + 2 - 3 * (6/2 +3) / 2 + ((1+2)*3 -2) 5 思路: 6 1: 检查输入是否有误,例如括号是否配对,是否都是数字,或是缺少操作数 7 2: 检测完毕后去除所有的空格,使处理简便 8 3: 能够挑出所有括号的表达式先计算 r'\((?:[^)]+)*\)' 9 4: 完成不带括号的4则运算逻辑 如1+2*3-3/4*5-2 10 4.1: 先挑选出带乘除的表达式 r'(\d+([*/]\d+)+)', 再计算 11 4.2: 再统一计算剩下的加减运算 12 5: 返回所有计算结果 13 """ 14 import re 15 16 17 def check(exp): 18 """检测字符串输入是否合法""" 19 # 一部分直接放到异常里了,出错了就是输入有误, 如括号的配对 20 # 含有非法字符 21 if re.search(r'[^-+*/.()\d]', exp): 22 return False 23 # 先检测是否有两个运算符连在一块 1++2 1..2 1+.2 24 if re.search(r'[-+*/.][-+*/.]+', exp): 25 return False 26 # 检测()的左边右边是否直接有数 1(3*3)4 或 ()() 和()里面没数 是不合法的 27 if re.search(r'[\d).]\(|\)[\d(.]', exp): 28 return False 29 # 缺失一个操作数也是不合法 1+ 或 12+) (+ 都是不合法的 30 if re.search(r'[-+*/(.]($|\))|\([-+*/.]', exp): 31 return False 32 return True 33 34 35 def strip(exp: str): 36 """去除表达式中的空格""" 37 return exp.replace(' ', '') 38 39 40 def cul_brackets(exp): 41 """先计算所有带括号的表达式""" 42 while 1: 43 # 计算每一个括号 44 exp, n = re.subn(r'\((?P<exp>[^(]+?)\)', lambda match: cul_without_brackets(match.group('exp')), exp) 45 if n == 0: 46 break 47 return exp 48 49 50 def cululate(exp: str): 51 """统一计算接口, 返回浮点数""" 52 # 1. 先去除空格 53 exp = strip(exp) 54 try: 55 if check(exp): 56 return float(cul_without_brackets(cul_brackets(exp))) 57 else: 58 print("输入有误") 59 except Exception as e: 60 print('输入错误') 61 62 63 def cul_without_brackets(exp: str): 64 """计算不带括号的四则运算, 并计算, 返回字符串""" 65 # 1 找到乘除的式子并替换 66 exp = re.sub(r'[\d.]+(?:[*/][\d.]+)+', lambda match: cul_addsub_muldiv(match.group(0)), exp) 67 exp = cul_addsub_muldiv(exp) 68 return exp 69 70 71 def cul_addsub_muldiv(exp: str): 72 """只能计算只包含乘除或加减的式子,例如1+2-3+...或3.5/6*5*... 为了统一返回也是字符串""" 73 result = None 74 val = 0 # 保存新读取到的数 75 is_float = False # 判断是不是浮点数 76 div_10_n = 10 # 浮点数的计算位数 初始是n/10, 然后是n/100... 77 op = None # 保存上一个操作符 78 for idx, c in enumerate(exp): 79 if c not in '+-*/': 80 if c == '.': 81 is_float = True 82 continue 83 if is_float: 84 val += int(c) / div_10_n 85 div_10_n *= 10 86 else: 87 val = int(c) + 10 * val 88 if idx < len(exp) - 1: 89 continue 90 if result is None: 91 op = c 92 result = val 93 val = 0 94 is_float = False 95 div_10_n = 10 96 continue 97 if op == '+': 98 result += val 99 elif op == '-': 100 result -= val 101 elif op == '*': 102 result *= val 103 elif op == '/': # 不用else是还有一种可能, 当只含有一个数的时候就直接返回 104 result /= val 105 val = 0 106 op = c 107 is_float = False 108 div_10_n = 10 109 return str(result) 110 111 112 while 1: 113 # exp = "1 + 2 - 3 * (6.2/2 +3) / 2 + ((1+2)*3 -2)" 114 exp = input('请输入表达式: ') 115 res = cululate(exp) 116 if res: 117 print('eval:', eval(exp)) 118 # print(('%.5f' % cululate(exp))) 119 print(cululate(exp))
请输入表达式: 1+2* 3 -1.5 eval: 5.5 5.5 请输入表达式: 1.4*1.3/1.6 + (1+2) eval: 4.137499999999999 4.137499999999999 请输入表达式: 3+2*14.4 eval: 31.8 31.8 请输入表达式: 1-34+34-134*4 eval: -535 -535.0
6. 爬取猫眼电影top100的电影信息
1 # coding=utf-8 2 """爬取猫眼电影的top100""" 3 4 import time 5 import requests 6 import re 7 import os 8 9 UserAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36" 10 headers = { 11 "User-Agent": UserAgent 12 } 13 14 15 # 要获取排名,详细信息链接url, 电影名, 主演,上映时间, 评分 16 pattern = r'class="board-index.*?>(?P<rank>.*?)</i>.*?href="(?P<url>.*?)".*?title="(?P<title>.*?)".*?'\ 17 r'class="star">(?P<star>.*?)</.*?class="releasetime">(?P<releasetime>.*?)</.*?class="integer">' \ 18 r'(?P<integer>.*?)<.*?class="fraction">(?P<fraction>\d)<' 19 com = re.compile(pattern, re.S) 20 21 22 def get_info(html): 23 ret = com.finditer(html.text) 24 for i in ret: 25 info = { 26 'rank': i.group('rank'), 27 'detail_url': 'http://maoyan.com' + i.group('url'), 28 'title': i.group('title'), 29 'star': i.group('star').strip(), 30 'release_time': i.group('releasetime'), 31 'score': i.group('integer') + i.group('fraction') 32 } 33 print(info) 34 yield info 35 36 37 def save_to_file(): 38 39 filename = '猫眼电影top100.txt' 40 if os.path.exists(filename): 41 os.remove(filename) 42 with open(filename, 'a') as f: 43 for offset in range(0, 100, 10): 44 # 爬取网页 45 url = 'http://maoyan.com/board/4?offset={offset}'.format(offset=offset) 46 html = requests.get(url, headers=headers) 47 for info in get_info(html): 48 f.write(str(info) + '\n') 49 50 time.sleep(3) 51 52 53 # 开始爬取 54 save_to_file()
未完待续...

浙公网安备 33010602011771号