0%

python cookbook(02):字符串和文本

这篇文章将学习 Python 中字符串/文本操作常见编码技巧。

使用多个界定符分隔字符串

问题

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

解决方案

string 对象的 split() 方法只适用于非常简单的字符串分割情形,它不允许有多种分隔符,或者分割符周围有不确定的空格。需要灵活分割字符串的时候可以使用 re.split()

  • re.split() 允许为分隔符指定多个正则模式
  • 通过括号捕获分组,那么被匹配的文本也会出现在结果列表中
  • 如果不想保留分隔字符串到结果列表中,但仍然需要使用括号来分组正则表达式的话,可以使用非捕获分组,即 (?:...)

示例

1
2
3
4
>>> line = 'asdf fjdk; afed, fjek,asdf, foo'
>>> import re
>>> re.split('[;,\s]\s*', line)
['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']
1
2
3
4
5
6
7
8
9
10
11
12
>>> fields = re.split('(;|,|\s)\s*', line)
>>> values = fields[::2]
>>> delimiters = fields[1::2]
>>> fields
['asdf', ' ', 'fjdk', ';', 'afed', ',', 'fjek', ',', 'asdf', ',', 'foo']
>>> values
['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']
>>> delimiters
[' ', ';', ',', ',', ',']

>>> re.split(r'(?:,|;|\s)\s*', line)
['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']

字符串开头或结尾匹配

问题

需要检查字符串的开头或结尾是否匹配特定的文本模式。

解决方案

  • 检查字符串开头或结尾最简单的方法是使用 str.startswith() 或者 str.endswith()
  • 如果需要检查多种匹配,只需要将所有匹配项放到一个元组中去。注意,上面两个方法只接受元组作为参数,如果你使用的是其他可迭代类型,需要先通过 tuple() 将其转换为元组类型

示例

1
2
3
4
5
6
7
8
9
10
11
>>> filename = 'test.txt'
>>> filename.endswith('.txt')
True
>>> filename.startswith('file://')
False
>>> url.startswith('http:')
True
>>> [name for name in filenames if name.endswith(('.gz', '.py'))]
['t1k_bot.tar.gz', 'jemalloc-5.3.0.tar.gz', 't1k_master.tar.gz', 't.py']
>>> any(name.endswith(('.pcap', '.py')) for name in filenames)
True
1
2
3
4
5
6
7
8
>>> choices = ['http', 'https']
>>> url = 'http://www.baidu.com'
>>> url.startswith(choices)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: startswith first arg must be str or a tuple of str, not list
>>> url.startswith(tuple(choices))
True

用 Shell 通配符匹配字符串

问题

想使用 Unix Shell 中常用的通配符去匹配文本字符串:

解决方案

  • fnmatch(Unix filename pattern match)模块提供了两个函数:fnmatch()fnmatchcase() 可以实现该匹配
  • fnmatch() 使用底层操作系统的大小写敏感规则(不同操作系统不一样)来匹配模式。而 fnmatchcase() 则直接按照大小写敏感方式进行匹配
  • 在处理非文件名字符串的时候,这两个函数也很有用。因为他们的匹配能力介于 简单的字符串方法强大的正则表达式 之间。所以对于简单的通配操作,可以使用这两个函数
  • 如果要做文件名的匹配,最好使用 glob 模块

示例

1
2
3
4
5
6
7
8
9
10
11
12
>>> from fnmatch import fnmatch, fnmatchcase
>>> fnmatch('foo.txt', '*.txt')
True
>>> fnmatch('foo.txt', '?oo.txt')
True
>>> fnmatch('Dat45.csv', 'Dat[0-9]*')
True

>>> fnmatch('foo.txt', '*.TXT')
False
>>> fnmatchcase('foo.txt', '*.TXT')
False
1
2
3
4
5
6
7
8
9
10
11
12
>>> addresses = [
... '5412 N CLARK ST',
... '1060 W ADDISON ST',
... '1039 W GRANVILLE AVE',
... '2122 N CLARK ST',
... '4802 N BROADWAY',
... ]
>>>
>>> [addr for addr in addresses if fnmatch(addr, "* ST")]
['5412 N CLARK ST', '1060 W ADDISON ST', '2122 N CLARK ST']
>>> [addr for addr in addresses if fnmatch(addr, '54[0-9][0-9] *CLARK*')]
['5412 N CLARK ST']

字符串匹配和搜索

问题

想匹配或者搜索特定模式的文本。

解决方案

  • 如果想匹配的是字面字符串,只需要使用基本的字符串方法即可,例如 str.find()str.endswith()str.startswith()
  • 对于复杂的匹配,需要使用正则表达式,即 re 模块
  • 在使用 re 模块时,如果想使用同一个模式做多次匹配,应该现将模式字符串预编译成模式对象
  • match() 方法总是从字符串开始去匹配,如果想查找字符串任意部分的模式出现位置,使用 findall() 方法去替代。findall() 方法会搜索文本并以列表形式返回所有匹配,如果想以迭代方式返回匹配,可以使用 finditer() 方法来替代
  • 当定义正则表达式的时候,通常会用括号去捕获分组,之后可以将每个组的内容提取出来
  • 当编写正则表达式字符串的时候,普遍方法是使用原始字符串,因为这时不用对反斜杠(在正则表达式中很常用)做特殊处理

示例

1
2
3
4
5
6
7
8
9
>>> text = 'yeah, but no, but yeah, but no, but yeah'
>>> text == 'yeah'
False
>>> text.startswith('yeah')
True
>>> text.endswith('no')
False
>>> text.find('no')
10
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
32
33
34
>>> 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

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

>>> text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> datepat.findall(text)
['11/27/2012', '3/13/2013']
>>> for date in datepat.finditer(text):
... print(date.group(0))
...
11/27/2012
3/13/2013
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> datepat = re.compile(r'(\d+)/(\d+)/(\d+)')
>>> m = datepat.match('11/27/2012')
>>> m.group(0)
'11/27/2012'
>>> m.group(1)
'11'
>>> m.group(2)
'27'
>>> m.group(3)
'2012'

>>> text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> datepat.findall(text)
[('11', '27', '2012'), ('3', '13', '2013')]
>>> for m, d, y in datepat.findall(text):
... print('{}-{}-{}'.format(y, m, d))
...
2012-11-27
2013-3-13

字符串搜索和替换

问题

想在字符串中实现搜索和替换。

解决方案

  • 对于简单的字面模式替换,直接使用 str.replace() 方法即可
  • 对于复杂的模式,则可以使用 re 模块的 sub 函数,它的第一个参数是被匹配的模式,第二个参数则是替换模式。如果使用了命名分组,那么第二个参数可以使用 \g<group_name>
  • 如果想使用相同的模式多多次替换,可以先编译它来提升性能
  • 对于复杂的替换,sub 函数的第二个参数可以使用一个 替换回调函数 来替代。替换回调函数 的参数是一个 match 对象,即 match()find() 等返回的对象,可以使用 group() 方法来提取特定的匹配
  • 除了想执行替换操作,还想知道发生了多少次替换,可以使用 re.subn() 来代替

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> text = 'yeah, but no, but yeah, but no, but yeah'
>>> text.replace('yeah', 'yep')
'yep, but no, but yep, but no, but yep'

>>> 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.'

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

>>> re.sub(r'(?P<month>\d+)/(?P<day>\d+)/(?P<year>\d+)', r'\g<year>-\g<month>-\g<day>', text)
'Today is 2012-11-27. PyCon starts 2013-3-13.'

>>> re.subn(r'(?P<month>\d+)/(?P<day>\d+)/(?P<year>\d+)', r'\g<year>-\g<month>-\g<day>', text)
('Today is 2012-11-27. PyCon starts 2013-3-13.', 2)
1
2
3
4
5
6
7
>>> from calendar import month_abbr
>>> def change_date(m):
... mon_name = month_abbr[int(m.group(1))]
... return '{} {} {}'.format(m.group(2), mon_name, m.group(3))
...
>>> datepat.sub(change_date, text)
'Today is 27 Nov 2012. PyCon starts 13 Mar 2013.'

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

问题

以忽略大小写的方式搜索与替换文本字符串。

解决方案

  • 如果想在文本操作时忽略大小写,re 模块为这些操作提供了 re.IGNORECASE 标志参数
  • 但是需要注意,替换字符串并不会自动跟被匹配字符串的大小写保持一致。如果你想实现该功能,可以为 sub 提供一个 替换回调函数

示例

1
2
3
4
5
>>> text = 'UPPER PYTHON, lower python, Mixed Python'
>>> re.findall('python', text, flags=re.IGNORECASE)
['PYTHON', 'python', 'Python']
>>> re.sub('python', 'snake', text, flags=re.IGNORECASE)
'UPPER snake, lower snake, Mixed snake'
1
2
3
4
5
6
7
8
9
10
11
12
def matchcase(word):
def replace(m):
text = m.group()
if text.isupper():
return word.upper()
elif text.islower():
return word.lower()
elif text[0].isupper():
return word.capitalize()
else:
return word
return replace
1
2
3
>>> re.sub('python', matchcase('snake'), text, flags=re.IGNORECASE)
'UPPER SNAKE, lower snake, Mixed Snake'
>>>

最短模式匹配

问题

使用正则表达式匹配某个文本模式时,想实现最短匹配模式。

解决方案

在正则表达式中,* 操作符是贪婪的,因此匹配操作会查找最长的可能匹配。如果想让匹配变成非贪婪模式,可以在 * 操作符后面加上 ? 修饰符,从而得到最短匹配。? 也适用于 + 操作符,可以强制匹配算法改成寻找最短的可能匹配。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> import re
>>> str_pat = re.compile(r'"(.*)"')
>>> text1 = 'Computer says "no."'
>>> str_pat.findall(text1)
['no.']
>>> text2 = 'Computer says "no." Phone says "yes."'
>>> str_pat.findall(text2)
['no." Phone says "yes.']
>>>
>>> str_pat =re.compile(r'"(.*?)"')
>>> str_pat.findall(text1)
['no.']
>>> str_pat.findall(text2)
['no.', 'yes.']

多行匹配模式

问题

当使用正则表达式去匹配一大块文本时,需要跨越多行进行匹配。

解决方案

  • 需要牢记,. 可以匹配除了换行符以外的任何字符。如果想实现跨行匹配,可以在模式字符串中增加对换行的支持
  • 另外,re.compile 函数接受一个标志参数:re.DOTALL,让可以让正则表达式中的 . 匹配包括换行符在内的任意字符
  • 对于简单的情况,re.DOTALL 标记参数工作得很好,但是如果模式非常复杂,或者是为了构造字符串令牌而将多个模式合并起来,这个时候最好还是定义自己的正则表达式模式

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> import re
>>> comment = re.compile(r'/\*(.*?)\*/')
>>> text1 = '/* this is a comment */'
>>> text2 = '''/* this is a
... multiple comment */
... '''
>>> comment.findall(text1)
[' this is a comment ']
>>> comment.findall(text2)
[]

>>> comment = re.compile(r'/\*((?:.|\n)*?)\*/')
>>> comment.findall(text1)
[' this is a comment ']
>>> comment.findall(text2)
[' this is a\nmultiple comment ']

>>> comment = re.compile(r'/\*(.*?)\*/', re.DOTALL)
>>> comment.findall(text1)
[' this is a comment ']
>>> comment.findall(text2)
[' this is a\nmultiple comment ']

将 Unicode 文本标准化

问题

你正在处理 Unicode 字符串,需要确保所有字符串在底层有相同的表示。

解决方案

在 Unicode 中,某些字符能够用多个合法的编码表示。在需要比较字符串的程序中,使用字符的多种表示会产生问题。为了纠正该问题,可以使用 unicodedata 模块先将文本标准化。

  • normalize() 第一个参数指定字符串标准化方式。NFC 表示字符应该是整体组成的,NFD 表示字符应该分解为多个组合字符表示
  • Python 也支持扩展的标准化形式 NFKCNFKD,它们在处理某些字符时增加了额外的兼容特性
  • unicodedata 模块也提供了测试字符类的工具函数

示例

1
2
3
4
5
6
7
8
9
10
11
12
>>> s1 = 'Spicy Jalape\u00f1o'
>>> s2 = 'Spicy Jalapen\u0303o'
>>> s1
'Spicy Jalapeño'
>>> s2
'Spicy Jalapeño'
>>> s1 == s2
False
>>> len(s1)
14
>>> len(s2)
15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> import unicodedata
>>> t1 = unicodedata.normalize('NFC', s1)
>>> t2 = unicodedata.normalize('NFC', s2)
>>> t1 == t2
True
>>> print(ascii(t1))
'Spicy Jalape\xf1o'

>>> t3 = unicodedata.normalize('NFD', s1)
>>> t4 = unicodedata.normalize('NFD', s2)
>>> t3 == t4
True
>>> print(ascii(t3))
'Spicy Jalapen\u0303o'

>>> t1 = unicodedata.normalize('NFD', s1)
>>> ''.join(c for c in t1 if not unicodedata.combining(c))
'Spicy Jalapeno'

在正则表达式中使用 Unicode

问题

在使用正则表达式处理文本时,需要对 Unicode 字符处理。

解决方案

默认情况下,re 模块已经对一些 Unicode 字符类有了基本的支持:

  • 如果你想在模式中包含指定的 Unicode 字符,可以使用 Unicode 字符对应的转义字符序列(例如 \uFFF 等)
  • 当执行匹配和搜索操作时,最好先标准化并且清理所有文本为标准化格式。

混合使用 Unicode 和正则表达式比较复杂,如果真要这样做,最好考虑安装第三方正则式库,它们可以为 Unicode 的大小写转换等特性提供全面支持。

示例

1
2
3
4
5
6
>>> import re
>>> num = re.compile('\d+')
>>> num.match('123')
<re.Match object; span=(0, 3), match='123'>
>>> num.match('\u0661\u0662\u0663')
<re.Match object; span=(0, 3), match='١٢٣'>
1
2
3
4
5
6
7
>>> pat = re.compile('stra\u00dfe', re.IGNORECASE)
>>> s = 'straße'
>>> pat.match(s)
<re.Match object; span=(0, 6), match='straße'>
>>> pat.match(s.upper())
>>> s.upper()
'STRASSE'

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

问题

想去除字符串开头、中间或结尾不想要的字符。

解决方案

  • strip() 方法可以删除开始或结尾的字符。lstrip()rstrip() 分别从左和右执行删除操作。默认情况下,这些方法会去除空白字符,但是也可以指定其他字符
  • 如果想去除中间的字符,可以使用 replace() 或正则表达式执行替换

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> s = ' hello world \n'
>>> s.strip()
'hello world'
>>> s.lstrip()
'hello world \n'
>>> s.rstrip()
' hello world'

>>> t = '-----hello====='
>>> t.lstrip('-')
'hello====='
>>> t.rstrip('=')
'-----hello'
1
2
3
4
5
6
7
8
>>> s = ' hello     world \n'
>>> s.strip()
'hello world'
>>> s.replace(' ', '')
'helloworld\n'
>>> import re
>>> re.sub('\s+', '', s)
'helloworld'
1
2
3
4
5
>>> with open(filename) as f:
... lines = (line.strip() for line in f)
... for line in lines:
... print(line)
...

审查清理文本字符串

问题

想要对输入的文本字符串进行审查清理。

解决方案

文本清理主要包括文本解析和数据处理等一系列问题。对于一些简单情形,可以使用 str.upper()str.lower() 将文本转换为标准格式。使用 str.replace()re.sub() 等函数执行删除、替换操作。unicodedata.normalize() 可以将 unicode 文本标准化。

如果你需要执行任何复杂的 字符对字符的重新映射删除操作translate() 方法也是一个选择。

另外一种清理文本的技术涉及到 I/O 解码与编码函数。这里的思路是先对文本做一些初步的清理,然后再结合 encode()decode() 操作来清楚或修改它。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> s = 'pýtĥöñ\fis\tawesome\r\n'
>>> s
'pýtĥöñ\x0cis\tawesome\r\n'
>>> remap = {
... ord('\t'): ' ',
... ord('\f'): ' ',
... ord('\r'): None
... }
>>> a = s.translate(remap)
>>> a
'pýtĥöñ is awesome\n'

>>> import unicodedata
>>> import sys
>>>
>>> cmb_chrs = dict.fromkeys(c for c in range(sys.maxunicode) if unicodedata.combining(chr(c)))
>>> b = unicodedata.normalize("NFD", a)
>>> b
'pýtĥöñ is awesome\n'
>>> b.translate(cmb_chrs)
'python is awesome\n'
1
2
3
4
5
6
7
8
9
10
>>> digitmap = { c: ord('0') + unicodedata.digit(chr(c)) for c in range(sys.maxunicode) if unicodedata.category(chr(c)) == "Nd" }
>>> len(digitmap)
650
>>> x = '\u0661\u0662\u0663'
File "<stdin>", line 1
x = '\u0661\u0662\u0663'
IndentationError: unexpected indent
>>> x = '\u0661\u0662\u0663'
>>> x.translate(digitmap)
'123'
1
2
3
4
5
>>> a
'pýtĥöñ is awesome\n'
>>> b = unicodedata.normalize("NFD", a)
>>> b.encode('ascii', 'ignore').decode('ascii')
'python is awesome\n'

字符串对齐

问题

想通过某种对齐方式来格式化字符串。

解决方案

  • 对于基本的字符串对齐操作,可以使用字符串的 ljust()rjust()center() 方法,这些方法都能接受一个可选的填充字符
  • 函数 format() 也可以用来对齐字符串。需要使用 <、>、^ 字符后面紧跟一个指定的宽度。如果想指定非空格的填充字符,将它写到对齐字符的前面即可
  • format() 函数的好处之一是它不仅适用于字符串,还可以用于格式化任何值。
  • 当需要格式化多个值的时候,这些格式化代码也可以用于 format() 方法

在一些老的代码中,经常会使用格式化文本的 % 操作符。但是新版本代码中,应该优先选择 format() 函数或者方法,它比 % 操作符更为强大。并且 format() 也比 ljust()rjust()center() 方法更为通用。因为它可以用来格式化任何对象,而不仅仅是字符串。

示例

1
2
3
4
5
6
7
8
9
10
11
>>> text = "hello world"
>>> text.ljust(20)
'hello world '
>>> text.rjust(20)
' hello world'
>>> text.center(20)
' hello world '
>>> text.ljust(20, '=')
'hello world========='
>>> text.rjust(20, '*')
'*********hello world'
1
2
3
4
5
6
7
8
9
10
>>> format(text, "<20")
'hello world '
>>> format(text, ">20")
' hello world'
>>> format(text, "^20")
' hello world '
>>> format(text, "=<20")
'hello world========='
>>> format(text, "*^20")
'****hello world*****'
1
2
3
4
>>> '{:>10s} {:>10s}'.format("hello", "world")
' hello world'
>>> '{:>10} {:>10.2f}'.format(10.1, 10.2)
' 10.1 10.20'

合并拼接字符串

问题

想把几个小的字符串合并为一个大的字符串。

解决方案

  • 如果只是合并少数字符串,使用 + 即可。需要注意,使用 + 去连接大量字符串是非常低效的,因为 + 会引起内存复制以及垃圾回收操作
  • 如果想要合并的字符串是在一个序列或者 iterable 中,可以直接使用 join 方法。此时只需要指定你想要的分隔字符串,并调用其 join() 方法即可将文本片段组合起来
  • 如果想在源代码中将两个字面字符串合并起来,只需要简单地将它们放到一起,不需要使用 +
  • 同时需要注意,避免不必要的字符串连接操作
  • 当混合使用 I/O 操作和字符串连接操作的时候,有时候需要仔细研究程序。

另外一个小技巧,当你准备编写构建大量小字符串的输出代码时,最好考虑使用生成器函数,利用 yield 语句产生输出片段。这种方法的优点是,它并没有对输出片段到底要怎么组织做出假设。

示例

1
2
3
4
>>> a = 'Is Chicago'
>>> b = 'Not Chicago?'
>>> a + ' ' + b
'Is Chicago Not Chicago?'
1
2
3
4
5
6
7
>>> parts = ['Is', 'Chicago', 'Not', 'Chicago?']
>>> ' '.join(parts)
'Is Chicago Not Chicago?'
>>> ','.join(parts)
'Is,Chicago,Not,Chicago?'
>>> ''.join(parts)
'IsChicagoNotChicago?'
1
2
3
>>> a = 'Is Chicago' 'Not Chicago?'
>>> a
'Is ChicagoNot Chicago?'
1
2
3
4
>>> a = 'Is Chicago'
>>> b = 'Not Chicago?'
>>> '{} {}'.format(a, b)
'Is Chicago Not Chicago?'
1
2
3
>>> data = ['ACME', 50, 91.1]
>>> ','.join(str(i) for i in data)
'ACME,50,91.1'
1
2
3
4
5
6
7
8
9
10
>>> a = "10"
>>> b = "20"
>>> c = "30"
>>>
>>> print(a + ':' + b + ':' + c)
10:20:30
>>> print(':'.join((a, b, c)))
10:20:30
>>> print(a, b, c, sep=':')
10:20:30
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
def sample():
yield 'Is'
yield 'Chicago'
yield 'Not'
yield 'Chicago?'


def combine(source, max_size):
parts = []
size = 0

for part in source:
parts.append(part)
size += len(part)

if size > max_size:
yield ''.join(parts)
parts = []
size = 0

yield ''.join(parts)


with open('filename', 'w') as f:
for part in combine(sample(), 32768):
f.write(part)

字符串中插入变量

问题

想创建一个内建变量的字符串,变量被它的值所表示的字符串替换。

解决方案

  • Python 并没有对在字符串中简单替换变量值提供直接支持。但是通过使用字符串的 format() 方法来解决该问题。
  • 如果要被替换的变量能够在变量域中找到,可以结合使用 format_map()vars()vars() 也适用于对象实例
  • format()format_map() 的一个缺点是它们并不能很好地处理变量缺失的情况。避免该错误的方法是另外定义一个含有 __missiing__() 方法的字典对象
  • 相比于格式化字符串、字符串模版,format()format_map() 更加先进,应该被优先选择。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> s = '{name} has {n} messages'
>>> s.format(name="you", n=38)
'you has 38 messages'

>>> name="Gunio"
>>> n = 37
>>> s.format_map(vars())
'Gunio has 37 messages'

>>> class Info:
... def __init__(self, name, n):
... self.name = name
... self.n = n
...
>>> s.format_map(vars(Info("Mike", 10)))
'Mike has 10 messages'
1
2
3
4
5
6
7
8
9
10
11
>>> s.format(name="Jim")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'n'

>>> class safesub(dict):
... def __missing__(self, key):
... return '{' + key + '}'
...
>>> s.format_map(safesub({'name': 'Jim'}))
'Jim has {n} messages'
1
2
3
4
5
6
7
8
9
10
11
12
13
>>> import sys
>>> def sub(text):
... return text.format_map(safesub(sys._getframe(1).f_locals))
...

>>> name = 'test'
>>> n = 10
>>> print(sub('Hello {name}'))
Hello test
>>> print(sub('You have {n} messages'))
You have 10 messages
>>> print(sub('Your favoriate color is {color}'))
Your favoriate color is {color}

以指定列宽格式化字符串

问题

对于一些长字符串,想要一指定列宽将它们重新格式化。

解决方案

  • 可以使用 textwrap 模块来格式化字符串的输出
  • textwrap 对字符串打印是非常有用的,特别是当你希望输出自动匹配终端大小的时候,可以首先使用 os.get_terminal_size() 来获取终端的大小
  • textwrap 还提供很多工具函数,例如 shorten()dedent()

示例

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
>>> 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."
>>>
>>> import textwrap
>>> 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, 40))
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, 40, 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.

>>> print(textwrap.fill(s, 40, subsequent_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.
1
2
3
4
5
6
7
>>> import os
>>> os.get_terminal_size()
os.terminal_size(columns=150, lines=43)
>>> os.get_terminal_size().columns
150
>>> print(textwrap.fill(s, os.get_terminal_size().columns))
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.

在字符串中处理 html 和 xml

问题

你想将 HTML 或 XML 实体替换为对应的文本。或者,你需要转换文本中特定的字符。

解决方案

  • 如果你想替换文本字符串中的 < 或者 >,使用 html.escape() 函数可以很容易完成
  • 如果你正在处理 ASCII 文本,并且想将非 ASCII 文本对应的编码实体嵌入进去,可以给某些 I/O 函数传递参数 errors='xmlcharrefreplace' 来达到该目的
  • 如果想将含有编码值的原始文本进行手动替换,只需要使用 HTML 或者 XML 解析器的一些工具函数/方法即可

示例

1
2
3
4
5
6
7
8
>>> s = 'Elements are written as "<tag>text</tag>".'
>>> import html
>>> print(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;".
1
2
3
>>> s = 'Spicy Jalapeño'
>>> s.encode('ascii', errors='xmlcharrefreplace')
b'Spicy Jalape&#241;o'
1
2
3
4
5
6
s = 'Spicy &quot;Jalape&#241;o&quot.'
>>> s = 'Spicy Jalapeño'
>>> t = 'The prompt is &gt;&gt;&gt;'
>>> from xml.sax.saxutils import unescape
>>> unescape(t)
'The prompt is >>>'

字符串令牌解析

问题

对于一个字符串,想从左到右将其解析为一个令牌流。

解决方案

为了令牌化字符串,不仅需要匹配模式,还需要指定模式的类型。我们可以使用命名捕获组的正则表达式来定义所有可能出现的令牌。模式对象提供一个 scanner() 方法,该方法会创建一个 scanner 对象,在该对象上不断调用 match() 方法会一步步的扫描文本,每步一个匹配。

通常令牌化是许多高级文本解析与处理的第一步。为了对文本进行扫描,需要牢记以下几点:

  • 需要确认正则表达式指定了输入中所有可能出现的文本序列,如果有任何不可匹配的文本出现,则扫描会停止
  • 令牌的顺序也是有影响的。如果一个模式恰好是另一个更长模式的子字符串,需要确定长模式写在前面
  • 需要确保模式的准确性

更高阶的令牌化技术,可以通过 PyParsing 或者 PLY 包实现。

示例

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
#!/usr/bin/env python


import re
from collections import namedtuple


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())


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+)'

pat = re.compile('|'.join([NAME, NUM, PLUS, TIMES, EQ, WS]))

text = 'foo = 23 + 42 * 10'
for tok in generate_tokens(pat, text):
print(tok)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# python tokens.py
Token(type='NAME', value='foo')
Token(type='WS', value=' ')
Token(type='EQ', value='=')
Token(type='WS', value=' ')
Token(type='NUM', value='23')
Token(type='WS', value=' ')
Token(type='PLUS', value='+')
Token(type='WS', value=' ')
Token(type='NUM', value='42')
Token(type='WS', value=' ')
Token(type='TIMES', value='*')
Token(type='WS', value=' ')
Token(type='NUM', value='10')

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

问题

你想根据一组语法规则解析文本并执行命令,或者构造一个代表输入的抽象语法树。如果语法非常简单,可以不去使用一些框架,而是自己写这个解析器。

解决方案

文本解析是一个很大的主题,通常编译器书籍里会有关于语法、解析算法等背景知识。编写一个下降解析器的整体思路是比较简单的:首先获得所有语法规则,然后将其转换为一个函数或者方法。方法的目的就是要么处理完语法规则,要么产生一个错误:

  • 如果规则中的下一个符号是另外一个语法规则的名字,那么调用同名方法即可。这也是下降的含义:控制下降到另一个语法规则中去
  • 如果规则中的下一个符号是个特殊符号,需要查找下一个令牌并确认是一个精确匹配
  • 如果规则中下一个富豪是一个可选项,需要针对每一种情况检查下一个令牌
  • 对于有重复部分的规则,重复动作通过一个 while 循环来实现
  • 一旦整个语法规则处理完成,每个方法会返回某种结果给调用者

对于复杂的语法,最好是选择某个解析工具 PyParsing 或者 PLY 来实现。此时你只需要为 Token 写正则表达式和规则匹配时的高阶处理函数即可,而实际的运行解析器、接受 Token 等底层动作已经被库函数实现了。

示例

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#!/usr/bin/env python


import re
import collections


NUM = r'(?P<NUM>\d+)'
PLUS = r'(?P<PLUS>\+)'
MINUS = r'(?P<MINUS>\-)'
TIMES = r'(?P<TIMES>\*)'
DIVIDE = r'(?P<DIVIDE>/)'
LPAREN = r'(?P<LPAREN>\()'
RPAREN = r'(?P<RPAREN>\))'
WS = r'(?P<WS>\s+)'

master_pat = re.compile('|'.join([NUM, PLUS, MINUS, TIMES,
DIVIDE, LPAREN, RPAREN, WS]))

Token = collections.namedtuple("Token", ['type', 'value'])


def generate_tokens(text):
scanner = master_pat.scanner(text)
for m in iter(scanner.match, None):
tok = Token(m.lastgroup, m.group())
if tok.type != 'WS':
yield tok


class ExpressionEvaluator:
def parse(self, text):
self.tokens = generate_tokens(text)
self.tok = None
self.nexttok = None
self._advance()
return self.expr()

def _advance(self):
self.tok, self.nexttok = self.nexttok, next(self.tokens, None)

def _accept(self, toktype):
if self.nexttok and self.nexttok.type == toktype:
self._advance()
return True
else:

exprval = self.term()
while self._accept('PLUS') or self._accept('MINUS'):
op = self.tok.type
right = self.term()
if op == 'PLUS':
exprval += right
elif op == 'MINUS':
exprval -= right
return exprval

def term(self):
termval = self.factor()
while self._accept('TIMES') or self._accept('DIVIDE'):
op = self.tok.type
right = self.factor()
if op == 'TIMES':
termval *= right
elif op == 'MINUS':
termval /= right
return termval

def factor(self):
if self._accept('NUM'):
return int(self.tok.value)
elif self._accept('LPAREN'):
exprval = self.expr()
self._expect('RPAREN')
return exprval
else:
raise SyntaxError('Expected NUMBER or LPAREN')


def decent_parser():
e = ExpressionEvaluator()
print(e.parse('2'))
print(e.parse('2 + 3'))
print(e.parse('2 + 3 * 4'))
print(e.parse('2 + (3 + 4) * 5'))


if __name__ == "__main__":
decent_parser()

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

问题

你想在字节字符串上执行普通的文本操作,例如移除、搜索、替换。

解决方案

字节字符串同样支持大部分和文本字符串一样的内置操作。这些操作也适用于字节数组。可以使用正则表达式去匹配字节字符串,但是正则表达式本身也必须是字节字符串。

关于字节字符串,有一些注意点:

  • 字节字符串的索引操作返回整数而不是单独字符
  • 字节字符串不会提供一个美观的字符串表示,也不能很好的打印出来。如果真要打印,需要先解码成一个文本字符串
  • 字节字符串不支持 format 方法。如果想格式化字节字符串,只能先试用标准的文本字符串,然后将其编码为字节字符串
  • 使用字节字符串可能会改变一些操作的含义,特别是那些和文件系统有关的操作。例如如果你使用一个编码为字节的文件名,而不是一个普通的文本字符串,会禁用文件名的编码/解码

建议:在处理文本的时候,就直接在程序中使用普通的文本字符串而不是字节字符串。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> data = b'hello, world'
>>> data[0:5]
b'hello'
>>> data.startswith(b'hello')
True
>>> data.split()
[b'hello,', b'world']
>>> data.replace(b'hello', b'Hello Cruel')
b'Hello Cruel, world'

>>> data = bytearray(b'hello, world')
>>> data[0:5]
bytearray(b'hello')
>>> data.startswith(b'hello')
True
>>> data.split()
[bytearray(b'hello,'), bytearray(b'world')]
>>> data.replace(b'hello', b'Hello Cruel')
bytearray(b'Hello Cruel, world')
1
2
3
4
>>> data = b'FOO:BAR,SPAM'
>>> import re
>>> re.split(b'[:,]', data)
[b'FOO', b'BAR', b'SPAM']
1
2
3
4
5
6
7
8
9
10
11
>>> s = 'hello world'
>>> s[0]
'h'
>>> bs = b'hello world'
>>> bs[0]
104

>>> print(bs)
b'hello world'
>>> print(bs.decode('ascii'))
hello world
1
2
3
4
5
6
7
8
9
>>> with open('jalape\xf1o.txt', 'w') as f:
... f.write('spicy')
...
5
>>> import os
>>> os.listdir('.')
['matchcase.py', 'parser.py', 'jalapeño.txt', 'combine.py', 'tokens.py', '__pycache__']
>>> os.listdir(b'.')
[b'matchcase.py', b'parser.py', b'jalape\xc3\xb1o.txt', b'combine.py', b'tokens.py', b'__pycache__']