Python中的re模块--正则表达式

Python中的re模块--正则表达式

使用match从字符串开头匹配

以匹配国内手机号为例,通常手机号为11位,以1开头。大概是这样13509094747,(这个号码是我随便写的,请不要拨打),我们通常还能看到其他美观的显示形式。

  • 135-0909-4747
  • 135 0909 4747

前三位由运营商规定,这里我们不考虑。

如何使用正则表达式匹配类似上面的手机号呢?

import re
 
result = re.match('\d\d\d-\d\d\d\d-\d\d\d\d', '135-0909-4747')
print(result)

\d表示匹配一个数字。于是上面的写法可以匹配,但是打印的内容是这样的

# out
<_sre.SRE_Match object; span=(0, 13), match='135-0909-4747'>

没有出现None说明匹配成功了,字符范围[0, 13],十一位的手机号加上两位分隔符-刚好13位。match里显示了匹配成功的字符串。这样的结果并不直观。

使用result.group()即可提取出match里面的内容。并且是str类型,更方便我们处理。

...
print(result.group()) # out: 135-0909-4747

上面的写法还是太臃肿,result = re.match('\d{3}-\d{4}-\d{4}', '135-0909-4747'),这种写法和上面等价。{}里面的次数表示要匹配的次数。当然里面可以填区间,区间是闭区间,包含左右的数字。比如

  • \d{3,} 匹配数字3或者3次以上
  • \d{,9} 匹配数字0次~9次之间
  • \d{2,4}匹配数字2次~4次之间

一定要注意,填入区间的时候,逗号左右都没有空格。

如果一个规则我们经常要用到,可以使用re.compile编译成一个pattern object对象。像这样

import re
 
phone_p = re.compile('\d{3}-\d{4}-\d{4}')
result = re.match(phone_p, '135-0909-4747')
print(result)
# result = phone_p.match('135-0909-4747')
# print(result)

phone_p是一个对象,可以用它直接调用match方法,直接填入要匹配的字符串就好了。就像上面被注释掉的地方一样。也可以使用re.match,不同的是,第一个参数需要填上这个模式对象,第二个参数才是要匹配的字符串。两种方法得到的结果一样,喜欢哪种用哪种。

使用search搜寻字符串中可能存在的匹配

re还有一个serach方法,和match用法极其相似。唯有不同的是,match要求匹配必须从字符串的开头开始,也就是说,如果第一个字符就不匹配,后面即使有和模式匹配的字符串,也被认为是匹配失败。这么说不好理解。举个例子,还是手机号。

import re
 
phone_p = re.compile('\d{3}-\d{4}-\d{4}')
result = re.match(phone_p, 'Bob 135-0909-4747')
print(result)

在手机号前加了机主姓名,我们可以看到,后面还是以前的手机没有变,按理说这个模式应该能提取出手机号,但是打印的却是None,因为使用的是match匹配,模式中要求是3个数字打头,然后给出的字符串以字母开始。第一个字符就挂掉了。所以说match是从字符的开头匹配的。

再看看search呢?

只需将match改成search,输出<_sre.SRE_Match object; span=(4, 17), match='135-0909-4747'>表示匹配成功,字符范围[4:17],不含17。可以看到search搜寻字符串里所有可能的情况,一旦发现有匹配的子字符串就返回。

为了加深理解,再看这样的例子

import re
# 注意多了个^
phone_p = re.compile('^\d{3}-\d{4}-\d{4}')
result = re.search(phone_p, 'Bob 135-0909-4747')
print(result) # None

再模式的最前面加上^表示匹配开始的标志,即必须以^后的内容开头,在这句里的意思就是必须以3个数字开头(而不是1个,\d{3}是一个整体)。可以看到,即使是search方法也不能匹配成功了。因被强制从字符串开头处开始匹配,这句的意思不就和和使用match方法达到同样的效果了吗?

说到^就不得不提$,后者是匹配结束的标志,必须以$前的字符结尾。

import re
 
phone_p = re.compile('^\d{3}-\d{4}-\d{4}$')
# 不小心在开头或者结尾多输入了一位
result = re.search(phone_p, '135-0909-47475') # or 1135-0909-4747
print(result) # None

显然结是4个数字结尾(或不是3个数字开头),返回None。

这句模式限制了必须是11位的数字加分隔符组成。多一位少一位都不行。

还有一个地方要注意,不管是match还是search,即使可能存在多个正确的匹配,它们找到第一个后就立即停止,所有我们得到的永远是第一个成功匹配的字符串。

import re
 
phone_p = re.compile('\d{3}-\d{4}-\d{4}')
result = re.search(phone_p, 'My phone number is 135-0909-4747 and another is 123-4567-8901')
print(result) # 135-0909-4747

找到第一个手机号就不在匹配了,第二个手机号被忽略了。

使用findall找到所有成功的匹配

上面的例子,如何找到所有的手机号呢?用re.findall,它返回所有成功匹配字符串的列表.

import re
 
phone_p = re.compile('\d{3}-\d{4}-\d{4}')
result = re.findall(phone_p, 'My phone number is 135-0909-4747 and another is 123-4567-8901')
print(result)

仅是将search换成findall,会打印['135-0909-4747', '123-4567-8901']可以看到,所有的手机号都被找到了!

在正则表达式中尽量使用原始字符串

由于正则表达式中经常要用到\,而转义字符可能影响到我们的模式表达。

p = re.compile('gg\\d')
p_1 = re.compile('gg\d')
 
print('\d') # \d
print('\\d') # \d

上面的例子,打印结果都一样\d,因为\d没有对应的转义。两种模式的写法也没有区别。

但是有些字符是可以转义的,比如n。

print('\n') # 换行
print('\\n') # \n

上面例子,结果就不一样了。又回到正则表达式中来

p_0 = re.compile('gg\n') # 匹配'gg\n', \n换行
p_1= re.compile('gg\\n') # 匹配'gg\n', \n换行
# 使用了原始字符串
p_2 = re.compile(r'gg\n') # 匹配'gg\n',\n换行
p_3 = re.compile(r'gg\\n') # 匹配'gg\\n', \n字符串

可以看到没有使用原始字符串时候,会让人迷惑,上述前两行,两种匹配模式匹配的都是gg和一个换行符。使用了原始字符串就比较清楚了,待匹配的字符串(就不要再使用原始字符串了),和模式对应起来了,不会混淆,如上述的最后两行代码。

当然打印的时候又会有些不一样

print('gg\\n') # gg\n
print('gg\n') # gg换行
print(r'gg\n') # gg\n
print(r'gg\\n') # gg\\n

打印时,原始字符串完全忽略了\对字符的转义,字符串里是啥样,打印出来就是啥样。

在正则表达式里面的原始字符串(对\还是有一定程度的影响)和打印时候的原始字符串还时有点差别的。

原始字符串在处理文件路径时相当有用。

# 这么写不对,会被转义,结果就是路径错了
filepath = 'F:\nb\person\a.txt'
# 保险一点的做法,用\\将自身转义,表示真正意义上的'\',
filepath = 'F:\\nb\person\\a.txt'
# 使用原始字符串
filepath = r'F:\nb\person\a.txt'

当然了,直接用Linux/OS X的路径方式在Windows上貌似也是可以的。直接远离了转义字符的困扰。

filepath = 'F:/nb/person/a.txt'

也可以运行成功,没问题。

讨论了这么多其实就想说,正则表达式编译模式时,尽可能地使用原始字符串。

高级匹配模式

"[]"匹配集合里面的任意一个字符

import re
 
p = re.compile(r'[朱刘马]帅吃饭了吗')
result = re.match(p, '马帅吃饭了吗') # or 朱帅吃饭了吗 or 刘帅吃饭了吗
print(result)

[]里面的内容表示任意一个字符,只要在这个集合里面的就能匹配成功。所以上面的模式可以匹配

马帅吃饭了吗
朱帅吃饭了吗
刘帅吃饭了吗

这是针对单个字符的,还可以这样写[a-z0-9]代表一个范围。这表示一个字符只要是字母或者数字就能匹配成功,当然后面可以加上{}p = re.compile(r'[0-9]{3}')可以匹配3位数字,其实和\d+{3}异曲同工。

"|"匹配这个或那个字符串

上面的例子还可以这样写。

import re
 
p = re.compile(r'朱|刘|马帅吃饭了吗')
 
result = re.match(p, '马帅吃饭了吗')
print(result)

效果和上面一样。这是单个字符的时候,来看看涉及到特定的多个字符时候。

import re
 
p = re.compile(r'Bob|Jerry|Tom Lee')
 
result = re.match(p, 'Jerry Lee')
print(result)

这能匹配三个人名

Bob Lee
Jerry Lee
Tom Lee

如果使用[]就不好操作了。下面也能匹配上面的三个名字,不过哪个更易懂不言而喻。所以要分场合用最合适的。

p = re.compile(r'[BJT][oe][brm][\sry]{,2} Lee')

还有一点,[]里可以使用^表示“非”的意思。

p = re.compile(r'[^0-9]')这就表示,除开数字的其他任意一个字符。

“?”匹配0次或者1次

import re
 
p = re.compile(r'我有一万?元')
 
result = re.match(p, '我有一元') # or我有一万元
print(result)

“万”字匹配0次(没有)或者1次都是成功的。通俗点讲,这个字符时可选的。其实用?可以看成是p = re.compile(r'我有一万{,1}元')的简写。

“*”匹配任意次, "+"匹配至少1次

*可以匹配0次,也可以匹配多次。实际上可看作p = re.compile(r'我有一万{0,}元')

+匹配至少一次,可以看作p = re.compile(r'我有一万{1,}元').这意味着它不能匹配我有一元,必须含有一个或者多个“万”字。

贪婪匹配和非贪婪

Python的正则表达式默认是贪婪匹配。这意味着它将尽可能多的,尽可能往后匹配。只要后面还有能成功匹配的字符串,就不会停下来。

比如

import re
 
p = re.compile(r'我有一万*')
result = re.match(p, '我有一万万万万万')
print(result)
 

虽然*可匹配0次,1次...多次。但是不是返回我有一或者我有一万,而是后面有多少就匹配到多少。

如果要变成非贪婪匹配呢?后加?

p = re.compile(r'我有一万*?')
result = re.match(p, '我有一万万万万万')

这样就会尽可能少的匹配,因为*最少能匹配0次,所以这里返回我有一

注意,这里的?不要解释成0次或者1次,在非贪婪里面的?和上面介绍的?是有差别的。

通配字符"."

.可以匹配除了换行符之外的所有字符,如果加入标志位flags=re.DOTALL,使得.什么都可以匹配(包括换行符),还有re.IGNORECASEre.VERBOSE

# re.DOTALL
p = re.compile(r'good.haha', re.DOTALL)
result = re.findall(p, 'good\nhaha')
# 按位或可以同时使用两种模式
p = re.compile(r'good.haha', re.IGNORECASE | re.DOTALL)
result = re.findall(p, 'GOOD\nHahA')
 
# re.VERBOSE可以忽略空白字符和注释,当模式比较复杂时这样可能会直观点
p = re.compile(r'''
    \w+. # asdf
    \w+''' # some..
               , re.IGNORECASE | re.DOTALL | re.VERBOSE)
 
result = re.findall(p, 'GOOD\nHahA')

顺便一提,\w匹配单词字符,它包括了数字

搭配*?更好用

.*  贪婪匹配所有字符
.*? 非贪婪匹配所有字符

举个例子

import re
#贪婪
p = re.compile(r'abcd.*1234', re.DOTALL)
result = re.findall(p, 'abcdDAMN1234IT1234')
print(result) # ['abcdDAMN1234IT1234']全部匹配
# 非贪婪
p = re.compile(r'abcd.*?1234', re.DOTALL)
result = re.findall(p, 'abcdDAMN1234IT1234')
print(result) # ['abcdDAMN1234']遇到第一个1234就停止

使用捕获组

上面的例子如果使用().*?包含起来,在findall下将只返回括号里的内容,这很有用,往往我们需要的只是那里面的内容。

import re
 
p = re.compile(r'abcd(.*?)1234', re.DOTALL)
result = re.findall(p, 'abcdFUCK1234')
# out: ['FUCK']
print(result)

如果有多个括号呢?

import re
p = re.compile(r'[a-z]+((\d+)-(\d+))[a-z]+')
print(result.group(1))
print(result.group(2))
print(result.group(3)))
 
result = re.findall(p, 'afs123-456gds')
print(result)

可以看到,我们把数字用括号包起来了,这里有3个括号。输出是这样的

[('123-456', '123', '456')]列表里面实际上是一个元组,分别对应了三个括号里面的值。如果觉得findall返回的形式不够清楚,可以用group

p = re.compile(r'[a-z]+((\d+)-(\d+))[a-z]+')
 
result = re.match(p, 'afs123-456gds')
print(result.group()) # afs123-456gds
print(result.group(1)) # 123-456
print(result.group(2)) # 123
print(result.group(3)) # 456

group()或者group(0)意思一样,永远放回匹配成功的整个字符串。貌似和括号没有什么关系。不过要是使用group(1)查看下就会发现,它返回了第一个分组里的内容。上面共有3个分组,所以最多group(3)group(4)就要报错了。发现Python将最外层的括号视为第一组,里面的分组按照从左到右的顺序依次为第二组、第三组

还能使用groups()方法,返回所有分组(注意和group()区分)

('123-456', '123', '456')按照顺序依次是第一第二第三组,这和用findall返回的数据一样(只是少了列表包围)

分割字符串

使用re.split()

import re
# 以这个模式为分隔符
p = re.compile(r'\d+')
 
result = re.split(p, 'tom32jerry456haha')
print(result) # ['tom', 'jerry', 'haha']

可以看到,以数字为分隔符,将单词提取出来了。

字符串的替换

还是上面的例子,上面以数字分割,这次让汉字替换掉数字。

import re
 
p = re.compile(r'\d+')
 
result = re.sub(p, '中文', 'tom32jerry456haha')
print(result) # tom中文jerry中文haha

如果要用到匹配得文本本身,可以使用\1\2这样的形式,表示使用分组得第一组和第二组,\0没有这样的写法,这会被当成空字符串

import re
 
p = re.compile(r'(\d+)abcd(\d+)')
 
result = re.sub(p, r'\2invert\1', '12345abcd67890')
print(result) # 67890invert12345

有两个分组,r'\2\1'这里要使用原始字符串,不用的话自己试试看输出啥东西。

表示用分组2invert分组1得内容替代原字符串。由于分组1为12345,分组2为67890,所以是使用了67890invert12345代替了原字符串.

哦对了,平常还有一个用得比较多。\s可以匹配空格/换行符/制表符等等空白字符。其他的,用到的时候再查表吧!

针对我个人日常得使用,掌握这么多应该差不多了。不过有个博客总结得更详细,推荐Python正则表达式指南


by @sunhaiyu

2017.6.24

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • re模块手册 本模块提供了和Perl里的正则表达式类似的功能,不关是正则表达式本身还是被搜索的字符串,都可以...
    喜欢吃栗子阅读 3,949评论 0 13
  • 初衷:看了很多视频、文章,最后却通通忘记了,别人的知识依旧是别人的,自己却什么都没获得。此系列文章旨在加深自己的印...
    DCbryant阅读 3,967评论 0 20
  • 老张我 没上过大学 教你们 还是绰绰有余的 说完 就用方言 把《陋室铭》 背一遍 坐在最后一排 的二牛站起来 老师...
    关中陈镜阅读 126评论 0 0
  • 作者:blue(又名一书and一世界)我的github 适用对象:ubuntu使用者 问题:terminator+...
    一书and一世界阅读 973评论 0 0
  • 今天母亲节,所以给妈妈画了一张,嘿嘿。明天醒了,就发给妈妈。。 这张是原图! 明天,祝全天下最伟大的母亲,母亲节快...
    萌283阅读 262评论 0 0