第二章:字符串和文本

2.1 使用多个界定符分割字符串

将一个字符串分割为多个字段,但是分隔符 (还有周围的空格) 并不是固定的。

使用 re.split() 方法 :允许你为指定多个分隔符

1
2
3
4
5
6
import re

line = 'asdf fjdk; afed, fjek,asdf, foo'
x = re.split(r'[\s;,]\s*',line)
print(x)
# ['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']

注意:
如果加了分组,那么就会连分组一起保留:

1
2
3
4
5
6
7
import re

line = 'asdf fjdk; afed, fjek,asdf, foo'

x = re.split(r'([\s;,]\s*)',line)
print(x)
# ['asdf', ' ', 'fjdk', '; ', 'afed', ', ', 'fjek', ',', 'asdf', ', ', 'foo']

如果你不想保留分割字符串,但仍然需要使用到括号来分组正则表达式的话,确保你的分组是非捕获分组,形如 (?:…) 。

1
re.split(r'(?:,|;|\s)\s*', line)

2.2 字符串开头或结尾匹配

str.startswith() 或者是 str.endswith() 方法。

注意 : 参数可以是一个元组,表示的关系

1
2
3
4
5
6
string = 'trainingfile/training1.py'
print(string.endswith('.py'))

filenames = [ 'Makefile', 'foo.c', 'bar.py', 'spam.c', 'spam.h' ]
print([name for name in filenames if name.endswith(('.c', '.h'))])
# ['foo.c', 'spam.c', 'spam.h']

2.3 用 Shell 通配符匹配字符串

fnmatch 模块提供了两个函数——fnmatch() 和 fnmatchcase()

fnmatch() 函数匹配能力介于简单的字符串方法和强大的正则表达式之间。

1
2
3
4
5
from fnmatch import fnmatch, fnmatchcase

print(fnmatch('foo.txt', '*.txt')) # True
print(fnmatch('foo.txt', '?oo.txt')) # True
print(fnmatch('Dat45.csv', 'Dat[0-9]*')) # True

2.4 字符串匹配和搜索

str.find()
re.match()
re.search()
re.findall()
re.finditer()

2.5 字符串搜索和替换

str.replace()

1
2
3
4
5
text = 'yeah, but no, but yeah, but no, but yeah'

x = text.replace('but','...')
print(x)
# yeah, ... no, ... yeah, ... no, ... yeah

re.sub()

1
2
3
4
import re
text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
new = re.sub(r'(\d{1,2})/(\d{2})/(\d{4})',r'\1-\2-\3',text)
print(new)

re.sub甚至可以传入回调函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
import re
from calendar import month_abbr


datepat = re.compile(r'(\d+)/(\d+)/(\d+)')
text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'

def change_date(m):
mon_name = month_abbr[int(m.group(1))]
return '{} {} {}'.format(m.group(2), mon_name, m.group(3))

print(datepat.sub(change_date, text))
# Today is 27 Nov 2012. PyCon starts 13 Mar 2013.

re.subn()知道有发生了多少次替换

2.6 字符串忽略大小写的搜索替换

re.IGNORECASE 标志参数

1
2
3
4
5
6
import re

text = 'UPPER PYTHON, lower python, Mixed Python'

res = re.findall(r'python',text,re.I)
print(res) # ['PYTHON', 'python', 'Python']

2.7 最短匹配模式(非贪婪模式)

模式中的 * 操作符后面加上? 修饰符

1
2
str_pat = re.compile(r'\"(.*)\"')
str_pat = re.compile(r'\"(.*?)\"')

2.8 多行匹配模式

标志参数 re.DOTALL

1
2
3
4
5
6
7
8
import re

text = '''UPPER 7PYTHON, lower python, Mixed
Py7thon'''

res = re.findall(r'7(.*?)7',text,re.S)
print(res)
# ['PYTHON, lower python, Mixed \n\nPy']

2.9 将 Unicode 文本标准化

在 Unicode 中,某些字符能够用多个合法的编码表示,需要确保所有字符串在底层有相同的表示。

1
2
3
4
5
6
7
8
s1 = 'Spicy Jalape\u00f1o'
s2 = 'Spicy Jalapen\u0303o'

print(s1,s2) # Spicy Jalapeño Spicy Jalapeño
print(s1 == s2) # False
print(s1 is s2) # False

print(len(s1),len(s2)) # 14 15

使用 unicodedata 模块先将文本标准化

1
2
3
4
5
6
7
8
9
10
11
12
13
import unicodedata

s1 = 'Spicy Jalape\u00f1o'
s2 = 'Spicy Jalapen\u0303o'

s1 = unicodedata.normalize('NFC',s1)
s2 = unicodedata.normalize('NFC',s2)

print(s1,s2) # Spicy Jalapeño Spicy Jalapeño
print(s1 == s2) # True
print(s1 is s2) # False

print(len(s1),len(s2)) # 14 14

2.10 在正则式中使用 Unico

默认情况下 re 模块已经对一些 Unicode 字符类有了基本的支持。比如,\d 已经匹配任意的 unicode 数字字符

1
2
3
4
5
6
7
import re

num = re.compile('\d+')
print(num.match('123'))
# <re.Match object; span=(0, 3), match='123'>
print(num.match('\u0661\u0662\u0663'))
# <re.Match object; span=(0, 3), match='١٢٣'>

2.11 删除字符串中不需要的字符

strip()
lstrip()
rstrip()

注意 : strip()删除的是开始或结尾指定字符,不单单是空白

1
2
3
4
text = '12345678589'
print(text.strip('19')) # 234567858
print(text.lstrip('19')) # 2345678589
print(text.rstrip('19')) # 1234567858

2.12 审查清理文本字符串

你可能想消除整个区间上的字符或者去除变音符。使用str.translate()

str.translate():
构建一个映射表,让对应的字符串映射成另一个字符串

1
2
3
4
5
6
7
8
9
s = 'pýtĥöñ\fis\tawesome\r\n'
remap = {
ord('\t') : ' ',
ord('\f') : ' ',
ord('\r') : None # Deleted
}

a = s.translate(remap)
print(a) # pýtĥöñ is awesome

2.13 字符串对齐

ljust()
rjust()
center()

1
2
3
4
5
text = 'hyl'

print(text.ljust(20,'-')) # hyl-----------------
print(text.rjust(20,'-')) # -----------------hyl
print(text.center(20,'-')) # --------hyl---------

format()

1
2
3
4
text = 'hyl'

print(format(text,'>20s'))
# hyl

2.14 合并拼接字符串

join()

python的接口是鸭子类型,所以join不仅支持可迭代对象,还支持生成器

1
2
3
4
5
6
7
8
9
def gen():
for num in range(9):
yield str(num)

g = gen()

x = ''.join(g)
print(x)
# 012345678

当我们使用加号 (+) 操作符去连接大量的字符串的时候是非常低效率的,因为加号连接

  1. 引起内存复制
  2. 引起垃圾回收操作

下面的代码建议不能写:

1
2
3
4
s = ''
for p in parts:
s += p

而是应该先收集所有的字符串片段然后再将它们连接起来 :

1
s = ''.join(parts)

还可以使用生成器:

1
2
data = ['ACME', 50, 91.1]
','.join(str(d) for d in data)

2.15 字符串中插入变量

format()
format_map() : 待被替换的变量在变量域中找

1
2
3
4
5
6
name = 'hyl'
age = 21

text = 'my name is {name} , i am {age} year old'
print(text.format_map(vars()))
# my name is hyl , i am 21 year old

format 和 format_map() 的一个缺陷就是它们并不能处理变量缺失的情况

1
2
3
4
5
name = 'hyl'
age = 21

text = 'my name is {name} , i am {age} year old'
print(text.format(name='hyl')) # KeyError: 'age'

为此,我们可以定义一个继承dict,实现__messing__的类:

__messing__方法可以让你定义如何处理缺失的值。

1
2
3
4
5
6
7
8
9
10
class safesub(dict):
""" 防止 key 找不到"""
def __missing__(self, key):
return '{' + key + '}'

a = safesub()
print(a['XXX']) # {XXX}

s = '{name} has {n} messages.'
print(s.format_map(safesub(vars()))) # {name} has {n} messages.

我们甚至可以将其封装起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import sys


def sub(text):
return text.format_map(safesub(sys._getframe(1).f_locals))


class safesub(dict):
""" 防止 key 找不到"""
def __missing__(self, key):
return '{' + key + '}'


name = 'Guido'
n = 37
print(sub('Hello {name}')) # Hello Guido

这其实就是f-string

2.16 以指定列宽格式化字符串

有一些长字符串,想以指定的列宽将它们重新格式化。

使用 textwrap 模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import textwrap

s = '''Look into my eyes, look into my eyes, the eyes, the eyes,
the eyes, not around the eyes, don't look around the eyes,
look into my eyes, you're under.'''

print(textwrap.fill(s,70))
# Look into my eyes, look into my eyes, the eyes, the eyes, the eyes,
# not around the eyes, don't look around the eyes, look into my eyes,
# you're under.

print(textwrap.fill(s,70,initial_indent=' '))
# Look into my eyes, look into my eyes, the eyes, the eyes, the eyes,
# not around the eyes, don't look around the eyes, look into my eyes,
# you're under.

2.17 在字符串中处理 html 和 xml

  • 将 HTML 或者 XML 实体如 &entity; 或 &#code; 替换为对应的文本。
  • 转换文本中特定的字符 (比如 <, >, 或 &)。

html.escape()

1
2
3
4
5
6
7
8
9
10
import html

s = 'Elements are written as "<tag>text</tag>".'

print(html.escape(s))
# Elements are written as &quot;&lt;tag&gt;text&lt;/tag&gt;&quot;.

# 禁止转义引号
print(html.escape(s, quote=False))
# Elements are written as "&lt;tag&gt;text&lt;/tag&gt;".

2.18 字符串令牌解析

你有一个字符串,想从左至右将其解析为一个令牌流。

  • 令牌化是指在分隔符的基础上将一个字符串分割为若干个子字符串
  • 例如,分隔符;分割字符串ac;bd;def;e为四个子字符串ac,bd,def和e。
  • 模式对象有一个 scanner() 方法
  • 这个方法会创建一个 scanner 对象,在这个对象上不断的调用 match() 方法会一步步的扫描目标文本,每步一个匹配。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import re

text = 'foo = 23 + 42 * 10'

tokens = [('NAME', 'foo'), ('EQ','='), ('NUM', '23'), ('PLUS','+'),
('NUM', '42'), ('TIMES', '*'), ('NUM', '10')]

NAME = r'(?P<NAME>[a-zA-Z_][a-zA-Z_0-9]*)'
NUM = r'(?P<NUM>\d+)'
PLUS = r'(?P<PLUS>\+)'
TIMES = r'(?P<TIMES>\*)'
EQ = r'(?P<EQ>=)'
WS = r'(?P<WS>\s+)'

print('|'.join([NAME, NUM, PLUS, TIMES, EQ, WS]))
# (?P<NAME>[a-zA-Z_][a-zA-Z_0-9]*)|(?P<NUM>\d+)|(?P<PLUS>\+)|(?P<TIMES>\*)|(?P<EQ>=)|(?P<WS>\s+)

master_pat = re.compile('|'.join([NAME, NUM, PLUS, TIMES, EQ, WS]))
scanner = master_pat.scanner('foo = 42')

print(scanner.match())
# <re.Match object; span=(0, 3), match='foo'>

print(scanner.match())
# <re.Match object; span=(3, 4), match=' '>

print(scanner.match())
# <re.Match object; span=(4, 5), match='='>

print(scanner.match())
# <re.Match object; span=(5, 6), match=' '>

所以我们可以将其转为生成器:

1
2
3
4
5
def generate_tokens(pat, text):
Token = namedtuple('Token', ['type', 'value'])
scanner = pat.scanner(text)
for m in iter(scanner.match, None):
yield Token(m.lastgroup, m.group())

2.19 实现一个简单的递归下降分析器

你想根据一组语法规则解析文本并执行命令,或者构造一个代表输入的抽象语法树。

(看不懂,略)

2.20 字节字符串上的字符串操作

在字节字符串上执行普通的文本操作 (比如移除,搜索和替换)。
字节字符串同样也支持大部分和文本字符串一样的内置操作

  • 字节字符串的索引操作返回整数而不是单独字符。

    1
    2
    3
    4
    5
    a = 'Hello World'
    a[0] # H

    b = b'Hello World'
    b[0] # 72
  • 字节字符串不会提供一个美观的字符串表示,也不能很好的打印出来,除
    非它们先被解码为一个文本字符串。

    1
    2
    3
    s = b'Hello World'
    print(s) # b'Hello World'
    print(s.decode('ascii')) # Hello World
  • 不存在任何适用于字节字符串的格式化操作