Python中的字符串和文本操作
1.针对任意多的分隔符拆分字符串
string 对象的 split() 方法只适应于非常简单的字符串分割情形,它并不允许有多个分隔符或者是分隔符周围不确定的空格。当你需要更加灵活的切割字符串的时候, 最好使用 re.split() 方法:
import re
line = 'asdf fjdk; afed, fjek,asfd, foo'
---函数 re.split() 是非常实用的,因为它允许你为分隔符指定多个正则模式。---
res_1 = re.split(r'[;,\s]\s*', line)
print(res_1)
--当你使用 re.split() 函数时候,需要特别注意的是正则表达式中的捕获组是否包含在了括号中。
--如果使用了捕获组,那么被匹配的文本也将出现在结果列表中。
res_2 = re.split(r'(;|,|\s)\s*', line)
print(res_2)
values = res_2[::2]
print(values)
--如果你不想保留分割字符串到结果列表中去,但仍然需要使用到括号来分组正则 表达式的话,确保你的分组是非捕获分组,形如 (?:...)
#非捕获组 (?:....)
res_3 = re.split(r'(?:;|,|\s)\s*', line)
print(res_3)
输出:
['asdf', 'fjdk', 'afed', 'fjek', 'asfd', 'foo']
['asdf', ' ', 'fjdk', ';', 'afed', ',', 'fjek', ',', 'asfd', ',', 'foo']
['asdf', 'fjdk', 'afed', 'fjek', 'asfd', 'foo']
['asdf', 'fjdk', 'afed', 'fjek', 'asfd', 'foo']
2.在字符串的开头或结尾处做文本匹配
检查字符串开头或结尾的一个简单方法是使用 str.startswith() 或者是 str.endswith() 方法。
--如果你想检查多种匹配可能,只需要将所有的匹配项放入到一个元组中去,然后传给 startswith() 或者 endswith() 方法:
choices = ['http', 'ftp']
url = 'http://www.python.org'
# print(url.startswith(choices))#TypeError: startswith first arg must be str or a tuple of str, not list
print(url.startswith(tuple(choices)))
3.利用shell通配符做字符串匹配(待续。。。。)
4.文本模式的匹配和查找
import re
text1 = '11/27/2012'
text2 = 'Nov 27, 2012'
if re.match(r'\d+/\d+/\d+', text1):
print('yes')
else:
print('no')
if re.match(r'\d+/\d+/\d+', text2):
print('yes')
else:
print('no')
输出:
yes
no
如果你想使用同一个模式去做多次匹配,你应该先将模式字符串预编译为模式对象。
datepat = re.compile(r'\d+/\d+/\d+')#将正则表达式模式预编译成一个模式对象
if datepat.match(text1):
print('yes')
else:
print('no')
if datepat.match(text2):
print('yes')
else:
print('no')
输出:
yes
no
match() 总是从字符串开始去匹配,如果你想查找字符串任意部分的模式出现位 置,使用 findall() 方法去代替。比如:
text = 'Today is 11/27/2012. PyCon starts 3/13/2013'
res = datepat.findall(text)
print(res)
输出:
['11/27/2012', '3/13/2013']
在定义正则式的时候,通常会将部分模式用括号包起来的方式引入捕获组。捕获组可以使得后面的处理更加简单,因为可以分别将每个组的内容提取出来。
findall() 方法会搜索文本并以列表形式返回所有的匹配。如果你想以迭代方式返回匹配,可以使用 finditer() 方法来代替,比如:
m = datepat.match('11/27/2012')
print(m)
print(datepat.findall(text))
for m in datepat.finditer(text):
print(m.group(0))
输出:
<re.Match object; span=(0, 10), match='11/27/2012'>
['11/27/2012', '3/13/2013']
11/27/2012
3/13/2013
以上就是使用 re 模块进行匹配和搜索文本的最基本方法,核心步骤就是先使用 re.compile() 编译正则表达式字符串,然后使用 match() , findall() 或者 finditer() 等方法。
当写正则式字符串的时候,相对普遍的做法是使用原始字符串比如 r’(\d+)/(\d+)/(\d+)’ 。这种字符串将不去解析反斜杠,这在正则表达式中是很有用的。如果不这样做的话,你必须使用两个反斜杠,类似 ‘(\d+)/(\d+)/(\d+)’ 。
如果你想精确匹配,确保你的正则表达式以 $ 结尾。
如果你打算做大量的匹配和搜索操作的话,最好先编译正则表达式,然后再重复使用它。模块级别的函数会将最近编译过的模式缓存起来,因此并不会消耗太多的性能,但是如果使用预编译模式的话,你将会减少查找和一些额外的处理损耗。
5.查找和替换文本
对于简单的字面模式,直接使用 str.replace() 方法即可,比如:
text = 'yy, yy, yy, hs, yy, hs'
print(text.replace('yy','hs'))
输出:
hs, hs, hs, hs, hs, hs
对于复杂的模式,请使用 re 模块中的 sub() 函数。为了说明这个,假设你想将形 式为 11/27/2012 的日期字符串改成 2012-11-27 。
text = 'Today is 11/27/2012. PyCon starts 3/13/2013'
--sub() 函数中的第一个参数是被匹配的模式,第二个参数是替换模式。反斜杠数字 比如 \3 指向前面模式的捕获组号。
print(re.sub(r'(\d+)/(\d+)/(\d+)', r'\3-\1-\2', text))
输出:
Today is 2012-11-27. PyCon starts 2013-3-13
除了得到替换后的文本外,如果还想知道一共完成了多少次替换,可以使用 re.subn() 来代替。
datepat = re.compile(r'(\d+)/(\d+)/(\d+)')
newtext, n = datepat.subn(r'\3-\1-\2', text)
print(newtext, n, sep='\n')
输出:
Today is 2012-11-27. PyCon starts 2013-3-13
2
6.以不区分大小写的方式对文本做查找和替换
为了在文本操作时忽略大小写,你需要在使用 re 模块的时候给这些操作提供 re.IGNORECASE 标志参数。
import re
text = 'UPPER PYTHON, lower python, Mixed Python'
res = re.findall('python', text, flags=re.IGNORECASE)
print(res)
res = re.sub('python', 'hs', text, flags=re.IGNORECASE)
print(res)
输出:
['PYTHON', 'python', 'Python']
UPPER hs, lower hs, Mixed hs
以上例子揭示了一个小缺陷,替换字符串并不会自动跟被匹配字符串的大 小写保持一致。为了修复这个,你可能需要一个辅助函数,就像下面的这样:
def matchcase(word):
def replace_word(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_word
res = re.sub('python', matchcase('hs'), text, flags=re.IGNORECASE)
print(res)
输出:
UPPER HS, lower hs, Mixed Hs
注意: matchcase(‘snake’) 返回了一个回调函数 (参数必须是 match 对象),前 面一节提到过,sub() 函数除了接受替换字符串外,还能接受一个回调函数。
7.定义实现最短匹配的正则表达式
你正在试着用正则表达式匹配某个文本模式,但是它找到的是模式的最长可能匹 配。而你想修改它变成查找最短的可能匹配。
这个问题一般出现在需要匹配一对分隔符之间的文本的时候 (比如引号包含的字符串)。
import re
--模式 r'\"(.*)\"' 的意图是匹配被双引号包含的文本。但是在正则表达式中 * 操作符是贪婪的,因此匹配操作会查找最长的可能匹配。
--于是在第二个例子中搜索 text2 的时候返回结果并不是我们想要的。
str_pat = re.compile(r'\"(.*)\"')
text1 = 'Computer says "no."'
res1 = str_pat.findall(text1)
print(res1)
text2 = 'Computer says "no." Phone says "yes."'
res2 = str_pat.findall(text2)
print(res2)
--为了修正这个问题,可以在模式中的 * 操作符后面加上? 修饰符,就像这样:
str_pat1 = re.compile(r'\"(.*?)\"')
res3 = str_pat1.findall(text2)
print(res3)
--这样就使得匹配变成非贪婪模式,从而得到最短的匹配,也就是我们想要的结果。
输出:
['no.']
['no." Phone says "yes.']
['no.', 'yes.']
在一 个模式字符串中,点 (.) 匹配除了换行外的任何字符。然而,如果你将点 (.) 号放在开始 与结束符 (比如引号) 之间的时候,那么匹配操作会查找符合模式的最长可能匹配。这 样通常会导致很多中间的被开始与结束符包含的文本被忽略掉,并最终被包含在匹配 结果字符串中返回。通过在 * 或者 + 这样的操作符后面添加一个 ? 可以强制匹配算法改成寻找最短的可能匹配。
8.编写多行模式的正则表达式
import re
comment = re.compile(r'/\*(.*?)\*/')
text1 = '/* this is a comment */'
text2 = '''/* this is a
multiline comment */
'''
print(comment.findall(text1))
print(comment.findall(text2))
--在这个模式中,(?:.|\n) 指定了一个非捕获组(即,这个组只做匹配但不捕获结果,也不会分配组号)
comment_1 = re.compile(r'/\*((?:.|\n)*?)\*/')
print(comment_1.findall(text2))
--re.DOTALL使得正则表达式中的句点可以匹配所有的字符,包括换行符。
comment_2 = re.compile(r'/\*(.*?)\*/', re.DOTALL)
print(comment_2.findall(text2))
输出:
[' this is a comment ']
[]
[' this is a\n multiline comment ']
[' this is a\n multiline comment ']
9.将Unicode文本统一表示为规范性形式
你正在处理 Unicode 字符串,需要确保所有字符串在底层有相同的表示。在 Unicode 中,某些字符能够用多个合法的编码表示。为了说明,考虑下面的这个
import unicodedata
s1 = 'Spicy Jalape\u00f1o'
s2 = 'Spicy Jalapen\u0303o'
print(s1)
print(s2)
print(s1 == s2)
print(len(s1))
print(len(s2))
输出:
Spicy Jalapeño
Spicy Jalapeño
False
14
15
这里的文本”Spicy Jalapeño”使用了两种形式来表示。第一种使用整体字符”ñ”
(U+00F1),第二种使用拉丁字母”n”后面跟一个”~”的组合字符 (U+0303)。 在需要比较字符串的程序中使用字符的多种表示会产生问题。为了修正这个问题,
你可以使用 unicodedata 模块先将文本标准化:
t1 = unicodedata.normalize('NFC', s1)
t2 = unicodedata.normalize('NFC', s2)
print(t1 == t2)
print(ascii(t1))
--normalize() 第一个参数指定字符串标准化的方式。NFC 表示字符应该是整体组成 (比如可能的话就使用单一编码),
--而 NFD 表示字符应该分解为多个组合字符表示。
t3 = unicodedata.normalize('NFD', s1)
t4 = unicodedata.normalize('NFD', s2)
print(t3 == t4)
print(ascii(t3))
输出:
True
'Spicy Jalape\xf1o'
True
'Spicy Jalapen\u0303o'
标准化对于任何需要以一致的方式处理 Unicode 文本的程序都是非常重要的。当 处理来自用户输入的字符串而你很难去控制编码的时候尤其如此。
在清理和过滤文本的时候字符的标准化也是很重要的。比如,假设你想清除掉一些文本上面的变音符的时候:
t1 = unicodedata.normalize('NFD', s1)
print(t1)
--combining() 函数可以测试一个字符是否为组合型字符。
x = ''.join(c for c in t1 if not unicodedata.combining(c))
print(x)
输出:
Spicy Jalapeño
Spicy Jalapeno
10.用正则表达式处理Unicode字符(待续…)
11.从字符串中去掉不需要的字符
strip() 方法能用于删除开始或结尾的字符。lstrip() 和 rstrip() 分别从左和从右执行删除操作。默认情况下,这些方法会去除空白字符,但是你也可以指定其他字符。
s1 = ' hello word \n'
print(s1.strip())
print(s1.lstrip())
print(s1.rstrip())
s2 = '-------------hello============='
print(s2.lstrip('-'))
print(s2.rstrip('='))
print(s2.strip('-='))
输出:
hello word
hello word
hello word
hello=============
-------------hello
hello
strip() 方法在读取和清理数据以备后续处理的时候是经常会被用到的。比 如,你可以用它们来去掉空格,引号和完成其他任务。但是需要注意的是去除操作不会对字符串的中间的文本产生任何影响。
如果你想处理中间的空格,那么你需要求助其他技术。比如使用 replace() 方法或者是用正则表达式替换。
通常情况下你想将字符串 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 操作。
12.文本过滤和清理(待续…)
13.对齐文本字符串
对于基本的字符串对齐操作,可以使用字符串的 ljust() , rjust() 和 center()方法。比如:
text = 'Hello World'
print(text.ljust(20))
print(text.rjust(20))
print(text.center(20))
--所有这些方法都能接受一个可选的填充字符。比如:
print(text.ljust(20, '*'))
print(text.rjust(20, '*'))
print(text.center(20, '-'))
输出:
Hello World
Hello World
Hello World
Hello World*********
*********Hello World
----Hello World-----
函数 format() 同样可以用来很容易的对齐字符串。你要做的就是使用 <,> 或者 ^ 字符后面紧跟一个指定的宽度。比如:
r = format(text, '>20')
le = format(text, '<20')
c = format(text, '^20')
print(r)
print(le)
print(c)
r1 = format(text, '=>20s')
print(r1)
c2 = format(text, '=^20s')
print(c2)
--format() 函数的一个好处是它不仅适用于字符串。它可以用来格式化任何值,使 得它非常的通用。比如,你可以用它来格式化数字:
n = 1.34569
print(format(n, '>10'))
print(format(n, '>10.3f'))
输出:
Hello World
Hello World
Hello World
=========Hello World
====Hello World=====
1.34569
1.346
14.字符串连接及合并
如果你想要合并的字符串是在一个序列或者 iterable 中,那么最快的方式就是使 用 join() 方法。
如果你仅仅只是合并少数几个字符串,使用加号 (+) 通常已经足够了。
最重要的需要引起注意的是,当我们使用加号 (+) 操作符去连接大量的字符串的 时候是非常低效率的,因为加号连接会引起内存复制以及垃圾回收操作。
如果你准备编写构建大量小字符串的输出代码,你最好考虑下使用生 成器函数,利用 yield 语句产生输出片段。比如:
def sample():
yield 'Is'
yield 'Chicago'
yield 'Not'
yield 'Chicago'
text = ''.join(sample())
print(text)
输出:
IsChicagoNotChicago
import sys
for part in sample():
sys.stdout.write(part)
sys.stdout.write('\n')
输出:
IsChicagoNotChicago
再或者你还可以写出一些结合 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)
for part in combine(sample(), 32768):
sys.stdout.write(part)
sys.stdout.write('\n')
输出:
IsChicagoNotChicago
这里的关键点在于原始的生成器函数并不需要知道使用细节,它只负责生成字符串片段就行了。
15.给字符串中的变量名做插值处理
Python 并没有对在字符串中简单替换变量值提供直接的支持。但是通过使用字符串的 format() 方法来解决这个问题。比如:
s = '{name} has {n} messages.'
x = s.format(name='hs', n=24)
print(x)
输出:
hs has 24 messages.
如果要被替换的变量能在变量域中找到,那么你可以结合使用 format_map() 和 vars() 。就像下面这样:
name = 'yy'
n = 21
y = s.format_map(vars())
print(y)
输出:
yy has 21 messages.
vars() 还有一个有意思的特性就是它也适用于对象实例。比如:
class Info:
def __init__(self, name, n):
self.name = name
self.n = n
a = Info('HS',30)
z = s.format_map(vars(a))
print(z)
输出:
HS has 30 messages.
format 和 format_map() 的一个缺陷就是它们并不能很好的处理变量缺失的情况, 比如:
w = s.format(name='YY')
print(w)#KeyError: 'n'
一种避免这种错误的方法是另外定义一个含有 missing() 方法的字典对象, 就像下面这样:
class safesub(dict):
def __missing__(self, key):
return '{' + key + '}'
del n
m = s.format_map(safesub(vars()))
print(m)
输出:
yy has {n} messages.
现在你可以像下面这样写了:
name = 'yangyang'
n = 21
print(sub('Hello {name}'))
print(sub('You have {n} message.'))
print(sub('You favorite color is {color}'))
输出:
Hello yangyang
You have 21 message.
You favorite color is {color}
字符串模板的使用:
import string
name = 'hongsong'
n = 24
s = string.Template('$name has $n message.')
print(s.substitute(vars()))
输出:
hongsong has 24 message.
然而,format() 和 format_map() 相比较上面这些方案而已更加先进,因此应该被优先选择。使用 format() 方法还有一个好处就是你可以获得对字符串格式化的所有 支持 (对齐,填充,数字格式化等待),而这些特性是使用像模板字符串之类的方案不可能获得的。
16.以固定的列数重新格式化文本
使用 textwrap 模块来格式化字符串的输出。
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))
print(textwrap.fill(s, 40))
print(textwrap.fill(s, 40, initial_indent=' '))
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.
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.
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.
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.
fill() 方法还有一些额外的选项可以用来控制如何处理制表符、句号等。
17.在文本中处理HTML和XML实体(待续…)
18.文本分词
要对字符串做分词处理,你不仅需要匹配模式,还得指定模式的类型。
import re
from collections import namedtuple
--,?P<TOKENNAME> 用于给一个模式命名
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+)'
master_pat = re.compile('|'.join([NAME, NUM, PLUS, TIMES, EQ, WS]))
Token = namedtuple('Token', ['type', 'value'])
--scanner() 方法会创建一个 scanner 对象,在这个对象上不断的调用 match() 方法会一步步的扫描目标文本,一次匹配一个模式。
def generate_tokens(pat, text):
scanner = pat.scanner(text)
for m in iter(scanner.match, None):
yield Token(m.lastgroup, m.group())
for tok in generate_tokens(master_pat, 'foo = 42'):
print(tok)
输出:
Token(type='NAME', value='foo')
Token(type='WS', value=' ')
Token(type='EQ', value='=')
Token(type='WS', value=' ')
Token(type='NUM', value='42')
这些标记在正则表达式(即re.compile(’|’.join([NAME, NUM, PLUS, TIMES, EQ, WS])))中的顺序同样也很重要。当进行匹配时,re模块会按照指定的顺序来对模式做匹配。如果碰巧某个模式是另一个较长模式的子串时,就必须确保较长的那个模式要先做匹配。
LT = r'(?P<LT><)' LE = r'(?P<LE><=)' EQ = r'(?P<EQ>=)'
master_pat = re.compile('|'.join([LE, LT, EQ])) # Correct
master_pat = re.compile('|'.join([LT, LE, EQ])) # Incorrect
第二个模式是错的,因为它会将文本 <= 匹配为令牌 LT 紧跟着 EQ,而不是单独的令牌 LE,这个并不是我们想要的结果。
最后也最重要的是,对于有可能形成子串的模式要多加小心。
19.编写一个简单的递归下降解析器(待续…)
20.在字节串上执行文本操作
字节串(Byte String)同样也支持大部分和文本字符串一样的内置操作。
data = b'Hello word'
print(data[0:5])
print(data.startswith(b'Hello'))
print(data.split())
print(data.replace(b'word', b'hsyy'))
data_array = bytearray(b'Hello Word')
print(data_array[0:5])
print(data_array.startswith(b'Hello'))
print(data_array.split())
# print(data_array.replace(b'word', b' word hs_yy'))#没有替换成功
# -------------------------------------#
data = b'FOO:BAR, SPAM'
import re
# print(re.split('[:,]', data))#TypeError: cannot use a string pattern on a bytes-like object
print(re.split(b'[:,]', data))
# ----------------------------------------------#
--大多数情况下,在文本字符串上的操作均可用于字节字符串。然而,这里也有一些需要注意的不同点。
--首先,字节串的索引操作返回整数而不是单独字符。
a = 'Hello World'
print(a[0])
b = b'Hello world'
print(b[0])
print(b)
--字节字符串不会提供一个美观的字符串表示,也不能很好的打印出来,除非它们先被解码为一个文本字符串。比如:
b_string = b.decode('ascii')
print(b_string)
# ----------------------------------------------------#
--如果你想格式化字节字符串,你得先使用标准的文本字符串,然后将其编码为字节串。比如:
b_string_format = '{:10s} {:10d} {:10.2f}'.format('Hongsong', 100, 490.1).encode('ascii')
print(b_string_format)
输出:
b'Hello'
True
[b'Hello', b'word']
b'Hello hsyy'
bytearray(b'Hello')
True
[bytearray(b'Hello'), bytearray(b'Word')]
[b'FOO', b'BAR', b' SPAM']
H
72
b'Hello world'
Hello world
b'Hongsong 100 490.10'
最后需要注意的是,使用字节字符串可能会改变一些操作的语义,特别是那些跟文件系统有关的操作。
最后提一点,一些程序员为了提升程序执行的速度会倾向于使用字节字符串而不是文本字符串。尽管操作字节字符串确实会比文本更加高效 (因为处理文本固有的 Unicode 相关开销)。这样做通常会导致非常杂乱的代码。你会经常发现字节字符串并不能和 Python 的其他部分工作的很好,并且你还得手动处理所有的编码/解码操作。坦白讲,如果你在处理文本的话,就直接在程序中使用普通的文本字符串而不是字节字符 串。不做死就不会死!