Jean's Blog

一个专注软件测试开发技术的个人博客

0%

Python字符串常用操作与正则表达式

字符串

常用基本操作

  • 反转字符串
  • 字符串切片操作
  • join串联字符串
  • 分割字符串
  • 替换
  • 子串判断
  • 去空格
  • 字符串的字节长度

反转字符串

1
2
3
4
5
6
7
8
>>> s = 'python'
# 方法一
>>> rs = ''.join(reversed(s))
>>> rs
'nohtyp'
# 方法二
>>> s[::-1]
'nohtyp'

字符串切片操作

生成 1 到 15 的序列,并在满足条件的索引处,替换为 javapython

1
2
3
4
5
6
>>> java, python = 'java', 'python'
>>> jl, pl = len(java), len(python)
# 索引 2、5、8 处替换为 Java 字符
# 索引 4 处替换为 Python 字符
>>> [str(java[i%3*jl:] + python[i%5*pl:] or i) for i in range(1, 10)]
['1', '2', 'java', '4', 'python', 'java', '7', '8', 'java']

输出结果,如下图:

image-20220408153726153

join 串联字符串

用下划线 _ 连接字符串 mystr:

1
2
3
4
>>> mystr = ['I', 'love', 'Python']
>>> res = '_'.join(mystr)
>>> res
'I_love_Python'

image-20220408153748946

分割字符串

根据指定字符或字符串,分割一个字符串时,使用方法 split。

join 和 split 可看做一对互逆操作:

1
2
>>> 'I_love_Python'.split('_')
['I', 'love', 'Python']

替换

字符串替换,使用 replace 方法。

如下字符串,小写的 o 全部替换为大写的 O:

1
2
3
>>> s = 'i love python'.replace('o', 'O')
>>> s
'i lOve pythOn'

子串判断

判断 a 串是否为 b 串的子串。

  • 方法一,使用in:

    1
    2
    3
    4
    5
    >>> a = 'our'
    >>> b = 'flourish'
    >>> r = True if a in b else False
    >>> r
    True
  • 方法二,使用方法find,返回字符串b中匹配子串a的最小索引

    1
    2
    3
    4
    >>> a = 'our'
    >>> b = 'flourish'
    >>> b.find(a)
    2

去空格

清洗字符串时,位于字符串开始和结尾的空格,有时需要去掉,strip 方法能实现。

如下字符串,使用 strip,清理字符串开头和结尾的空格和制表符。

1
2
3
4
5
>>> a = ' \tI love python \b\n'
>>> a
' \tI love python \x08\n'
>>> a.strip()
'I love python \x08'

字符串的字节长度

encode 方法对字符串编码后:

1
2
3
4
5
6
>>> def str_byte_len(mystr):
... mystr_bytes = mystr.encode('utf-8')
... return (len(mystr_bytes))
...
>>> str_byte_len('i love python')
13

image-20220408153816565

正则表达式

字符串封装的方法,处理一般的字符串操作,还能应付。但是,稍微复杂点的字符串处理任务,需要靠正则表达式,简洁且强大。

首先,导入所需要的模块 re:

1
import re
  • 首先,认识常用的元字符

    | 符号 | 说明 |
    | ———- | ——————————————————— |
    | . | 匹配除 “\n” 和 “\r” 之外的任何单个字符 |
    | ^ | 匹配字符串开始位置 |
    | $ | 匹配字符串中结束的位置 |
    | * | 前面的原子重复 0 次、1 次、多次 |
    | ? | 前面的原子重复 0 次或者 1 次 |
    | + | 前面的原子重复 1 次或多次 |
    | {n} | 前面的原子出现了 n 次 |
    | {n,} | 前面的原子至少出现 n 次 |
    | {n,m} | 前面的原子出现次数介于 n-m 之间 |
    | () | 分组,输出需要的部分 |

  • 再认识常用的通用字符

    | 符号 | 说明 |
    | ———- | ————————————————————————- |
    | \s | 匹配空白字符 |
    | \w | 匹配任意字母/数字/下划线 |
    | \W | 和小写 w 相反,匹配任意字母/数字/下划线以外的字符 |
    | \d | 匹配十进制数字 |
    | \D | 匹配除了十进制数以外的值 |
    | [0-9] | 匹配一个 0~9 之间的数字 |
    | [a-z] | 匹配小写英文字母 |
    | [A-Z] | 匹配大写英文字母 |

正则表达式,常会涉及到以上这些元字符或通用字符,下面通过 14 个细分的与正则相关的小功能,讨论正则表达式。

search 第一个匹配串

使用正则模块,search 方法,找出子串第一个匹配位置。

1
2
3
4
5
6
>>> import re
>>> s = 'i love python very much'
>>> pat = 'python'
>>> r = re.search(pat, s)
>>> r.span()
(7, 13)

match 与 search 不同

正则模块中,match、search 方法匹配字符串不同

具体不同:

  • match 在原字符串的开始位置匹配
  • search 在字符串的任意位置匹配

原字符串:

1
>>> s = 'flourish'

寻找模式串 our,使用 match 方法:

1
2
3
4
>>> recom = re.compile('our')
>>> recom.match(s)
>>>
# 返回空,找不到匹配

使用 search 方法:

1
2
3
4
5
6
>>> s = 'flourish'
>>> recom = re.compile('our')
>>> res = recom.search(s)
>>> res.span()
(2, 5)
# 匹配成功,our 在原字符串的起始索引为 2

那么,什么字符串才能使用 match 方法匹配到 our?比如,字符串 ourselves,ours 才能 match 到 our。

1
2
3
4
5
6
7
8
>>> s = 'ourselves'
>>> recom = re.compile('our')
>>> recom.match(s)
<_sre.SRE_Match object at 0x10b9a5ac0>
>>> res = recom.match(s)
>>> res.span()
(0, 3)
# 匹配成功,our 在原字符串的起始索引为 0

finditer 匹配迭代器

使用正则模块,finditer 方法,返回所有子串匹配位置的迭代器。

通过返回的对象 re.Match,使用它的方法 span 找出匹配位置。

1
2
3
4
5
6
7
8
9
>>> import re
>>> s = '山东省潍坊市青州第1中学高三1班'
>>> pat = '1'
>>> r = re.finditer(pat, s)
>>> for i in r:
... print(i)
...
<re.Match object; span=(9, 10), match='1'>
<re.Match object; span=(14, 15), match='1'>

findall 所有匹配

正则模块,findall 方法能查找出子串的所有匹配。

原字符串 s:

1
>>> s = '一共20行代码运行时间13.59s'

目标查找出所有所有数字:通用字符 \d 匹配一位数字 [0-9],+ 表示匹配数字前面的一个字符 1 次或多次。

1
2
3
4
>>> pat = r'\d+'
>>> r = re.findall(pat, s)
>>> print(r)
['20', '13', '59']

返回一个列表,找到三个数字 20、13、59,没有达到预期,期望找到 20、13.59。

因此,需要修改正则表达式。接下来看如下的案例:

案例

匹配浮点数和整数

  • ? 表示前一个字符匹配 0 或 1 次
  • .? 表示匹配小数点(.)0 次或 1 次。

匹配浮点数和整数,第一版正则表达式:r'\d+\.?\d+',图形化演示,此正则表达式的分解演示:

image-20220408153912331

1
2
3
4
5
>>> s = '一共20行代码运行时间13.59s'
>>> pat = r'\d+\.?\d+'
>>> r = re.findall(pat, s)
>>> r
['20', '13.59']

上面的正则表达式 r'\d+\.?\d+',能适配所有情况吗?

1
2
3
4
5
>>> s = '一共2行代码运行时间1.66s'
>>> pat = r'\d+\.?\d+'
>>> r = re.findall(pat, s)
>>> r
['1.66']

观察结果,没有匹配到数字 2。

正则难点之一,需要考虑全面、足够细心,才可能写出准确无误的正则表达式。

出现问题原因:r'\d+\.?\d+',后面的 \d+ 表示至少有一位数字,因此,整个表达式至少会匹配两位数。

修复问题,重新优化正则表达式,将最后的 + 后修改为 *,表示匹配前面字符 0 次、1 次或多次。

最终正则表达式为:r'\d+\.?\d*',正则分解图:

image-20220408153933643

1
2
3
4
5
>>> s = '一共2行代码运行时间1.66s'
>>> pat = r'\d+\.?\d*'
>>> r = re.findall(pat, s)
>>> r
['2', '1.66']

匹配正整数

案例:写出匹配所有正整数的正则表达式。

如果这样写:^\d*$,会匹配到 0,所以不准确。

如果这样写:^[1-9]*,会匹配 1. 串中 1,不是完全匹配,体会 $ 的作用。

正确写法:^[1-9]\d*$,正则分解图:

image-20220408153949728

1
2
3
4
>>> s = [-16, 1.5, 11.43, 10, 5]
>>> pat = r'^[1-9]\d*$'
>>> [i for i in s if re.match(pat, str(i))]
[10, 5]

re.l忽略大小写

re.l是方法的可选参数,表示忽略大小写。

如下,找出字符串中所有字符 t 或 T 的位置,不区分大小写。

1
2
3
4
5
6
7
8
9
>>> import re
>>> s = 'That'
>>> pat = r't'
>>> r = re.finditer(pat, s, re.I)
>>> for i in r:
... print(i.span())
...
(0, 1)
(3, 4)

split 分割单词

正则模块中 split 函数强大,能够处理复杂的字符串分割任务。

如果一个规则简单的字符串,直接使用字符串,split 函数。

如下字符串,根据分割符 \t 分割:

1
2
3
>>> s = 'id\tname\taddress'
>>> s.split('\t')
['id', 'name', 'address']

但是,对于分隔符复杂的字符串,split 函数就无能为力。

如下字符串,可能的分隔符有 ,;\t|

1
s = 'This,,,   module ; \t   provides|| regular ; '

正则字符串为:[,\s;|]+\s 匹配空白字符,正则分解图,如下:

image-20220408154014364

1
2
3
4
>>> s = 'This,,,   module ; \t   provides|| regular ; '
>>> words = re.split('[,\s;|]+', s)
>>> words
['This', 'module', 'provides', 'regular', '']

sub 替换匹配串

正则模块,sub 方法,替换匹配到的子串:

1
2
3
4
5
>>> content = 'hello 12345, hello 456321'
>>> pat = re.compile(r'\d+')
>>> m = pat.sub('666', content)
>>> m
'hello 666, hello 666'

compile 预编译

如果要用同一匹配模式,做很多次匹配,可以使用 compile 预先编译串。

案例:从一系列字符串中,挑选出所有正浮点数。

正则表达式为:^[1-9]\d*\.\d*|0\.\d*[1-9]\d*$,字符 a|b 表示 a 串匹配失败后,才执行 b 串,正则分解图见下:

image-20220408154036191

首先,生成预编译对象 rec:

1
2
>>> s = [-16, 'good', 1.5, 0.2, -0.1, '11.43', 10, '5e10']
>>> rec = re.compile(r'^[1-9]\d*\.\d*|0\.\d*[1-9]\d*$')

下面直接使用 rec,匹配列表中的每个元素,不用每次都预编译正则表达式,效率更高。

1
2
>>> [ i for i in s if rec.match(str(i))]
[1.5, 0.2, '11.43']

贪心捕获

正则模块中,根据某个模式串,匹配到结果。

待爬取网页的部分内容如下所示,现在想要提取 <div> 标签中的内容。

1
>>> content = '<h>ddedadsad</h><div>graph</div>aa<div>math</div>'

如果正则匹配串写做 <div>.*</div>

1
2
3
>>> result = re.findall(r'<div>.*</div>', content)
>>> result
['<div>graph</div>aa<div>math</div>']

看到返回结果后,如果我们不想保留字符串的<div>和结尾的</div>,那么,就需要使用一对()去捕获。

正则匹配串修改为:<div>(.*)</div>,只添加一对括号。

1
2
3
>>> result = re.findall(r'<div>(.*)</div>', content)
>>> result
['graph</div>aa<div>math']
  • 看到结果中已经没有了<div></div>仅使用一对括号,便成功捕获到我们想要的部分
  • (.*) 表示捕获任意多个字符,尽可能多地匹配字符,也被称为贪心捕获
  • (.*) 的正则分解图如下所示,. 表示匹配除换行符外的任意字符

image-20220408154102005

非贪心捕获

观察上面返回的结果 ['graph</div>aa<div>math'],如果只想要得到两个 <div></div> 间的内容,该怎么写正则表达式?

相比 (.*),仅多添加一个 ?,匹配串为 (.*?)

1
2
3
4
>>> content = '<h>ddedadsad</h><div>graph</div>aa<div>math</div>'
>>> result = re.findall(r'<div>(.*?)</div>', content)
>>> result
['graph', 'math']

终于得到 2 个 <div> 对间的内容。

这种匹配模式串 (.*?),被称为非贪心捕获。正则图中,红色虚线表示非贪心匹配。

image-20220408154121337