据上一篇文章的更新已经6天了,我不是故意偷懒的(不过确实还是偷懒了).因为最近离职了压力比较大,再加上我前两天仔细地看了一下字符串那一章感觉也没啥好写的,但凡有点编程基础的人都能知道字符串是如何用的,我看的这本书介绍字符串的那一章基本就是简单介绍了一下字符串这种数据类型的概念和几个操作字符串的函数,感觉我单独提出来写一章也没啥好写的,之后的项目介绍中肯定也离不开字符串,所以我打算在以后的章节如果遇到了字符串的操作再根据具体问题具体介绍.余下的两天里我看了正则表达式,说实话作为参与过机器学习项目的人来说,这个知识点说不上精通但也应该很熟悉,毕竟数据分析需要爬虫,爬虫离不开正则表达式.但是本科做项目的时候我偷懒了,还记得第一次学长告诉我让我学爬虫的时候我打开了一篇博客,看到了很多奇奇怪怪的符号之后就被劝退了,以后的项目中我需要数据的时候都会找我一个会爬虫的同学帮我爬,我直接拿着数据调库就完事了,现在真的很后悔,出来混总是要还的呀.毕业两个月换了两个工作一个是app开发,一个是网络安全,都不是自己喜欢的所以最终我还是选择了离职,正在家里纠结着是出去工作还是复习考研再读我想做的方向,一来一回想这些问题也大幅地拉低了我看书的效率,所以昨天晚上11点多才把这本书的正则表达式部分看完,把该写的demo和书上该做的习题做完就半夜1点了,现在8点刚刚起床吃完早餐在电脑旁准备把这两天的所学记录下来.一下就说多了,是时候言归正传了.
正则表达式
正则表达式一般是用在字符串的匹配上面,试想,假如你登录了一个网页,你想找到网页上的几个字或者一行字,你该怎么办?一般有两个办法,1.脑子里想着你想要找到的内容,然后用眼睛一行一行地去看,这种办法如果要找的内容少还可以,但是内容多的话很容易让你眼花缭乱,浏览器给我们想了第二种办法,2.大部分人都应该知道,ctrl+f这个快捷键,按下该快捷键在搜索框里输入想要搜索的内容就能很快找到这个页面中与之匹配的内容.这个操作就是字符串的匹配,其实早在我学数据结构的时候就介绍了KMP这样的经典算法,还有各种各样更多的算法都可以取实现这种匹配,正则表达式虽然看起来比较复杂,但是匹配效率以及功能都比这些匹配算法要强很多.
不用正则表达式来匹配字符
这种方式有难有异,难得话比如上面提到的用KMP算法来匹配,这个算法很简短但是会有一些思维难度,感兴趣的话可以随便拿一本数据结构的书就能找到算法原理以及代码实现.,我就不大篇幅地去介绍KMP算法了(好久没翻数据结构的书了,现在也只能记个大概了),当然有简单的方法了,那就是if-else
了,记得大三的时候上编译原理的时候,老师让我们写词法分析器,语法分析器的时候没少用if-else
,记得最开始写的词法分析器版本,虽然达到老师的要求了,但是差不多代码写了差不多3000行,写了无数个if-else
所以,我不推荐在匹配的时候用if-else
因为效率真的真的很低.个人水平有限(懒癌不想翻数据结构的书了)在这里先举个例子就举个书上简单的例子吧:
假如我们需要匹配一串电话号码:415-555-4242,这串字符一共有5部分,3个数字,一个-,三个数字,一个-,4个数字.那该如何匹配呢?
默认上述的电话号码是标准的电话号码,可以向到的是上面的字符串一共有12位,并且1–3,5–7,9–12位都必须是数字,数字中间的连接符则必须是’-’,所以可以这样来写:
def phonenumber(number):
if(len(number != 12):
return False
for i in range(0,3):
if not number[i].isdecimal():
return False
if number[3] != '-':
return False
for i in range(4,7):
if not number[i].isdecimal():
return False
if number[7] != '-':
return False
for i in range(8, 12):
if not number[i].isdecimal():
return False
return True
print(phonenumber('415-555-4242'))
print(phonenumber('woshilk12138'))
在终端输入python test.py运行结果如下:
True
False
很明显415-555-4242匹配成功了,而woshilk12138不符合规则所以匹配失败了.
用正则表达式来匹配字符
常用的正则表达式符号
str?表示匹配0次或者一次str
str表示匹配0次或者多次str
str+表示匹配1次或者多次str
str{n}表示匹配n次str
str{n,}表示匹配至少n次str
str{,n}表示匹配至多n次str
str{n,m}表示匹配至少n次,至多m次str
str{n,m}?或str?或str+?表示对str的分组不使用贪心方式匹配
^str表示匹配的字符串必须以str开始
str$表示匹配的字符串必须以str结束
\d或\w或\s表示匹配数字或单词或空格
\D或\W或\S表示匹配不是数字或单词或空格的字符
[abcd]表示匹配[]内的所有字符(如a,b,c,d)
[^abcd]表示匹配除a,b,c,d以外的字符
上述的这些就是正则表达式符号,正则表达式就是根据实际需要匹配的字符串,通过这些符号的组合写出来的式子.下面将一一用代码展示这些符号的具体作用以及一些零碎的知识点.
正则表达式的介绍
首先要说的就是在使用证则表达式的时候,需要导入一个模块import re
.下面就开始切入正题吧.
假如现在需要匹配一串电话号码4155554242,\d是匹配数字的正则表达式符号,那么
代码如下:
import re
>>> num = re.compile(r'\d\d\d\d\d\d\d\d\d\d')
>>> number = num.search('my phonenumber is 4155554242')
>>> print(number.group())
4155554242
可以看到首先导入了re模块,然后compile()函数中的参数就是我们设置的正则表达式(上述代码里是要匹配10个数字),compile()函数将返回一个regex对象,我们使用regex对象调用它的search()函数传入需要匹配的原始字符串,search()函数会返回一个match对象,最后再通过match对象调用它的group()函数就可以得到原始字符串中我们想要的数字.这个过程我用文字来描述显得有些复杂,可能是我语言表达不是很好,但是过程其实很简单,就是调用别人写好的函数传入我们自己设定的参数最后得到我们预想的结果.
也可以对电话号码分组
import re
>>> num = re.compile(r'(\d\d\d)(\d\d\d\d)(\d\d\d)')
>>> number = num.search('my phonenumber is 4155554242)')
>>> number.group()
'4155554242'
>>> number.group(0)
'4155554242'
>>> number.group(1)
'415'
>>> number.group(2)
'5554'
>>> number.group(3)
'242'
可以看到在正则表达式中使用了()将要匹配的字符串分成了3组,当我们传入原始字符串之后,通过group()函数来可以显示匹配后字符串的不同部分.group()函数中不传参数和传入0都表示返回匹配到的整个字符串.
通过管道匹配
先看代码:
>>> name = re.compile(r'Tony|Alice')
>>> na = name.search('Tony and Alice')
>>> na.group()
'Tony'
>>> na = name.search('Alice and Tony')
>>> na.group()
'Alice'
'|'在正则表达式中是管道符号,我理解的管道符号就是’或’的意思,就上述代码而言,我们需要匹配Tony或Alice,但是这个’或’是有条件的,在原始字符串中哪个先出现就先匹配哪个,一旦匹配到一个以后就立马返回匹配到的字符串.
正则表达式中的’?’
代码如下:
>>> name = re.compile(r'ba(na)?na')
>>> na = name.search('I say bana')
>>> na.group()
'bana'
>>> na = name.search('I like banana')
>>> na.group()
'banana'
>>> na = name.search('I like bananananananananananana')
>>> na.group()
'banana'
?表示表示匹配前面字符0次或者1次,可以看到第8行不管字符串中’na’有多少个,顶多就只匹配1个.
正则表达式中的’*‘和’+’
代码如下:
>>> name = re.compile(r'p(ea)*ear')
>>> na = name.search('I like pear')
>>> na.group()
'pear'
>>> na = name.search('I like peaeaeaeaeaeaeaear')
>>> na.group()
'peaeaeaeaeaeaeaear'
'*'可以匹配括号里的字符0次和多次
>>> name = re.compile(r'p(ea)+ear')
>>> na = name.search('I like peaear')
>>> na.group()
'peaear'
>>> na = name.search('I like peaeaeaeaeaeaeaeaear')
>>> na.group()
'peaeaeaeaeaeaeaeaear'
>>> na = name.search('I like pear')
>>> na.group()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'group'
'+'可以匹配括号里的字符1次或多次,可以看到上述代码的第8行,当匹配次数是0次的时候程序就会报错.
正则表达式中’{}'的用法
代码如下:
>>> name = re.compile(r'(hello){5}')
>>> na = name.search('hellohellohellohellohello')
>>> na.group()
'hellohellohellohellohello'
{5}表示匹配()中的hello5次.
>>> name = re.compile(r'(hello){3,5}')
>>> na = name.search('hellohellohello')
>>> na.group()
'hellohellohello'
>>> na = name.search('hellohellohellohello')
>>> na.group()
'hellohellohellohello'
>>> na = name.search('hellohellohellohellohello')
>>> na.group()
'hellohellohellohellohello'
(3,5)表示至少匹配()中的hello3次,至多匹配5次.这都很简单,很容易理解.但是看到上述代码的第8行,我们的原始字符串有5个hello,正则表达式中写的是匹配hello至少3次,至多5次,那为什么有5个hello的时候匹配的结果是5个,而不是3个或者4个呢?这就要说到贪心匹配的问题了.
一句话说明白贪心匹配,在python中默认的匹配方式就是贪心匹配,也就是在匹配字符串的时候默认匹配最多次数,所以上述代码的第8行就匹配了5个hello.
非贪心匹配很简单,就是在{}后加上一个’?'就是按最小匹配次数匹配字符串了.
>>> name = re.compile(r'(hello){3,5}?')
>>> na = name.search('hellohellohellohellohello')
>>> na.group()
'hellohellohello'
这样就只匹配3次了.
findall()函数
上述的search()函数在传入原始字符串后会返回一个match对象,通过调用match对象的group()函数就可以返回一个字符串,这个字符串就是匹配的目标字符串.而findall()函数在传入原始字符串后会直接返回一个字符串列表.
>>> num = re.compile(r'\d\d\d\d\d\d\d\d\d\d')
>>> num.findall('cell:4155559999 work:2425550000')
['4155559999', '2425550000']
如果正则表达式中有分组,则会返回一个元组列表.
>>> num = re.compile(r'(\d\d\d)(\d\d\d\d\d\d\d)')
>>> num.findall('cell:4155559999 work:2425550000')
[('415', '5559999'), ('242', '5550000')]
正则表达式中\d \w \s的用法
文字表述有点麻烦,用书上的例子直接看代码反而易懂许多:
>>> num = re.compile(r'\d+\s\w+')
>>> num.findall('12 drummers, 11 pipers, 10 lords, 9 ladies, 8 maids, 7 swans')
['12 drummers', '11 pipers', '10 lords', '9 ladies', '8 maids', '7 swans']
很简单吧,代码第1行的正则表达式意思是,1次或多次匹配数字,匹配单词,1次或多次匹配空格.
正则表达式中^和$的用法
如下,可以看到书上的例子,第1行的正则表达式的意思是匹配的内容必须从’Hello’处开始,很明显第4行的原始字符串中没有’Hello’
所以匹配失败.
>>> beginsWithHello = re.compile(r'^Hello')
>>> beginsWithHello.search('Hello world!')
<_sre.SRE_Match object; span=(0, 5), match='Hello'>
>>> beginsWithHello.search('He said hello.') == None
True
再看书上的第二个例子,要求匹配的内容必须以数字结束,第2行的内容匹配成功了,而第4行中的内容结尾处没有数字,所以匹配失败.
>>> endsWithNumber = re.compile(r'\d$')
>>> endsWithNumber.search('Your number is 42')
<_sre.SRE_Match object; span=(16, 17), match='2'>
>>> endsWithNumber.search('Your number is forty two.') == None
True
正则表达式中[]的用法
一句话说明白:[]表示匹配[]内的任意字符.注意这里使用的是findall()函数.
>>> num = re.compile(r'[defhIs]')
>>> num.findall('I might have to drop out of school')
['I', 'h', 'h', 'e', 'd', 'f', 's', 'h']
下面的代码与上述代码正好相反,在正则表达式中加入’^'它匹配的是除[]内字符外的任意字符.
>>> num = re.compile(r'[^defhIs]')
>>> num.findall('I might have to drop out of school')
[' ', 'm', 'i', 'g', 't', ' ', 'a', 'v', ' ', 't', 'o', ' ', 'r', 'o', 'p', ' ', 'o', 'u', 't', ' ', 'o', ' ', 'c', 'o', 'o', 'l']
正则表达式中’.‘和’*'的用法
‘×’和’.'属于通配字符
‘.’是匹配除换行之外’.‘之前的所有的字符
书上的例子:
>>> atRegex = re.compile(r'.at')
>>> atRegex.findall('The cat in the hat sat on the flat mat.')
['cat', 'hat', 'sat', 'lat', 'mat']
’.ב是匹配所有的字符
书上的例子:
>>> nameRegex = re.compile(r'First Name: (.*) Last Name: (.*)')
>>> mo = nameRegex.search('First Name: Al Last Name: Sweigart')
>>> mo.group(1)
'Al'
>>> mo.group(2)
'Sweigart'
可以看到上述的正则表达式规定匹配’First Name’之后和’Last Name’之后的所有字符.
'.*'也有贪心和非贪心匹配
书上的例子:
>>> nongreedyRegex = re.compile(r'<.*?>')
>>> mo = nongreedyRegex.search('<To serve man> for dinner.>')
>>> mo.group()
'<To serve man>'
>>> greedyRegex = re.compile(r'<.*>')
>>> mo = greedyRegex.search('<To serve man> for dinner.>')
>>> mo.group()
'<To serve man> for dinner.>'
和之前处理贪心匹配一样加上’?'正则表达式就按照非贪心模式匹配.
'.*'也可以匹配换行字符
在compile()函数中再传入一个DOTALL参数即可匹配.
>>> num = re.compile(r'.*',re.DOTALL)
>>> number = num.search('I\nmight\nhave\nto\ndrop\nout\nof\nschool')
>>> number.group()
'I\nmight\nhave\nto\ndrop\nout\nof\nschool'
不区分大小写的匹配
可以看到,我们规定匹配’hello’后在原始字符串中匹配,按以前的方式是匹配不到’hello‘的,但是当我们在compile()函数中假如参数I后就能不区分大小写地匹配到大写的’HELLO’了.
>>> num = re.compile(r'hello')
>>> num.findall('HELLOworld')
[]
>>> num = re.compile(r'hello',re.I)
>>> num.findall('HELLOworld')
['HELLO']
sub函数替换字符串
书上的例子
>>> namesRegex = re.compile(r'Agent \w+')
>>> namesRegex.sub('CENSORED', 'Agent Alice gave the secret documents to Agent Bob.')
'CENSORED gave the secret documents to CENSORED.'
可以看到原始字符串中的Agent被CENSORED替换掉了.
书上的项目
这个项目位于《Python编程快速上手—让繁琐工作自动化》Page:130:电话号码和 E-mail 地址提取程序.
简单来说就是提取这个网址http://www.nostarch.com/contactus.htm的电话号码以及邮箱.
书上的做法导入了pypercilp这个库,我暂时是真的不太能理解这个剪贴板有什么用,可能是学习能力不太强吧,所以我就删除了源代码中的这个模块,将网页的所有内容存在了一个字符串里面然后再用书中的正则表达式去匹配,我整合了书上的代码,让代码可以正常跑起来,具体的过程我也看了其实很简单但是描述起来就很麻烦,具体过程书中Page:130–133有详细的描述,我在这里只做一个勤劳的搬运工.
代码如下:
import re
#匹配phonenumber
phoneRegex = re.compile(r'''(
(\d{3}|\(d\{3}\))?
(\s|-|\.)?
(\d{3})
(\s|-|\.)
(\d{4}))''',re.VERBOSE)
#匹配e-mail
emailRegex = re.compile(r'''(
[a-zA-Z0-9._%+-]+
@
[a-zA-Z0-9.-]+
(\.[a-zA-Z{2,4}])
)''',re.VERBOSE)
text = '''Skip to main content
Home
Search form
Catalog
Blog
Media
Write for Us
About Us
Contact Us
Topics
Art & Design
General Computing
Hacking & Computer Security
Hardware / DIY
Kids
LEGO®
Linux & BSD
Manga
Programming
Python
Science & Math
Scratch
System Administration
Early Access
Free ebook edition with every print book purchased from nostarch.com!
Shopping cart
0 Items Total: $0.00
User login
Log in
Create account
Contact Us
No Starch Press, Inc.
245 8th Street
San Francisco, CA 94103 USA
Phone: 800.420.7240 or +1 415.863.9900 (9 a.m. to 5 p.m., M-F, PST)
Fax: +1 415.863.9950
Reach Us by Email
General inquiries: info@nostarch.com
Media requests: media@nostarch.com
Academic requests: academic@nostarch.com (Please see this page for academic review requests)
Help with your order: info@nostarch.com
Reach Us on Social Media
Twitter
Facebook
Instagram
Pinterest
Navigation
My account
Want sweet deals?
Sign up for our newsletter.
About Us | Jobs! | Sales and Distribution | Rights | Media | Academic Requests | Conferences | Order FAQ | Contact Us | Write for Us | Privacy
Copyright 2019. No Starch Press, Inc'''
matches = []
for groups in phoneRegex.findall(text):
#phoneNum = '-'.join([groups[1],groups[3],groups[5]])
matches.append(groups[0])
for groups in emailRegex.findall(text):
matches.append(groups[0])
if len(matches)>0:
print('\n'.join(matches))
else:
print('no phone or e-mail addresses found')
结果输出:
800.420.7240
415.863.9900
415.863.9950
info@nostarch.c
media@nostarch.c
academic@nostarch.c
info@nostarch.c
text中的字符串是我直接ctrl+a,ctrl+c了整个网页中的内容.
两个简单的实例
第一个题目取自《Python编程快速上手—让繁琐工作自动化》page:136:强口令检测
写一个函数,它使用正则表达式,确保传入的口令字符串是强口令.强口令的定义是:长度不少于 8 个字符,同时包含大写和小写字符,至少有一位数字。你可能需要用多个正则表达式来测试该字符串,以保证它的强度.
代码如下:
import re
def check(text):
if len(text) <= 8:
return False
number = re.compile(r'\d+')
if number.search(text) == None:
return False
number2 = re.compile(r'[A-Z]+')
if number2.search(text) == None:
return False
number3 = re.compile(r'[a-z]+')
if number3.search(text) == None:
return False
return True
结果输出:
True
False
str1符合强口令故最终输出True,str只包含了小写字母和数字所以输出False.
第一个题目取自《Python编程快速上手—让繁琐工作自动化》page:136:strip()的正则表达式版本
写一个函数,它接受一个字符串,做的事情和 strip()字符串方法一样.如果只传入了要去除的字符串,没有其他参数,那么就从该字符串首尾去除空白字符。否则,函数第二个参数指定的字符将从该字符串中去除.
说实话没怎么读明白题目的意思,所以我就按照strip()函数的功能来设计代码了.
strip()函数的功能分为两种情况:
1.若strip()函数中没有参数,则默认去除传入字符串首尾的空格.
2.若strip()函数中有参数,假如是strip(a),则去除传入字符串首尾的字符’a’
根据这两种情况就可以编写strip()函数的功能.
代码如下:
import re
def strip(text, str=None):
if chars is None:
reg = re.compile('^ *| *$')
else:
reg = re.compile('^[' + str +']*|[' + str +']*$')
return reg.sub('', text)
print(strip(' 123456 '))
print(strip('abcdcba'),'a')
print(strip('abcd dcba'),'ab')
结果输出:
123456
bcdcb
cd dc
越往后东西就越多,可能我的理解也不到位,如果有什么说错的理解不到位的地方(到现在为止我写的关于书上每一章后面实践项目的代码基本都是一遍过,没调过bug,只要能输出我想要的结果也没怎么测试),有缘看到的朋友就请帮我纠正一下,感谢!
本文内容主要参考《Python编程快速上手—让繁琐工作自动化》第七章内容,如果有什么错误的地方欢迎指正.