【Python Cookbook】进阶解析字符串与文本处理(三)
目录 |
案例 |
目录 |
案例 |
---|---|---|---|
字符串和文本(一) | 1.使用多个界定符分割字符串 2.字符串开头或结尾匹配 3.用 Shell 通配符匹配字符串 4.字符串匹配和搜索 5.字符串搜索和替换 |
字符串和文本(三) | 11.删除字符串中不需要的字符 12.审查清理文本字符串 13.字符串对齐 14.合并拼接字符串 15.字符串中插入变量 |
字符串和文本(二) | 6.字符串忽略大小写的搜索替换 7.最短匹配模式 8.多行匹配模式 9.将 Unicode 文本标准化 10.在正则式中使用 Unicode |
字符串和文本(四)
字符串和文本(五) |
(四)16.以指定列宽格式化字符串 (四)17.在字符串中处理 html 和 xml (四)18.字符串令牌解析 (四)19.字节字符串上的字符串操作 (五)20.实现一个简单的递归下降分析器 |
字符串和文本(三)
11.删除字符串中不需要的字符
你想去掉文本字符串开头,结尾或者中间不想要的字符,比如空白。
strip()
方法能用于删除 开始 或 结尾 的字符。lstrip()
和 rstrip()
分别从左和从右执行删除操作。默认情况下,这些方法会去除空白字符,但是你也可以指定其他字符。比如:
>>> # Whitespace stripping
>>> s = ' hello world \n'
>>> s.strip()
'hello world'
>>> s.lstrip()
'hello world \n'
>>> s.rstrip()
' hello world'
>>>
>>> # Character stripping
>>> t = '-----hello====='
>>> t.lstrip('-')
'hello====='
>>> t.strip('-=')
'hello'
这些 strip()
方法在读取和清理数据以备后续处理的时候是经常会被用到的。比如,你可以用它们来去掉空格,引号和完成其他任务。
但是需要注意的是去除操作不会对字符串的中间的文本产生任何影响。比如:
>>> s = ' hello world \n'
>>> s = s.strip()
>>> s
'hello world'
如果你想处理中间的空格,那么你需要求助其他技术。比如使用 replace()
方法或者是用正则表达式替换。示例如下:
>>> s.replace(' ', '')
'helloworld'
>>> import re
>>> re.sub('\s+', ' ', s)
'hello world'
🚀
re.sub('\s+', ' ', s)
:将所有连续的空白字符替换为单个空格。\s
:匹配任意空白字符(包括空格、\t
、\n
、\r
等)。+
:表示匹配前面的字符(这里是\s
)一次或多次(即连续多个空白字符)。
通常情况下你想将字符串 strip
操作和其他迭代操作相结合,比如从文件中读取多行数据。如果是这样的话,那么生成器表达式就可以大显身手了。比如:
with open(filename) as f:
lines = (line.strip() for line in f)
for line in lines:
print(line)
在这里,表达式 lines = (line.strip() for line in f)
执行数据转换操作。 这种方式非常高效,因为它不需要预先读取所有数据放到一个临时的列表中去。它仅仅只是创建一个生成器,并且每次返回行之前会先执行 strip
操作。
对于更高阶的 strip
,你可能需要使用 translate()
方法。
12.审查清理文本字符串
一些无聊的幼稚黑客在你的网站页面表单中输入文本 pýtĥöñ
,然后你想将这些字符清理掉。
文本清理问题会涉及到包括文本解析与数据处理等一系列问题。在非常简单的情形下,你可能会选择使用字符串函数(比如 str.upper()
和 str.lower()
)将文本转为标准格式。使用 str.replace()
或者 re.sub()
的简单替换操作能删除或者改变指定的字符序列。你同样还可以使用 unicodedata.normalize()
函数将 Unicode 文本标准化。
然后,有时候你可能还想在清理操作上更进一步。比如,你可能想消除整个区间上的字符或者去除变音符。为了这样做,你可以使用经常会被忽视的 str.translate()
方法。为了演示,假设你现在有下面这个凌乱的字符串:
>>> s = 'pýtĥöñ\fis\tawesome\r\n'
>>> s
'pýtĥöñ\x0cis\tawesome\r\n'
第一步是清理空白字符。为了这样做,先创建一个小的转换表格然后使用 translate()
方法:
>>> remap = {
... ord('\t') : ' ',
... ord('\f') : ' ',
... ord('\r') : None # Deleted
... }
>>> a = s.translate(remap)
>>> a
'pýtĥöñ is awesome\n'
正如你看的那样,空白字符 \t
和 \f
已经被重新映射到一个空格。回车字符 r
直接被删除。
你可以以这个表格为基础进一步构建更大的表格。比如,让我们删除所有的和音符:
>>> 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'
unicodedata.combining(chr(c))
:检查 Unicode 码点c
对应的字符是否为 组合字符(如音调符号´
、分音符¨
等)。如果是,返回True
。dict.fromkeys(...)
:将所有组合字符的 Unicode 码点作为键,值设为None
,生成一个字典。为后续translate()
方法提供需删除的字符映射表。unicodedata.normalize('NFD', a)
:将字符串a
转换为 Unicode 分解形式(NFD),即把带重音的字符(如ñ
)拆解为 基础字符 + 组合符号(n
+~
)。translate(cmb_chrs)
:根据字典cmb_chrs
的键(组合字符的码点),将字符串b
中所有匹配的字符替换为None
(即删除)。
上面例子中,通过使用 dict.fromkeys()
方法构造一个字典,每个 Unicode 和音符作为键,对应的值全部为 None 。
然后使用 unicodedata.normalize()
将原始输入标准化为分解形式字符。然后再调用 translate
函数删除所有重音符。同样的技术也可以被用来删除其他类型的字符(比如控制字符等)。
作为另一个例子,这里构造一个将所有 Unicode 数字字符映射到对应的 ASCII 字符上的表格:
>>> digitmap = { c: ord('0') + unicodedata.digit(chr(c))
... for c in range(sys.maxunicode)
... if unicodedata.category(chr(c)) == 'Nd' }
...
>>> len(digitmap)
460
>>> # Arabic digits
>>> x = '\u0661\u0662\u0663'
>>> x.translate(digitmap)
'123'
unicodedata.category(chr(c)) == 'Nd'
:检查 Unicode 码点 c
对应的字符是否属于 十进制数字(Nd
类别),例如:
١
(U+0661)१
(U+0661)1
(U+FF11)unicodedata.digit(chr(c))
:返回该数字字符的 数值(如 ١
返回 1,२
返回 2)。ord('0') + unicodedata.digit(chr(c))
:计算该数字对应的 ASCII 码点:
ord('0')
是 48(ASCII 的 0
)١
的数值是 1 → 48 + 1 = 49(即 1
的 ASCII 码点)x
是字符串 ١٢٣
(Unicode 码点 U+0661
,U+0662
,U+0663
)。translate(digitmap)
根据字典将每个字符替换为对应的 ASCII 数字:
١
(U+0661
)→ 1
٢
(U+0662
)→ 2
٣
(U+0663
)→ 3
另一种清理文本的技术涉及到 I/O 解码与编码函数。这里的思路是先对文本做一些初步的清理,然后再结合 encode()
或者 decode()
操作来清除或修改它。比如:
>>> a
'pýtĥöñ is awesome\n'
>>> b = unicodedata.normalize('NFD', a)
>>> b.encode('ascii', 'ignore').decode('ascii')
'python is awesome\n'
这里的标准化操作将原来的文本分解为单独的和音符。接下来的 ASCII 编码/解码只是简单的一下子丢弃掉那些字符。当然,这种方法仅仅只在最后的目标就是获取到文本对应 ACSII 表示的时候生效。
文本字符清理一个最主要的问题应该是运行的性能。一般来讲,代码越简单运行越快。对于简单的替换操作,str.replace()
方法通常是最快的,甚至在你需要多次调用的时候。比如,为了清理空白字符,你可以这样做:
def clean_spaces(s):
s = s.replace('\r', '')
s = s.replace('\t', ' ')
s = s.replace('\f', ' ')
return s
如果你去测试的话,你就会发现这种方式会比使用 translate()
或者正则表达式要快很多。
另一方面,如果你需要执行任何复杂字符对字符的重新映射或者删除操作的话,translate()
方法会非常的快。
从大的方面来讲,对于你的应用程序来说性能是你不得不去自己研究的东西。不幸的是,我们不可能给你建议一个特定的技术,使它能够适应所有的情况。因此实际情况中需要你自己去尝试不同的方法并评估它。
尽管这一节集中讨论的是文本,但是类似的技术也可以适用于字节,包括简单的替换,转换和正则表达式。
🚀 应用场景
国际化文本处理:将各种语言的数字统一为 ASCII 格式。 数据清洗:确保数字符号的一致性(如爬取多语言网页时)。 安全校验:防止混淆字符攻击(如用 1
(全角)冒充1
)。
13.字符串对齐
你想通过某种对齐方式来格式化字符串。
对于基本的字符串对齐操作,可以使用字符串的 ljust()
,rjust()
和 center()
方法。比如:
>>> text = 'Hello World'
>>> text.ljust(20)
'Hello World '
>>> text.rjust(20)
' Hello World'
>>> text.center(20)
' Hello World '
所有这些方法都能接受一个可选的填充字符。比如:
>>> text.rjust(20,'=')
'=========Hello World'
>>> text.center(20,'*')
'****Hello World*****'
函数 format()
同样可以用来很容易的对齐字符串。你要做的就是使用 <
,>
或者 ^
字符后面紧跟一个指定的宽度。比如:
>>> format(text, '>20')
' Hello World'
>>> format(text, '<20')
'Hello World '
>>> format(text, '^20')
' Hello World '
如果你想指定一个非空格的填充字符,将它写到对齐字符的前面即可:
>>> format(text, '=>20s')
'=========Hello World'
>>> format(text, '*^20s')
'****Hello World*****'
当格式化多个值的时候,这些格式代码也可以被用在 format()
方法中。比如:
>>> '{:>10s} {:>10s}'.format('Hello', 'World')
' Hello World'
format()
函数的一个好处是它不仅适用于字符串。它可以用来格式化任何值,使得它非常的通用。比如,你可以用它来格式化数字:
>>> x = 1.2345
>>> format(x, '>10')
' 1.2345'
>>> format(x, '^10.2f')
' 1.23 '
在老的代码中,你经常会看到被用来格式化文本的 %
操作符。比如:
>>> '%-20s' % text
'Hello World '
>>> '%20s' % text
' Hello World'
%-20s
:这是一个格式化字符串,其中:%
是格式化操作符的开始。-
表示左对齐(不加-
则默认右对齐)。20
表示最小字段宽度为 20 个字符。s
表示格式化的对象是一个字符串(str
类型)。
但是,在新版本代码中,你应该优先选择 format()
函数或者方法。format()
要比 %
操作符的功能更为强大。并且 format()
也比使用 ljust()
,rjust()
或 center()
方法更通用,因为它可以用来格式化任意对象,而不仅仅是字符串。
14.合并拼接字符串
你想将几个小的字符串合并为一个大的字符串。
如果你想要合并的字符串是在一个序列或者 iterable
中,那么最快的方式就是使用 join()
方法。比如:
>>> parts = ['Is', 'Chicago', 'Not', 'Chicago?']
>>> ' '.join(parts)
'Is Chicago Not Chicago?'
>>> ','.join(parts)
'Is,Chicago,Not,Chicago?'
>>> ''.join(parts)
'IsChicagoNotChicago?'
初看起来,这种语法看上去会比较怪,但是 join()
被指定为字符串的一个方法。这样做的部分原因是你想去连接的对象可能来自各种不同的数据序列(比如列表,元组,字典,文件,集合或生成器等), 如果在所有这些对象上都定义一个 join()
方法明显是冗余的。因此你只需要指定你想要的分割字符串并调用他的 join()
方法去将文本片段组合起来。
如果你仅仅只是合并少数几个字符串,使用加号(+
)通常已经足够了:
>>> a = 'Is Chicago'
>>> b = 'Not Chicago?'
>>> a + ' ' + b
'Is Chicago Not Chicago?'
加号(+
)操作符在作为一些复杂字符串格式化的替代方案的时候通常也工作的很好,比如:
>>> print('{} {}'.format(a,b))
Is Chicago Not Chicago?
>>> print(a + ' ' + b)
Is Chicago Not Chicago?
如果你想在源码中将两个字面字符串合并起来,你只需要简单的将它们放到一起,不需要用加号。比如:
>>> a = 'Hello' 'World'
>>> a
'HelloWorld'
字符串合并可能看上去并不需要用一整节来讨论。但是不应该小看这个问题,程序员通常在字符串格式化的时候因为选择不当而给应用程序带来严重性能损失。
最重要的需要引起注意的是,当我们使用加号操作符去连接大量的字符串的时候是非常低效率的,因为加号连接会引起内存复制以及垃圾回收操作。特别的,你永远都不应像下面这样写字符串连接代码:
s = ''
for p in parts:
s += p
这种写法会比使用 join()
方法运行的要慢一些,因为每一次执行 +=
操作的时候会创建一个新的字符串对象。你最好是先收集所有的字符串片段然后再将它们连接起来。
一个相对比较聪明的技巧是利用生成器表达式转换数据为字符串的同时合并字符串,比如:
>>> data = ['ACME', 50, 91.1]
>>> ','.join(str(d) for d in data)
'ACME,50,91.1'
同样还得注意不必要的字符串连接操作。有时候程序员在没有必要做连接操作的时候仍然多此一举。比如在打印的时候:
print(a + ':' + b + ':' + c) # Ugly
print(':'.join([a, b, c])) # Still ugly
print(a, b, c, sep=':') # Better
当混合使用 I/O 操作和字符串连接操作的时候,有时候需要仔细研究你的程序。比如,考虑下面的两端代码片段:
# Version 1 (string concatenation)
f.write(chunk1 + chunk2)
# Version 2 (separate I/O operations)
f.write(chunk1)
f.write(chunk2)
如果两个字符串很小,那么第一个版本性能会更好些,因为 I/O 系统调用天生就慢。另外一方面,如果两个字符串很大,那么第二个版本可能会更加高效,因为它避免了创建一个很大的临时结果并且要复制大量的内存块数据。还是那句话,有时候是需要根据你的应用程序特点来决定应该使用哪种方案。
最后谈一下,如果你准备编写构建大量小字符串的输出代码,你最好考虑下使用生成器函数,利用 yield
语句产生输出片段。比如:
def sample():
yield 'Is'
yield 'Chicago'
yield 'Not'
yield 'Chicago?'
这种方法一个有趣的方面是它并没有对输出片段到底要怎样组织做出假设。例如,你可以简单的使用 join()
方法将这些片段合并起来:
text = ''.join(sample())
或者你也可以将字符串片段重定向到 I/O:
for part in sample():
f.write(part)
再或者你还可以写出一些结合 I/O 操作的混合方案:
def combine(source, maxsize):
parts = []
size = 0
for part in source:
parts.append(part)
size += len(part)
if size > maxsize:
yield ''.join(parts)
parts = []
size = 0
yield ''.join(parts)
# 结合文件操作
with open('filename', 'w') as f:
for part in combine(sample(), 32768):
f.write(part)
这里的关键点在于原始的生成器函数并不需要知道使用细节,它只负责生成字符串片段就行了。
15.字符串中插入变量
你想创建一个内嵌变量的字符串,变量被它的值所表示的字符串替换掉。
Python 并没有对在字符串中简单替换变量值提供直接的支持。但是通过使用字符串的 format()
方法来解决这个问题。比如:
>>> s = '{name} has {n} messages.'
>>> s.format(name='Guido', n=37)
'Guido has 37 messages.'
或者,如果要被替换的变量能在变量域中找到,那么你可以结合使用 format_map()
和 vars()
。就像下面这样:
>>> name = 'Guido'
>>> n = 37
>>> s.format_map(vars())
'Guido has 37 messages.'
vars()
还有一个有意思的特性就是它也适用于对象实例。比如:
>>> class Info:
... def __init__(self, name, n):
... self.name = name
... self.n = n
...
>>> a = Info('Guido',37)
>>> s.format_map(vars(a))
'Guido has 37 messages.'
format
和 format_map()
的一个缺陷就是它们并不能很好的处理变量缺失的情况,比如:
>>> s.format(name='Guido')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'n'
一种避免这种错误的方法是另外定义一个含有 __missing__()
方法的字典对象,就像下面这样:
class safesub(dict):
"""防止key找不到"""
def __missing__(self, key):
return '{' + key + '}'
现在你可以利用这个类包装输入后传递给 format_map()
:
>>> del n # Make sure n is undefined
>>> s.format_map(safesub(vars()))
'Guido has {n} messages.'
如果你发现自己在代码中频繁的执行这些步骤,你可以将变量替换步骤用一个工具函数封装起来。就像下面这样:
import sys
def sub(text):
return text.format_map(safesub(sys._getframe(1).f_locals))
现在你可以像下面这样写了:
>>> name = 'Guido'
>>> n = 37
>>> print(sub('Hello {name}'))
Hello Guido
>>> print(sub('You have {n} messages.'))
You have 37 messages.
>>> print(sub('Your favorite color is {color}'))
Your favorite color is {color}
多年以来由于 Python 缺乏对变量替换的内置支持而导致了各种不同的解决方案。作为本节中展示的一个可能的解决方案,你可以有时候会看到像下面这样的字符串格式化代码:
>>> name = 'Guido'
>>> n = 37
>>> '%(name) has %(n) messages.' % vars()
'Guido has 37 messages.'
你可能还会看到字符串模板的使用:
>>> import string
>>> s = string.Template('$name has $n messages.')
>>> s.substitute(vars())
'Guido has 37 messages.'
然而,format()
和 format_map()
相比较上面这些方案而已更加先进,因此应该被优先选择。使用 format()
方法还有一个好处就是你可以获得对字符串格式化的所有支持(对齐,填充,数字格式化等待), 而这些特性是使用像模板字符串之类的方案不可能获得的。
本机还部分介绍了一些高级特性。映射或者字典类中鲜为人知的 __missing__()
方法可以让你定义如何处理缺失的值。在 SafeSub 类中,这个方法被定义为对缺失的值返回一个占位符。你可以发现缺失的值会出现在结果字符串中(在调试的时候可能很有用),而不是产生一个 KeyError 异常。
sub()
函数使用 sys._getframe(1)
返回调用者的栈帧。可以从中访问属性 f_locals
来获得局部变量。毫无疑问绝大部分情况下在代码中去直接操作栈帧应该是不推荐的。但是,对于像字符串替换工具函数而言它是非常有用的。另外,值得注意的是 f_locals
是一个复制调用函数的本地变量的字典。尽管你可以改变 f_locals
的内容,但是这个修改对于后面的变量访问没有任何影响。所以,虽说访问一个栈帧看上去很邪恶,但是对它的任何操作不会覆盖和改变调用者本地变量的值。
作者:G皮T