Python中的字符串处理(re正则表达式)by Young 2021.11.13

本文详细探讨了Python中的字符串处理,重点在于re模块的正则表达式使用。通过实例讲解了如何利用转义字符进行特殊字符组合的匹配,适合前端和后端开发者提升正则技能。
摘要由CSDN通过智能技术生成
写在前面的说明:
(1)作者的部分翻译可能和python 标准中文文档之间有出入,作者已尽力保持一致,如有不妥,烦请读者指出,作者一定及时更正;
(2)文中介绍的所有内容作者均使用pythong 3.9在jupyter lab上验证通过,但是由于作者本地文本编辑器格式与CSDN网页编辑器有冲突,在修改中可能引入错误,如果需要实际使用,需要加以注意,如果出现作者描述与读者运行结果不符请联系作者更正;
1. 正则表达式简介 (regular expression, 简写为regex 或者regexp)
简单说来正则表达式就是字符串的公式化表达。在处理实际文本数据的过程中,我们需要的格式和原有数据的格式几乎不可能一致,这就会产生各种各样的字符串处理需求。简单的例子,比如校验用户输入的邮箱是否为有效,大体上有效地址是这个样子:sample@###.com ,但是用户输入可能长短不一,有的有多个子域名,这个时候无法通过简单的If-else逻辑去做判断(理论上可以,但是用if-else去写会非常非常非常之冗杂,而且无法维护),类似这样的场景,我们就需要使用正则表达式了,在本文的最后,我们会给出如何判断一个用户输入是否为有效邮箱的正则表达式。在此之前,我们先介绍一些简单的基础知识:(当然,邮箱这个其实也很基础)
2. 正则表达式使用示例
在python中使用正则表达式一般是通过调用re库来实现的。
我们先以一个从字符串中清理出数字部分的情况为例,看看正则表达式在实际中的使用效果
Example 1 
常见需求,从字符中提取仅数字部分。由于很多软件生成的结果默认带单位或是有特殊格式,无法直接转换为float、double等数字类型做后处理,需要先去掉部分非数字部分字符串
比如: input : “12.0 mm” , output: 12.0
这种情况正则表达式大有用武之地,我们来看看代码实现:
import re #使用正则表达式来处理数据需要先调用re库(python默认安装)
var_str_list = ["+5.02 mm", "-5.02 mm", "1.2e6 MPa", "-1.e6 MPa", "100,000.01"]
print(var_str_list)
var_float_list = [float(re.sub("[^0-9\-.eE]","",var_str)) 
                                for var_str in var_str_list ]
运行结果:
['+5.02 mm', '-5.02 mm', '1.2e6 MPa', '-1.e6 MPa', '100,000.01']
[5.02, -5.02, 1200000.0, -1000000.0, 100000.01]
从结果中我们可以看到,我们成功的取出了我们想要的数字部分
使用正则表达式的核心是利用正则表达式给出期望的数据形式,并完成对原始数据的查找或者替换,因此理解正则表达式的基本逻辑是重中之重,让我们先来分析上述例子中正则表达式的表达逻辑:
首先来分析re.sub的功能:
re.sub("[^0-9\-.eE]","",var_str) 
是将字符串中所有非数字组成的部分以空字符("")实现替换从而实现变相删除,最后就可以将纯数字部分直接转化为float数值型变量
我们再来看内部表达式的逻辑:
"[^0-9\-.eE]"
这就是典型的正则表达式:
首先[^]表示:除后续声明的字符类型,其他均被选择,也就是实现了反选功能(这里是反选非数字部分以便删除)
而 0-9-\.eE部分则是我们要声明保留的数字部分字符,
具体看来0-9表示数字字符0到9均要保留,\-是减号需要保留(\是escape,转义用),而 .  是小数点需要保留,eE是常见科学计数法的符号,需要保留
这里有两个问题:
(1)为什么减号 - 需要转义符,而小数点 . 不需要呢? 
    这里留一个悬念,后文揭晓
(2)感兴趣的童鞋可以试着用这里的表达式去处理这个字符串:"Velocity = 5.0m/s"
    猜猜看,我们可以正确提取出来5.0的有效信息吗?如果不能是哪里出错了呢?所以这个正则表达式的使用限制条件是什么呢?希望你阅读完本文后可以回答这些问题
3. re库中的基本知识 (Python re库简介
3.1.1 常见函数 (1)
实际应用中对string的处理包括两个主要操作:查找和替换(替换的本质是查找+直接修改),这里介绍4个常见function
函数描述 操作实质
findall返回包含所有匹配的字符段的list查找
search
返回包含匹配的字符段的 Match object(后文介绍对象构成)
查找
split将字符串在符合匹配表达式的位置处分割,以list形式返回替换
sub将字符串内匹配一处或者多处表达式的字段替换成自定义的字段替换
我们以一段代码来展示这四个函数:
re.findall("a","a , b , c")
re.search("a","a , b , c")
re.split(",","a , b , c")
re.sub(",","+","a , b , c")
输出结果如下所示:(一一对应)
['a']
<re.Match object; span=(0, 1), match='a'>
['a ', ' b ', ' c'] #格外注意这里的空格,实际中这里是容易出问题的地方
'a + b + c'
这几个函数具体的用法比较直观,不再赘述,这里重点说一下match object的有关信息, Match object 是记录本次search结果的有关信息的一个对象,它包含有以下常用方法(method)和特性(property):
Methods:
.span() 返回一个包含对应字符段在字符串中起止序号的若干元组(tuple) containing the start-, and end positions of the match.
.group() 返回字符串中对应本次search的字符段
Property:
.string 返回本次search中被用于输入的字符串
下面我们以一段代码为例展示match object的有关使用:
thisMatch = re.search("xyz","xyz , abc , xyz")
print(thisMatch.span())
print(thisMatch.string)
print(thisMatch.group())

输出结果:
(0, 3)
xyz , abc , xyz
xyz
Match object比此处的介绍会稍微复杂一些,尤其3.7版本后还添加了一些新的功能,具体细节还请移步python documentation
3.1.2 常见函数 (2)
与前述函数功能类似,但是在实际使用中略有不同的还有
函数描述 操作实质
finditer与findall返回匹配的字符段的list不同,这里返回的是可调用的iterator查找
fullmatch返回完全符合匹配的字符段的 Match object,注意这里是完全符合查找
subn返回一个tuple,包含替换后的新字符串,同时给出替换发生的次数替换
finditer:
x=re.finditer("[a-zA-Z]","a, A")
for s in x:
    print(s.span())
输出结果:
((0, 1)
(3, 4)
fullmatch
print(re.fullmatch("^hel{2}o world$","hello world"))
print(re.fullmatch("^hel{2}o world$","hello world!"))
输出结果:
<re.Match object; span=(0, 11), match='hello world'>
None
subn
re.subn("\d","d","1 2 3")
输出结果:
('d d d', 3)
3.1.3 一些可能有用的函数
re.escape(pattern)
用于转义 pattern 中的特殊字符。主要用于自我测试场景,如果目标文本可能包含正则表达式的元字符,那么想要确认转义的正确格式时,它会比较有用的,举例:
print(re.escape('https://www.xyz.org')) 
输出为:
https://www\.xyz\.org
至于其他函数本文就不再分析了,详情请移步python文档
3.2 元字符(Metacharacters)
元字符描述范例范例解释
[]一类字符的集合"[a-m]"

目标字符为a-m中其中一个字母(包括a,m),

类似还可以写出"[3-9]",目标字符为3-9之间一个数字(包括3,9)

\表示开始一个特殊字符段或者用于转义"\d"

\d表示返回字符串中含有表示数字的字符,

这类特殊标记后文会详细介绍

.可以用于表示任意字符,换行(new line)符号例外"he..o"

任何以he开始后面跟两个字符再加上o的组合均满足需求,

比如“hello”, “hexxo”等等

^目标字符以此开始"^hello"

任何以hello开始的字符串:

re.findall("^hello", "hello world")会返回hello,

而re.findall("^hello", "world hello")会返回空

$目标字符以此结束"world$"
任何以world结束的字符段:
re.findall("world$", "hello world")会返回world
,而re.findall("world$", "world hello")会返回空
*之前的一个字段出现零次或者更多次"he.*o"

任意字符(.)不出现或者重复出现很多次,“hello”,“helo”, “heo”

均符合要求。注意这里的小陷阱,“heldaffaxo”也是符合要求的,

因为是任意字符出现多次,并非指定字符出现零次或者多次,

换句话说,这里任何以he开口,o结尾的字符串均符合要求,

这在实际使用中及其常用

+
之前的一个字段出现一次或者更多次
"he.+o"

任意字符(.)至少出现一次,“hello”,“helo”均符合要求,

 “heo”不符合要求。如前分析,“heldaffaxo”也是符合要求的

?之前的一个字段出现零次或者一次"he.?o"

任意字符(.)至多出现一次, “heo”,“helo”均符合要求,

“hello”不符合要求。在这里“heldaffaxo”就不再符合要求了

{m,n}
{n}
之前字段重复出现m-n次(包括m,n)
之前字段重复出现n次
"he.{2}o"

任意字符(.)必须出现两次,“hello”符合要求, “helo”,“heo”不符合

要求。 与"he.*o"相比,其实实际上"he.{2}o"是在限制开头和结尾的

基础上加上了字符长度限制的硬性规定,这个实际中也较常用到,

比如检索学校数据库中以“A”开头,以“2021”结尾的所有学号

|或者"hello|world"不言自明
()群组(group) ^(world|hello)

re.sub("(world|hello)","XY", "world world hello")返回'XY XY XY',

()内的字符串会作为一个整体参与后续运算,

换句话说()加上()内的东西在运算中等价于一个字符

*?, +?, ??关闭*,+, ?的贪婪(greedy)模式<.*?> 

在*,+, ?后面在加上一个?可以关闭默认的贪婪模式,

re.findall('<.*>','<a> b <c>')会返回整个字符串['<a> b <c>'],

而re.findall('<.*?>','<a> b <c>')仅仅返回['<a>', '<c>']

(此处列表并不完整,详情请移步python文档)
3.3 具有特殊含义的字符序列

在正则表达式中以'\'开头具有特殊含义的字符组合

字符具体描述示例示例解释
\A若本字符后的字符段是对应字符串的开头则为对应match"\Ahel"

re.findall("\Ahel","hello")返回['hello'],

而re.findall("\Ahel","xhello")返回为空

\b若本字符后的字符段是对应字符串的开头或者结尾则为对应match
(此处r是确保字符串被当作raw string处理,缺失则无法正常识别)
r"llo\b"re.findall(r"llo\b","hello")返回['hello'],而
re.findall(r"llo\b","hellox")则返回为空
\B若本字符后的字符段出现在字符串中,但是不在开头或者不在结尾则为对应match
(此处r是确保字符串被当作raw string处理,缺失则无法正常识别)
r"\Blo"(不在开头)
r"lo\B"(不在结尾)
re.findall(r"\Blo","hello")返回['lo']而
re.findall(r"\Blo","lohel")则返回为空
\Z
若本字符后的字符段是对应字符串的结尾则为对应match
r"llo\Z"re.findall("lo\Z","hello")返回['lo']
\d返回字符串中字符为数字的部分"\d"re.findall(r"\d","hi 2.2")返回['2', '2']
\D返回字符串中字符不是数字的部分"\D"re.findall(r"\D","hi 2.2")返回['h', 'i', ' ', '.']
\s返回字符串中包含空格符的部分"\s"re.findall("\s","hello world")返回[' ']
\S返回字符串中不包含空格符的部分"\S"

re.findall("\S","hello world")返回

['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']

\w返回字符串中包含word的部分 (a-Z,  0-9以及下划线_)"\w"

re.findall("\w","hi_you % /1.0")返回

['h', 'i', '_', 'y', 'o', 'u', '1', '0']

\W返回字符串中不包含word的部分 (除a-Z,  0-9以及下划线_以外的字符)"\W"

re.findall("\W","hi_you % /1.0")返回

[' ', '%', ' ', '/', '.']

简单总结:
(1)特殊符号小写与大写直接存在互为补集的关系
(2)前四个符号返回的都是字符段( 字符串匹配模式),但后面的六个符号返回的则是分立的字符构成的list( 字节匹配模式)
3.4 一些常用与集合(Set[ ])有关的信息
集合(set)描述示例解释
[arn]找到字符段中存在 ar, 或者 n 的一个字符
[a-n]找到字符段中存在 a-n 之间的一个字符
[^arn]找到字符段中除ar, 或者 n 以外的一个其他字符,[^]操作非常实用
[012]找到字符段中存在 0, 1, 或者 2 的一个数字字符 
[0-9]找到字符段中存在 0-9 之间的一个数字字符
[0-1][0-9]找到字符段中两位的数字,第一位在0-1之间,第二位在0-9之间(实际就是00-19)re.findall("[0-1][0-9]","12")返回['12']
re.findall("[0-1][0-9]","42")返回空
[a-zA-Z]找到字符段中存在 a-z, A-Z 之间的一个字符re.findall("[a-zA-Z]","aA")返回['a', 'A']
re.findall("[a-zA-Z]","42")返回空
[+]在集合(set)的符号([ ])中间,+, *, ., |, (), $,{} 都是没有特殊含义的
这里有一些小细节在书写正则表达式的时候非常重要,
(1)没事不要加空格,空格会成为逻辑的一部分,打进去的空格符也是会参与运算的,除非是需要考虑空格,因此在书写过程中一定要十分注意
(2)在集合(set)的符号([ ])中间,+, *, ., |, (), $,{} 都是没有特殊含义的,注意,这里可不包括 - ,还记得前文讨论的转义符号吗,为什么有时候有,有时候不用呢,就是这个原因,下面我们来看看具体区别:
import re
txt = "+-rain 1.0-"
x = re.findall("[+-0-9]", txt)
print(x)

运行结果不包含1,但多出来一个.
['+', '-', '.', '0', '-']
而加上转义字符之后:
txt = "+-rain 1.0-"
x = re.findall("[+\-0-9]", txt)
print(x)

运行结果包含 1, 没有不需要的其他字符:
['+', '-', '1', '0', '-']
 
4. 常用的模块常量
模块常量调用方法解释
IGNORECASEre.IGNORECASE 或简写为 re.I匹配中忽略大小写
ASCIIre.ASCII 或简写为 re.A

保证 \w, \W, \b, \B, \d, \D, \s 和 \S 只匹配ASCII,

而不是Unicode

DOTALLre.DOTALL 或简写为 re.S

匹配所有字符,包括换行符\n。注意默认模式下 .

是不能匹配行符\n的

MULTILINEre.MULTILINE 或简写为 re.M 

 多行模式,当某字符串中有换行符\n,

默认模式下是不支持换行符特性的,比如:行开头

和 行结尾,而多行模式下是支持匹配行开头的
正则表达式中^表示匹配行的开头,

默认模式下它只能匹配字符串的开头;

而在多行模式下,它还可以匹配 换行符\n后面的字符。
注意:正则语法中^匹配行开头、\A匹配字符串开头,

单行模式下它两效果一致,多行模式下\A不能识别\n

VERBOSE re.VERBOSE 或简写为 re.X

详细模式,可以在正则表达式中加注释,

可以说非常非常有用!

模块常量表格部分来自猪哥66的整理,更加详细的内容请移步Python正则表达式,这一篇就够了! - SegmentFault 思否
这里展开说明一下VERBOSE部分,个人感觉非常有用。
这里我们来看一下document中给出的例子:
a = re.compile(r"""\d +  # the integral part
                   \.    # the decimal point
                   \d *  # some fractional digits""", re.X)
b = re.compile(r"\d+\.\d*")

这里a和b的效果是等价的,都是用于匹配一个带小数点的浮点数
还记得前文提到的空格问题么,在VERBOSE模式下,所有的空格都将被忽略。
5. 实例:如何识别输入文本为有效邮箱地址
最后的最后,让我们应用学习到的知识一起来构建一个识别邮箱地址的正则表达式
(声明:此处表达式是作者自己构建并没有被大量输入验证过,仅作为简单范例,有不周之处还请读者多多批评)

emails = ["Aacb111.111@qq.com", "11D1-1d11@163.com", "111_1d1d1@py.p-y.py.com","d?@sina.com","12d", ".111111@qq.com"]
regexPattern = re.compile(r"""^[0-9a-zA-z][0-9a-zA-z_.\-]+   # 邮箱名称可以包含数字,字母和下划线_横线-和点.,且必须以字母或数字开始
                   @                          # 标准邮箱符号
                   (([0-9a-zA-z\-]+).)+        # 子域名1
                   [0-9a-zA-z\-]+$             # 高级域名""", re.X)
for email in emails:
    print(re.fullmatch(regexPattern, email))

它的输出结果是:
<re.Match object; span=(0, 18), match='Aacb111.111@qq.com'>
<re.Match object; span=(0, 17), match='11D1-1d11@163.com'>
<re.Match object; span=(0, 23), match='111_1d1d1@py.p-y.py.com'>
None
None
None
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值