flyEn'blog

python cookbook学习笔记(二)

此文仅为学习python cookbook书籍时候记的笔记,记录过程权当加深记忆,无任何主观内容,若需参考可直接至此在线文档学习。

——字符串和文本

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

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

1
2
3
4
>>> line = 'asdf fjdk; afed, fjek,asdf, foo'
>>> import re
>>> re.split(r'[;,\s]\s*', line)
['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']

在上面的例子中,分隔符可以是逗号,分号或者是空格,并且后面紧跟着任意个的空格。
当你使用 re.split() 函数时候,需要特别注意的是正则表达式中是否包含一个括号捕获分组。

1
2
3
4
>>> fields = re.split(r'(;|,|\s)\s*', line)
>>> fields
['asdf', ' ', 'fjdk', ';', 'afed', ',', 'fjek', ',', 'asdf', ',', 'foo']
>>>

获取分割字符在某些情况下也是有用的。 比如,你可能想保留分割字符串,用来在后面重新构造一个新的输出字符串:

1
2
3
4
5
6
7
8
9
10
>>> values = fields[::2]
>>> delimiters = fields[1::2] + ['']
>>> values
['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']
>>> delimiters
[' ', ';', ',', ',', ',', '']
>>> # Reform the line using the same delimiters
>>> ''.join(v+d for v,d in zip(values, delimiters))
'asdf fjdk;afed,fjek,asdf,foo'
>>>

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

字符串开头或结尾匹配:str.startswith(),str.endswith()

1
2
3
4
5
6
7
8
9
>>> filename = 'spam.txt'
>>> filename.endswith('.txt')
True
>>> filename.startswith('file:')
False
>>> url = 'http://www.python.org'
>>> url.startswith('http:')
True
>>>

如果你想检查多种匹配可能,只需要将所有的匹配项放入到一个元组中去, 然后传给 startswith() 或者 endswith() 方法:

1
2
3
4
5
6
>>> import os
>>> filenames = os.listdir('.')
>>> filenames
[ 'Makefile', 'foo.c', 'bar.py', 'spam.c', 'spam.h' ]
>>> [name for name in filenames if name.endswith(('.c', '.h')) ]
['foo.c', 'spam.c', 'spam.h'

1
2
3
4
5
6
7
8
from urllib.request import urlopen

def read_data(name):
if name.startswith(('http:', 'https:', 'ftp:')):
return urlopen(name).read()
else:
with open(name) as f:
return f.read()

注:必须要输入一个元组作为参数。
类似的操作也可以使用切片来实现,但是代码看起来没有那么优雅。
可以能还想使用正则表达式去实现。这种方式也行得通,但是对于简单的匹配实在是有点小材大用了。
当和其他操作比如普通数据聚合相结合的时候 startswith() 和 endswith() 方法是很不错的。 比如,下面这个语句检查某个文件夹中是否存在指定的文件类型:

1
2
if any(name.endswith(('.c', '.h')) for name in listdir(dirname)):
...

用Shell通配符匹配字符串fnmatch.fnmatch()和fnmatchcase()

1
2
3
4
5
6
7
8
9
10
11
>>> from fnmatch import fnmatch, fnmatchcase
>>> fnmatch('foo.txt', '*.txt')
True
>>> fnmatch('foo.txt', '?oo.txt')
True
>>> fnmatch('Dat45.csv', 'Dat[0-9]*')
True
>>> names = ['Dat1.csv', 'Dat2.csv', 'config.ini', 'foo.py']
>>> [name for name in names if fnmatch(name, 'Dat*.csv')]
['Dat1.csv', 'Dat2.csv']
>>>

fnmatch() 函数使用底层操作系统的大小写敏感规则(不同的系统是不一样的)来匹配模式。fnmatchcase()完全使用你的模式大小写匹配。

在处理非文件名的字符串时候它们也是很有用的

1
2
3
4
5
6
7
addresses = [
'5412 N CLARK ST',
'1060 W ADDISON ST',
'1039 W GRANVILLE AVE',
'2122 N CLARK ST',
'4802 N BROADWAY',
]

1
2
3
4
5
6
>>> from fnmatch import fnmatchcase
>>> [addr for addr in addresses if fnmatchcase(addr, '* ST')]
['5412 N CLARK ST', '1060 W ADDISON ST', '2122 N CLARK ST']
>>> [addr for addr in addresses if fnmatchcase(addr, '54[0-9][0-9] *CLARK*')]
['5412 N CLARK ST']
>>>

字符串匹配和搜索

如果你想匹配的是字面字符串,那么你通常只需要调用基本字符串方法就行, 比如 str.find() , str.endswith() , str.startswith() 。
match() 总是从字符串开始去匹配,如果你想查找字符串任意部分的模式出现位置, 使用 findall() 方法去代替。

1
2
3
4
>>> text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> datepat.findall(text)
['11/27/2012', '3/13/2013']
>>>

在定义正则式的时候,通常会利用括号去捕获分组。

1
2
>>> datepat = re.compile(r'(\d+)/(\d+)/(\d+)')
>>>

捕获分组可以使得后面的处理更加简单,因为可以分别将每个组的内容提取出来。

1
2
3
4
5
6
7
8
>>> m = datepat.match('11/27/2012')
>>> m
<_sre.SRE_Match object at 0x1005d2750>
>>> # Extract the contents of each group
>>> m.group(0)
'11/27/2012'
>>> m.group(1)
'11'

字符串搜索和替换

str.repalce()

1
2
3
4
>>> 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() 函数。
为了说明这个,假设你想将形式为 11/27/2012 的日期字符串改成 2012-11-27 。

1
2
3
4
5
>>> 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 指向前面模式的捕获组号。
如果需要用相同的模式做多次替换,可考虑先编译它来提升性能。

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

对于更加复杂的替换,可以传递一个替换回调函数来代替。

1
2
3
4
5
6
7
8
>>> 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.'
>>>

一个替换回调函数的参数是一个 match 对象,也就是 match() 或者 find() 返回的对象。 使用 group() 方法来提取特定的匹配部分。回调函数最后返回替换字符串。
想知道有多少替换发生了,可以使用 re.subn() 来代替。

1
2
3
4
5
>>> newtext, n = datepat.subn(r'\3-\1-\2', text)
>>> newtext
'Today is 2012-11-27. PyCon starts 2013-3-13.'
>>> n
2

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

1
2
3
4
5
6
>>> 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
>>> 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.']

在正则表达式中操作符是贪婪的,因此匹配操作会查找最长的可能匹配。
为了修正这个问题,可以在模式中的
操作符后面加上?修饰符

1
2
3
4
>>> str_pat = re.compile(r'\"(.*?)\"')
>>> str_pat.findall(text2)
['no.', 'yes.']
>>>

这样就使得匹配变成非贪婪模式。

多行匹配模式

1
2
3
4
5
6
7
8
9
10
11
>>> comment = re.compile(r'/\*(.*?)\*/')
>>> text1 = '/* this is a comment */'
>>> text2 = '''/* this is a
... multiline comment */
... '''
>>>
>>> comment.findall(text1)
[' this is a comment ']
>>> comment.findall(text2)
[]
>>>

可以修改模式字符串,增加对换行的支持。

1
2
3
4
>>> comment = re.compile(r'/\*((?:.|\n)*?)\*/')
>>> comment.findall(text2)
[' this is a\n multiline comment ']
>>>

(?:.|\n)指定了一个非捕获组 (也就是它定义了一个仅仅用来做匹配,而不能通过单独捕获或者编号的组)。

re.compile() 函数接受一个标志参数叫 re.DOTALL ,在这里非常有用。 它可以让正则表达式中的点(.)匹配包括换行符在内的任意字符。比如:

1
2
3
>>> comment = re.compile(r'/\*(.*?)\*/', re.DOTALL)
>>> comment.findall(text2)
[' this is a\n multiline comment ']

将Unicode文本标准化: unicodedata

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> s1 = 'Spicy Jalape\u00f1o'
>>> s2 = 'Spicy Jalapen\u0303o'
>>> s1
'Spicy Jalapeño'
>>> s2
'Spicy Jalapeño'
>>> s1 == s2
False
>>> 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'
>>>

在正则式中使用Unicode

re模块已经对一些Unicode字符类有了基本的支持,\\d已经匹配任意的Unicode数字字符:

1
2
3
4
5
6
7
8
9
>>> import re
>>> num = re.compile('\d+')
>>> # ASCII digits
>>> num.match('123')
<_sre.SRE_Match object at 0x1007d9ed0>
>>> # Arabic digits
>>> num.match('\u0661\u0662\u0663')
<_sre.SRE_Match object at 0x101234030>
>>>

如果你想在模式中包含指定的Unicode字符,你可以使用Unicode字符对应的转义序列(比如\uFFF或者\UFFFFFFF )。 比如,下面是一个匹配几个不同阿拉伯编码页面中所有字符的正则表达式:

1
2
>>> arabic = re.compile('[\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff]+')
>>>

1
2
3
4
5
6
7
8
>>> pat = re.compile('stra\u00dfe', re.IGNORECASE)
>>> s = 'straße'
>>> pat.match(s) # Matches
<_sre.SRE_Match object at 0x10069d370>
>>> pat.match(s.upper()) # Doesn't match
>>> s.upper() # Case folds
'STRASSE'
>>>

删除字符串中不需要的字符: strip()

strip()方法能用于删除开始或结尾的字符。lstrip()rstrip()分别从左和从右执行删除操作。 默认情况下,这些方法会去除空白字符,但是你也可以指定其他字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> # 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'
>>>

但是需要注意的是去除操作不会对字符串的中间的文本产生任何影响。

1
2
3
4
5
>>> s = ' hello     world \n'
>>> s = s.strip()
>>> s
'hello world'
>>>

如果你想处理中间的空格,那么你需要求助其他技术。比如使用 replace() 方法或者是用正则表达式替换。

1
2
3
4
5
6
>>> s.replace(' ', '')
'helloworld'
>>> import re
>>> re.sub('\s+', ' ', s)
'hello world'
>>>

通常情况下你想将字符串 strip 操作和其他迭代操作相结合,比如从文件中读取多行数据。如果是这样的话,那么生成器表达式就可以大显身手了。

1
2
3
4
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)执行数据转换操作。

审查清理文本字符串: str.translate()

使用字符串函数(比如 str.upper()str.lower() )将文本转为标准格式。
使用 str.replace() 或者 re.sub()的简单替换操作能删除或者改变指定的字符序列。
使用unicodedata.normalize() 函数将unicode文本标准化。
如果想消除整个区间上的字符或者去除变音符。可以使用str.translate()方法。
另一种清理文本的技术涉及到I/O解码与编码函数。这里的思路是先对文本做一些初步的清理, 然后再结合 encode() 或者 decode() 操作来清除或修改它

1
2
3
4
5
6
 >>> a
'pýtĥöñ is awesome\n'
>>> b = unicodedata.normalize('NFD', a)
>>> b.encode('ascii', 'ignore').decode('ascii')
'python is awesome\n'
>>>

字符串对齐

对于基本的字符串对齐操作,ljust() , rjust() 和 center() 方法。

1
2
3
4
5
6
7
8
>>> text = 'Hello World'
>>> text.ljust(20)
'Hello World '
>>> text.rjust(20)
' Hello World'
>>> text.center(20)
' Hello World '
>>>

所有这些方法都能接受一个可选的填充字符。

1
2
3
4
5
>>> text.rjust(20,'=')
'=========Hello World'
>>> text.center(20,'*')
'****Hello World*****'
>>>

函数 format() 同样可以用来很容易的对齐字符串。 你要做的就是使用 <,> 或者 ^ 字符后面紧跟一个指定的宽度。

1
2
3
4
5
6
>>> format(text, '>20')
' Hello World'
>>> format(text, '<20')
'Hello World '
>>> format(text, '^20')
' Hello World

如果你想指定一个非空格的填充字符,将它写到对齐字符的前面即可:

1
2
3
4
5
>>> format(text, '=>20s')
'=========Hello World'
>>> format(text, '*^20s')
'****Hello World*****'
>>>

当格式化多个值的时候,这些格式代码也可以被用在 format() 方法中。

1
2
>>> '{:>10s} {:>10s}'.format('Hello', 'World')
' Hello World'

format() 函数的一个好处是它不仅适用于字符串。它可以用来格式化任何值,使得它非常的通用。 比如:数字。

1
2
3
4
5
>>> x = 1.2345
>>> format(x, '>10')
' 1.2345'
>>> format(x, '^10.2f')
' 1.23 '

format() 要比 % 操作符的功能更为强大。 并且 format() 也比使用 ljust() , rjust() 或 center() 方法更通用, 因为它可以用来格式化任意对象,而不仅仅是字符串。

合并拼接字符串

如果你想要合并的字符串是在一个序列或者 iterable 中,那么最快的方式就是使用 join()方法。

1
2
3
4
5
6
7
8
>>> parts = ['Is', 'Chicago', 'Not', 'Chicago?']
>>> ' '.join(parts)
'Is Chicago Not Chicago?'
>>> ','.join(parts)
'Is,Chicago,Not,Chicago?'
>>> ''.join(parts)
'IsChicagoNotChicago?'
>>>

如果你仅仅只是合并少数几个字符串,使用加号(+)通常已经足够了。

1
2
3
4
5
>>> a = 'Is Chicago'
>>> b = 'Not Chicago?'
>>> a + ' ' + b
'Is Chicago Not Chicago?'
>>>

利用生成器表达式(参考1.19小节)转换数据为字符串的同时合并字符串。

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

如果你准备编写构建大量小字符串的输出代码, 你最好考虑下使用生成器函数,利用yield语句产生输出片段。

1
2
3
4
5
def sample():
yield 'Is'
yield 'Chicago'
yield 'Not'
yield 'Chicago?'

这种方法一个有趣的方面是它并没有对输出片段到底要怎样组织做出假设。

1
text = ''.join(sample())

或者你也可以将字符串片段重定向到I/O:

1
2
for part in sample():
f.write(part)

或者你还可以写出一些结合I/O操作的混合方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)

字符串中插入变量

format()

1
2
3
>>> s = '{name} has {n} messages.'
>>> s.format(name='Guido', n=37)
'Guido has 37 messages.'

format_map()vars()

1
2
3
>>> name = 'Guido'
>>> n = 37
>>> s.format_map(vars())

vars() 还有一个有意思的特性就是它也适用于对象实例。

1
2
3
4
5
6
7
8
9
>>> 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() 的一个缺陷就是它们并不能很好的处理变量缺失的情况。

1
2
3
4
>>> s.format(name='Guido')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'n

一种避免这种错误的方法是另外定义一个含有 __missing__() 方法的字典对象。

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

1
2
3
4
>>> del n # Make sure n is undefined
>>> s.format_map(safesub(vars()))
'Guido has {n} messages.'
>>>

如果你发现自己在代码中频繁的执行这些步骤,你可以将变量替换步骤用一个工具函数封装起来。

1
2
3
4
import sys

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

1
2
3
4
5
6
7
8
9
>>> 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}
>>>

在字符串中处理html和xml

替换文本字符串中的‘<’或者‘>’ ,使用 html.escape()

1
2
3
4
5
6
>>> s = 'Elements are written as "<tag>text</tag>".'
>>> import html
>>> 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;".

如果你正在处理的是ASCII文本,并且想将非ASCII文本对应的编码实体嵌入进去, 可以给某些I/O函数传递参数 errors=’xmlcharrefreplace’ 来达到这个目。

1
2
3
4
>>> s = 'Spicy Jalapeño'
>>> s.encode('ascii', errors='xmlcharrefreplace')
b'Spicy Jalape&#241;o'
>>>

含有编码值的原始文本替换成文本字符串

1
2
3
4
5
6
7
8
9
10
11
>>> s = 'Spicy &quot;Jalape&#241;o&quot.'
>>> from html.parser import HTMLParser
>>> p = HTMLParser()
>>> p.unescape(s)
'Spicy "Jalapeño".'
>>>
>>> t = 'The prompt is &gt;&gt;&gt;'
>>> from xml.sax.saxutils import unescape
>>> unescape(t)
'The prompt is >>>'
>>>

字符串令牌解析(*)

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

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

Fork me on GitHub