Python 字符串匹配、搜索及替换

文章目录

  • 字符串匹配、搜索及替换
    • 字符串开头或结尾匹配
      • str.startswith() 和 str.endswith()
    • 用 Shell 通配符匹配字符串
      • fnmatch() 和 fnmatchcase()
    • 字符串匹配和搜索
      • str.find()
      • 正则表达式及 re 模块
        • re.match()
        • re.compile()
        • re.findall()
        • re.finditer()
    • 字符串搜索和替换
      • str.replace()
      • re.sub()
      • re.subn()

字符串匹配、搜索及替换


在处理字符串中,有时会需要定位字符串,然后对字符串进行相应的处理。

字符串开头或结尾匹配


对于字符串的检查,可以通过特定的文本模式进行匹配。在 Python 内置类型中也提供了 str.startswith()str.endswith() 方法对字符串进行开头或结尾的检查。

str.startswith() 和 str.endswith()

str.startswith()str.endswith() 是检查字符串开头或结尾的一个简单方法。例如文件的后缀,URL 跳转协议,示例如下:

>>> filename = "string.txt"
>>> filename.endswith('.txt')
True
>>> filename.startswith('file:')
False
>>> url = 'http://www.python.org'
>>> url.startswith('http:')
True

如果要检查多种匹配的可能,可以提供一个元组,将所有的匹配项写入元组中,然后传给 startswith()endswith() 方法的第一个参数中:

>>> import os
>>> filenames = os.listdir('.')
>>> filenames
['tools', 'list_links.txt', 'access_test.py', 'control_roll.py', 'access.log', 'data.xls']
>>> [name for name in filenames if name.endswith(('.py', '.txt'))]
['list_links.txt', 'access_test.py', 'control_roll.py']
>>> any(name.endswith('.py') for name in filenames)
True

这里需要说明的是startswith()endswith() 两个方法的第一个参数,如果是要传入多种匹配选项,需要传入的类型必须是 tuple,如果有 listset 类型的选项,可以使用 tuple() 方法将已有选项进行转换。

上面提及的 startswith()endswith() 方法有一种替代的方法,就是切片操作,但是代码看起来会相对繁冗。示例如下:

>>> filename = 'string.txt'
>>> filename[-4:] == '.txt'
True
>>> url = 'http://www.python.org'
>>> url[:5] == 'http:' or url[:6] == 'https:' or url[:4] == 'ftp:'
True

还有另外一种方法是通过正则表达式去实现,示例如下:

>>> import re
>>> url = 'http://www.python.org'
>>> re.match('http:|https:|ftp:', url)
<_sre.SRE_Match object; span=(0, 5), match='http:'>

正则表达式方法放在这里,的确能够解决问题,但是有点大材小用。而且,Python 内置的 startswith()endswith() 方法,对于这种简单匹配运行会更快。

用 Shell 通配符匹配字符串


除了通过你内置类型的方法对字符串进行匹配,还可以使用 Unix Shell 中的常用的通配符(比如 *.pyDat[0-9]*.csv 等)去匹配字符串。

fnmatch() 和 fnmatchcase()

Python 提供的 fnmatch 模块就有两个函数 fnmatch()fnmatchcase() 可以用来实现这样的匹配。示例如下:

>>> from fnmatch import fnmatch, fnmatchcase
>>> fnmatch('string.txt','*.txt')
True
>>> fnmatch('string.txt','?ing.txt')
False
>>> fnmatch('string.txt','?tring.txt')
True
>>> fnmatch('Dat57.csv','Dat[0-9]*')
True
>>> names = ['Dat1.csv','Dat2.csv','config.ini','test.py']
>>> [name for name in names if fnmatch(name, 'Dat*.csv')]
['Dat1.csv', 'Dat2.csv']

但是需要注意的是 fnmatch() 函数,在使用不同的底层操作系统的大小写敏感规则是不同的。这里是因为这个函数中的形参都会使用 os.path.normcase() 进行大小写正规化,在 Linux,Mac 中,会原样返回;而在 Windows 中,这里会将字符转换为小写返回。示例如下:

>>> # On Mac
>>> fnmatch('string.txt', '*.TXT')
False
>>> # On Windows
>>> fnmatch('string.txt', '*.TXT')
True

如果对于这个大小写敏感规则很在意的情况下,可以使用 fnmatchcase() 来替代。这个函数完全区分大小写。示例如下:

>>> fnmatchcase('string.txt', '*TXT')
False

fnmatch() 函数匹配的能力介于简单字符串方法和正则表达式之间。如果在数据处理操作中,能够用简单通配符匹配的情况下,这是一个可以考虑的方案。

如果需求中需要做文件名的匹配,建议使用 glob 模块中的方法。具体的使用方法可参考下面的官方文档:

https://docs.python.org/3.6/library/glob.html

字符串匹配和搜索


如果需要匹配的字符串比较简单,通常情况下,需要调用基本字符串方法就可以。

str.find()

除了前面介绍的 str.endswith()str.startswith() 的方法,还有 str.find() 方法,这个方法用于查找匹配项第一次出现的位置,这些都能够实现简单的匹配搜索,示例如下:

>>> text = 'yeah, but no, but yeah, but no, but yeah'
>>> # 匹配开头或结尾
...
>>> text.startswith('yeah')
True
>>> text.endswith('no')
False
>>> # 搜索第一次出现位置
...
>>> text.find('no')
10

正则表达式及 re 模块


对于一些复杂的匹配,则需要借助正则表达式1re 模块。

re.match()

下面举例简单介绍匹配数字格式的日期字符串,示例如下:

>>> text1 = '11/27/2012'
>>> text2 = 'Nov 27, 2012'
>>> import re
>>> # 简单匹配一个或多个数字
...
>>> if re.match(r'\d+/\d+/\d+', text1):
...     print('yes')
... else:
...     print('no')
...
yes
>>> if re.match(r'\d+/\d+/\d+', text2):
...     print('yes')
... else:
...     print('no')
...
no
>>>

re.compile()

如果需要使用同个模式进行多次匹配,那么可以考虑将模式字符串编译为模式对象。re.compile() 提供编译正则表达式字符串,示例如下:

>>> pattern = re.compile(r'\d+/\d+/\d+')
>>> if re.match(pattern, text1):
...     print('yes')
... else:
...     print('no')
...
yes
>>> if re.match(pattern, text2):
...     print('yes')
... else:
...     print('no')
...
no

re.findall()

match() 方法总是从字符串开始去匹配,如果查找字符串任意部分出现的位置,建议使用 findall() 方法代替。示例如下:

>>> text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> pattern = r'\d+/\d+/\d+'
>>> re.findall(pattern, text)
['11/27/2012', '3/13/2013']
>>>

有时候定义正则表达式的时候,会利用括号进行捕获分组。示例如下:

>>> pattern = re.compile(r'(\d+)/(\d+)/(\d+)')
>>>

捕获分组的用处主要是用于将每个组的内容提取出来,也对后续的处理进行备用。示例如下:

>>> import re
>>> pattern = re.compile(r'(\d+)/(\d+)/(\d+)')
>>> match_data = re.match(pattern,'11/27/2012')
>>> match_data
<_sre.SRE_Match object; span=(0, 10), match='11/27/2012'>
>>> match_data.group(0)
'11/27/2012'
>>> match_data.group(1)
'11'
>>> match_data.group(2)
'27'
>>> match_data.group(3)
'2012'
>>> match_data.groups()
('11', '27', '2012')
>>> month, day, year = match_data.groups()
>>> text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> # 使用 findall(),注意返回的结果是列表里面包裹元组
... re.findall(pattern, text)
[('11', '27', '2012'), ('3', '13', '2013')]
>>> for month, day, year in re.findall(pattern, text):
...     print('{}-{}-{}'.format(year, month, day))
...
2012-11-27
2013-3-13

re.finditer()

findall() 方法会以搜索文本并以列表形式返回所有的匹配。如果想以迭代的方式返回匹配,可以考虑使用 re.finditer() 方法代替。

re.finditer() 方法返回一个迭代器,方法是从左到右扫描字符串,按照找到匹配项的顺序返回,示例如下:

>>> for match_data in re.finditer(pattern, text):
...     print(match_data.groups())
...
('11', '27', '2012')
('3', '13', '2013')

综合上述,re 模块进行匹配和搜索文本,核心的步骤是先使用 re.compile() 将正则表达式字符串进行编译,然后根据需求使用 match()findall() 或者 finditer() 方法。

一些建议: 编写正则表达式字符串的时候,通常会是 r'' 这种形式,这种使用原始字符串的做法,能够不用去解析反斜杠。如果不这样做的话,需要使用两个反斜杠,例如:(\\d+/\\d+/\\d+)

注意事项: match() 方法仅仅检查字符串的开始部分, 匹配的结果可能并非期望的结果。示例如下:

>>> pattern = re.compile(r'(\d+)/(\d+)/(\d+)')
>>> match_data = re.match(pattern, '11/27/2012abcdef')
>>> match_data
<_sre.SRE_Match object; span=(0, 10), match='11/27/2012'>
>>> match_data.group()
'11/27/2012'

如果需要精确匹配,得在正则表达式以 $ 结尾,如下示例:

>>> pattern = re.compile(r'(\d+)/(\d+)/(\d+)$')
>>> re.match(pattern, '11/27/2012abcdef')
>>> re.match(pattern, '11/27/2012')
<_sre.SRE_Match object; span=(0, 10), match='11/27/2012'>
>>>

上面的代码并未匹配 11/27/2012abcdef,因为编译的正则表达式以 $ 结尾,说明必须精确匹配才会返回结果。

如果仅仅是做一次简单的文本匹配/搜索操作的话,可以略过编译部分,但是,需要进行大量的匹配和搜索操作的话,最好先对正则表达式字符串进行编译,然后再重复使用。这样做的原因是不会消耗太多的性能 。

字符串搜索和替换


对于字符串的搜索时,有时候会对搜索的匹配项进行替换操作。

str.replace()

如果是简单的字面模式,可以直接用 str.replace() 方法,示例如下:

>>> text = 'yeah, but no, but yeah, but no, but yeah'
>>> text.replace('yeah', 'yep')
'yep, but no, but yep, but no, but yep'
>>>

re.sub()

对于复杂的模式,请使用 re 模块中的 sub() 函数。现在的需求是这样,将形式为 11/27/2012 的日期字符串改为 2012-11-27,下面的是实现的代码:

>>> text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> import re
>>> re.sub(r'(\d+)/(\d+)/(\d+)', r'\3-\1-\2', text)
'Today is 2012-11-27. PyCon starts 2013-3-13.'

sub() 函数中,第一个参数表示的是被匹配的模式,第二个参数表示的是替换模式。反斜杠数字,比如 \3 指向的是前面模式捕获分组的组号。

如果打算用相同的模式做多次替换,同样可以考虑先编译再重复使用。

re.subn()

如果除了替换后的结果外,还需要知道进行了多少次替换,可以使用 re.subn() 来代替。比如:

>>> text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> pattern = re.compile(r'(\d+)/(\d+)/(\d+)')
>>> newtext, n = re.subn(pattern, r'\3-\1-\2', text)
>>> newtext
'Today is 2012-11-27. PyCon starts 2013-3-13.'
>>> n
2
>>>

以上就是本篇的主要内容。


结语: 新年快乐!


  1. 可参考【正则表达式 笔记整理】 ↩︎

你可能感兴趣的:(Python,随记)