当前位置:   article > 正文

cookbook-第二章-字符串和文本处理

cookbook-第二章-字符串和文本处理

在任意多分隔符下分离字符串

字符串对象的split()方法适用场景很少(不适合于多分隔符或者分隔符后空格数目未知的情况)。

使用re.split()

import re

line = 'asdf dasdasdas; dasdasda,     dadasd'
print(re.split(r'[;,\s]\s*', line))

['asdf', 'dasdasdas', 'dasdasda', 'dadasd']
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

使用捕获(capture group)

import re

line = 'asdf dasdasdas; dasdasda,     dadasd'
print(re.split(r'(;|,|\s)\s*', line))

['asdf', ' ', 'dasdasdas', ';', 'dasdasda', ',', 'dadasd']
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

我们发现,捕获可以获取()里面对应的模式,即分隔符

获取分隔符常常很有用,比如用分隔符重构字符串

import re

line = 'asdf dasdasdas; dasdasda,     dadasd'
fields = re.split(r'(;|,|\s)\s*', line)
#::表示隔2取,默认以0开始,如0,2,4,...
values = fields[::2]
#以1开始隔2取
delimiters = fields[1::2] + ['']
print(values)
print(delimiters)
#''.join表示连接
print(''.join(v + d for v, d in zip(values, delimiters)))

['asdf', 'dasdasdas', 'dasdasda', 'dadasd']
[' ', ';', ',', '']
asdf dasdasdas;dasdasda,dadasd
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

如果不想获取分隔符,并且想用括号来对正则表达式分段,注意使用?:

import re

line = 'asdf dasdasdas; dasdasda,     dadasd'
fields = re.split(r'(?:;|,|\s)\s*', line)
print(fields)

['asdf', 'dasdasdas', 'dasdasda', 'dadasd']
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

字符串前缀后缀

使用str.startswith()或者str.endswith()

filename = 'spam.txt'
print(filename.endswith('txt'))
print(filename.startswith('file'))
print('http://www.python.org'.startswith('http:'))

True
False
True
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

使用元组提供多个选择

filename.startswith(('txt', 'spam'))
  • 1

需要注意的地方:
元组不能替换为列表或者集合,如果为列表或集合,注意先使用tuple()转换为元组

startswith(endswith),元组和any结合使用

import os

if any(name.startswith(('.c', '.h')) for name in os.listdir(dirname)):
  • 1
  • 2
  • 3

用于判断某个目录下是否具有特定类型的文件

使用类shell通配符匹配字符串

使用fnmatch()

from fnmatch import fnmatch

print(fnmatch('foo.txt', '*.txt'))
print(fnmatch('foo.txt', '?oo.txt'))
print(fnmatch('Dat45.csv', 'Dat[0-9]*'))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

需要注意的是,fnmatch()的大小写判断依赖于操作系统。

使用fnmatchcase()

from fnmatch import fnmatch,fnmatchcase
#无论任何操作系统,遵守约定俗成的大小写规则
print(fnmatchcase('foo.txt', '*.TXT'))

False
  • 1
  • 2
  • 3
  • 4
  • 5

需要注意的地方:
fnmatch()是字符串方法和强大的正则表达式之间的折中,如果只需要简单的通配符机制,那么这是一个不错的选择。

匹配和查找文本模式

如果你想匹配的文本是简单的字面值,那么可以使用startswith(),endswith(),find()来匹配

如果是更复杂的匹配,那么就要用到正则表达式了

使用正则表达式

import re

text1 = '3/25/2018'
text2 = 'March 25, 2018'

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

如果需要使用同一模式来多次匹配字符串,那么需要先用compile编译正则表达式

使用compile()

import re

text1 = '03/25/2018'

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

if datepat.match(text1):
    print('yes')
else:
    print('no')
yes
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

需要注意的是,match总是匹配第一次出现的模式,如果想匹配所有模式,使用findall

使用findall匹配所有模式

import re

text = 'Today is 03/25/2018.Yesterday is 03/24/2018.'

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

print(datepat.findall(text))

['03/25/2018', '03/24/2018']
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

使用捕获

import re


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

m = datepat.match('03/25/2018')

print(m)

print(m.group(0), m.group(1), m.group(2), m.group(3), m.groups())

text = 'Today is 03/25/2018.Yesterday is 03/24/2018'

print(datepat.findall(text))

for month, day, year in datepat.findall(text):
    print('{}-{}-{}'.format(month, day, year))

<_sre.SRE_Match object; span=(0, 10), match='03/25/2018'>
03/25/2018 03 25 2018 ('03', '25', '2018')
[('03', '25', '2018'), ('03', '24', '2018')]
03-25-2018
03-24-2018
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

注意到,使用捕获很利于后续处理,因为捕获将匹配的模式分开了

findall找出所有匹配并且返回列表,如果想迭代处理,那么可以使用finditer()

使用finditer

import re

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

text = 'Today is 03/25/2018.Yesterday is 03/24/2018'

for m in datepat.finditer(text):
    print(m.groups()) 

('03', '25', '2018')
('03', '24', '2018')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

对使用正则表达式的简单总结:
1.编译正则表达式,返回pattern对象
2.使用match(),find(),findall(),finditer()等方法进行匹配

对于match,需要注意,它只检查字符串的开始,这就导致有时候不是你想要的结果

import re


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

m = datepat.match('11/27/2012dzxdzxdzx')

print(m)

print(m.group())

<_sre.SRE_Match object; span=(0, 10), match='11/27/2012'>
11/27/2012
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

因此需要用到结束标志$

总结一下需要注意的地方:使用正则表达式之前记得使用compile()编译,可以减少开销

查找和替换文本

对于普通的字面字符串而言,使用str.replace()

使用str.replace()

text = 'how do you'

print(text.replace('do', 'are'))

how are you
  • 1
  • 2
  • 3
  • 4
  • 5

对于更复杂的情况,使用re.sub().

使用re.sub()

import re

text = 'Today is 03/25/2018.Yesterday is 03/24/2018'

#\3表示取捕获组的第三个,即年
#re.sub()的第一个参数为匹配的模式,第二个参数是替换的模式
print(re.sub(r'(\d+)/(\d+)/(\d+)', r'\3-\1-\2', text))

Today is 2018-03-25.Yesterday is 2018-03-24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

使用re.compile()

import re

text = 'Today is 03/25/2018.Yesterday is 03/24/2018'

datepat = re.compile(r'(\d+)/(\d+)/(\d+)')
print(datepat.sub(r'\3-\1-\2', text))

Today is 2018-03-25.Yesterday is 2018-03-24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

对于更加复杂的替换,可以使用一个回调函数来定义替换部分

使用回调函数

import re
from calendar import month_abbr

#回调函数的参数是一个match对象,通常由find或者match返回,可以调用group提取匹配特定部分
def change_date(m):
    #m.group(1)提取了月的部分,并且由month_abbr()转换为了月份缩写形式
    mon_name = month_abbr[int(m.group(1))]
    #注意这里的{}相当于占位符,由format指定的参数来替换
    return '{} {} {}'.format(m.group(2), mon_name, m.group(3))

text = 'Today is 03/25/2018.Yesterday is 03/24/2018'

datepat = re.compile(r'(\d+)/(\d+)/(\d+)')
print(datepat.sub(change_date, text))

Today is 25 Mar 2018.Yesterday is 24 Mar 2018
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

整个功能相当于,datepat调用sub(),通过编译的正则表达式获取到了匹配,即match对象,将match对象传递给回调函数,回调函数进行处理,返回的结果替换匹配,一次替换结束,再进行第二次替换,依次进行,直到结束。

如果想知道进行了多少次替换,可以使用re.subn()

使用re.subn()

import re
from calendar import month_abbr

text = 'Today is 03/25/2018.Yesterday is 03/24/2018'

datepat = re.compile(r'(\d+)/(\d+)/(\d+)')
newtext, n = datepat.subn(r'\3-\1-\2', text)

print(newtext)
print(n)

Today is 2018-03-25.Yesterday is 2018-03-24
2
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

查找和替换大小写不敏感的文本

使用re.IGNORECASE标志

import re

text = 'UPPER PYTHON,lower python, Mixed Python'
print(re.findall('python', text, flags = re.IGNORECASE))
print(re.sub('python', 'case', text, flags = re.IGNORECASE))

['PYTHON', 'python', 'Python']
UPPER case,lower case, Mixed case
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这种做法暴露了一个缺陷。不能根据匹配内容的大小写替换对应的大小写内容(比如如果python为大写,仍然替换的是case)

使用支持函数

import re

#这种写法的意思是,每次调用re.sub,会调用matchcase,而matchcase返回replace,
#于是将匹配对象match传递给了replace,replace在matchcase的作用范围内,于是可以
#获取word的值进行处理
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

text = 'UPPER PYTHON,lower python, Mixed Python'
print(re.sub('python', matchcase('case'), text, flags = re.IGNORECASE))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

我们可以发现,替换的case随着python的大写,小写,首字母大写而变化

使匹配最短

先来看一个例子

import re

str_pat = re.compile(r'\"(.*)\"')
text1 = 'Computer says "no."'
print(str_pat.findall(text1))

text2 = 'Computer says "no." Phone says "yes."'
print(str_pat.findall(text2))
['no.']
['no." Phone says "yes.']
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

注意到匹配了整个”no.” Phone says “yes.”“,而我们想要的应该是”no.”和”yes.”。这是由于正则表达式中的*为贪婪的(greedy),所以匹配总会找出符合条件的最长的匹配。为了解决这个问题,需要使用?

?操作符使匹配最短

import re

str_pat = re.compile(r'\"(.*?)\"')
text2 = 'Computer says "no." Phone says "yes."'
print(str_pat.findall(text2))

['no.', 'yes.']
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

?操作符使匹配非贪心,并且使匹配最短

正则表达式处理多行

先来看一个处理注释的例子

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

[' this is a comment ']
[]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

我们会发现匹配多行的例子失败了,这是因为’.’虽然可以匹配任意字符,但是不能匹配换行符。
解决方法1

使用?:

import re
#注意,这里使用了?:非捕获符
comment = re.compile(r'/\*((?:.|\n)*?)\*/')
text2 = '''/* this is a
              multiline comment */
        '''
print(comment.findall(text2))

[' this is a\n              multiline comment ']
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

解决方法2

使用re.DOTALL

import re

comment = re.compile(r'/\*(.*?)\*/', re.DOTALL)
text2 = '''/* this is a
              multiline comment */
        '''
print(comment.findall(text2))

[' this is a\n              multiline comment ']
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

需要注意的地方:
re.DOTALL适用于简单的情况,如果正则表达式相当复杂可能会出问题。因此首推编写出正确的正则表达式,即第一种方法。

将Unicode文本正规化为标准文本

当处理Unicode字符串时,你需要确保每个字符串拥有相同的潜在表示形式。
对于Unicode字符集,有些字符可以存在多种表示形式。看一个例子

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

“Spicy Jalapeño”存在两种表示形式,第一种是一个整体\u00f1,第二种是n结合一个结合字符(combining character)\u0303
为了解决这个问题,我们需要使用unicodedata模块将字符串转换为同一种形式

使用unicodedata.normalize()

import unicodedata

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

#normalize()的第一个参数指定正规化形式
#NFC代表完全组成
#NFD代表完全解题,使用结合字符
t1 = unicodedata.normalize('NFC', s1)
t2 = unicodedata.normalize('NFC', s2)
print(t1 == t2)
print(ascii(t2))

t3 = unicodedata.normalize('NFD', s1)
t4 = unicodedata.normalize('NFD', s2)
print(t3 == t4)
print(ascii(t4))

True
'Spicy Jalape\xf1o'
True
'Spicy Jalapen\u0303o'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

总结:
1.正规化是相当重要的,因为你不知道你的输入文本是什么形式,尤其你无法控制输入的时候
2.正规化也可以用来过滤文本,如下所示

import unicodedata

s1 = 'Spicy Jalape\u00f1o'
t1 = unicodedata.normalize('NFD', s1)
#unicodedata.combing()可以识别出结合字符
print(''.join(c for c in t1 if not unicodedata.combining(c)))

Spicy Jalapeno
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

注意到,结合字符被过滤了

在正则表达式中处理Unicode字符集

正则表达式库可以初步识别一些基本的Unicode字符类型,如\d可以识别数字

import re

num = re.compile('\d+')
print(num.match('123'))

#如果你想匹配特定Unicode字符,可以使用转义字符
print(num.match('\u0661\u0662\u0663'))

<_sre.SRE_Match object; span=(0, 3), match='123'>
<_sre.SRE_Match object; span=(0, 3), match='١٢٣'>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

看一个比较麻烦的例子
这里写图片描述

总结一下关于处理Unicode需要注意的地方:
一定要注意正规化,将所有字符串都处理为同一种表示形式

从字符串中去除不想要的字符

使用strip(),lstrip(),rstrip()

s = '      hello word       \n'
print(s.strip())
print(s.lstrip())
print(s.rstrip())

t = '---------helloxxxxxxx'
print(t.lstrip('-'))
print(t.strip('-x'))

hello word
hello word       

      hello word
helloxxxxxxx
hello
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

需要注意的地方:
1,strip()默认去除空白符(包括换行符),但是也可以指定去除特定字符
2.strip()只处理左右两边的空白处,不处理中间的。如果想处理中间的空白字符,需要使用replace或者re.sub()

迭代时,结合使用strip除去空白符

with open(filename) as f:
    #注意这里使用的是生成器表达式,因此会返回一个迭代器,而不是创建临时的列表保存数据
    lines = (line.strip() for line in f)
    for line in lines:
        pass
  • 1
  • 2
  • 3
  • 4
  • 5

清理文本

在一些很简单的层面上,可以使用str.upper(),str.lower()或者str.replace(),re.sub()等方法来处理文本,或者使用之前讲到的unicodedata.normalize()来正规化文本。但是更进一层,可以使用translate()

使用translate()

这里写图片描述
这里写图片描述

注意到,translate()方法做的相当于是文本处理的映射。

更进一步,清除所有结合字符
这里写图片描述
可以看出,先通过dict.fromkeys建立了一个以所有结合字符为键,值为None的字典,然后translate使用这个字典将结合字符映射为空,即清除掉

再看一个例子,将Unicode字符集中的数字字符替换为ASCII码的等价形式
这里写图片描述

使用encode和decode

这里写图片描述

总结:
1.越简单,通常越快。例如str.replace()通常是最快的,尽管可能需要调用多次。
2.translate()对于字符->字符转换,或者字符->清除也是很快的
3.没有通用的最好的方法。模式是尝试各种方法,找出最优性能的那一个。

分配文本字符串

使用ljust(),rjust(),center()

text = 'Hello World'
#填充到20个字符,不足的地方填充空白,字符串左移
print(text.ljust(20))
##填充到20个字符,不足的地方填充空白,字符串右移
print(text.rjust(20))
##填充到20个字符,不足的地方填充空白,字符串往中间移动
print(text.center(20))

Hello World         
         Hello World
    Hello World     

#指定填充字符
print(text.rjust(20, '='))
print(text.center(20, '*'))

=========Hello World
****Hello World*****
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

使用format()

text = 'Hello World'
print(format(text, '>20'))
print(format(text, '<20'))
print(format(text, '^20'))
print(format(text, '=>20s'))
print(format(text, '*^20s'))

         Hello World
Hello World         
    Hello World     
=========Hello World
****Hello World*****

#注意,format可以一次性格式化多个字符串
print('{:>10s} {:>10s}'.format('Hello', 'World'))

     Hello      World
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

对于format()需要注意的地方:
它不仅适用于字符串,它适用于所有值类型。因此format()比其他方法也更具一般性

x = 1.2345
print(format(x, '>10'))
print(format(x, '^10.2f'))

    1.2345
   1.23   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

使用’%’

text = 'Hello World'
print('%-20s' % text)
print('%20s' % text)

Hello World         
         Hello World
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

总结:
format()方法功能最强大,也更具一般性。另外,尽量不要用’%’

连接字符串

如果你想连接序列或者可迭代对象,最快的方法是join()

使用join()

parts = ['Is', 'Chicago', 'Not', 'Chicago?']

print(' '.join(parts))

Is Chicago Not Chicago?
  • 1
  • 2
  • 3
  • 4
  • 5

如果只是想连接很少的字符串,那么可以使用’+’

使用’+’

a = 'is'
b = 'you'

print(a + ' ' + b)

is you
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

‘+’可以是format的替换
这里写图片描述

如果只是连接字面值,可以这样做

a = 'Hello ' 'World'

print(a)

Hello World
  • 1
  • 2
  • 3
  • 4
  • 5

需要注意的地方:
当连接大量字符串时,’+’号的开销很巨大,因为有内存拷贝和垃圾回收等开销(跟java一样,字符串多了用StringBuilder).这种情况一定要用join()

使用生成器表达式

这里写图片描述
注意到,str()字符串转换和使用生成器表达式进行字符串连接同时进行。

有时候,字符串连接是不必要的。看看例子
这里写图片描述

如果涉及到I/O,那么字符串的连接需要一些权衡
这里写图片描述
对于版本1,当chunk1和chunk2很小时,会比版本2还快,因为版本2由于有两次I/O操作,会有额外开销,但是当chunk1和chunk2很大时,版本1的额外开销很大,因为它会产生临时结果以及内存拷贝等开销

通过生成器函数产生片段

def sample():
    yield 'Is'
    yield 'Chicago'
    yield 'Not'
    yield 'Chicago'

text = ''.join(sample())

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

#短短几行就实现了缓冲区的功能,是不是相当酷(我说的是Python)
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):
    f.write(part)
  • 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

注意到,生成器函数并不在意数据组合的细节,他只负责产生数据(搬运工)

在字符串中插入变量

使用字符串对象的format()

s = '{name} has {n} messages'
print(s.format(name = 'xxx', n = 'xxx'))

xxx has xxx messages
  • 1
  • 2
  • 3
  • 4

如果变量已经定义过了,那么可以结合使用format_map()和vars()

name = 'xxx'
n = 'xxx'

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

xxx has xxx messages
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

vars()还有一个特性,那就是适用于实例对象

class Info:
    def __init__(self, name, n):
        self.name = name
        self.n = n

s = '{name} has {n} messages'
a = Info('xxx', 11)
print(s.format_map(vars(a)))

xxx has 11 messages
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

需要注意的是,format()和format的一个缺陷是不能处理缺失值

s = '{name} has {n} messages'
print(s.format(name = 'xxx'))

Traceback (most recent call last):
  File "C:\Users\Administrator\Desktop\test.py", line 3, in <module>
    print(s.format(name = 'xxx'))
KeyError: 'n'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

一个解决办法是定义一个字典子类,编写missing()方法

使用missing()

class safesub(dict):
    def __missing__(self, key):
        return '{' + key + '}'

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

{name} has xxx messages
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

注意到,缺失的数据没有被替换

如果你发现你经常使用format_map,那么应该用一个方法将它包装起来,这个方法用到了”frame hack”

使用frame hack

import sys

class safesub(dict):
    def __missing__(self, key):
        return '{' + key + '}'

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


name = 'xxx'
n = 111

print(sub('Hello {name}'))
print(sub('{n} hello'))
print(sub('your favourite color is {color}'))

Hello xxx
111 hello
your favourite color is {color}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

其他的插入变量的方式

这里写图片描述
需要注意的是,以上两种方式都没有format()好

总结:
1.format()的优势在于,可以结合字符串格式化(alignment, padding, numerical formatting)等使用
2.f_locals是一个本地变量的拷贝,类型为字典,因此不用担心改变它会产生额外影响

格式化文本到指定的列数

使用textwrap()

这里写图片描述

使用get_terminal_size()获取终端窗口大小

这里写图片描述

处理html和xml实体

使用html.escape()

这里写图片描述

使用errors处理ascii码转换

这里写图片描述

使用html parser或者xml parser

这里写图片描述

将文本转换为token

假设你有这样一个字符串,text = ‘foo = 23 +42 * 10’

为了符号化这个字符串,你需要某种识别模式的方式,例如转化为这样的形式
这里写图片描述

为了符号化,第一步就需要定义所有可能的符号,包括空格。方法是正则表达式和命名捕获组(named capture group)

使用正则表达式和命名捕获组

这里写图片描述

我们注意到,一般形式为?p<符号名称>+正则表达式

符号化的第一步完成了,第二步是使用scanner()

使用scanner()

这里写图片描述

结合使用生成器和scanner

这里写图片描述

使用生成器表达式过滤token

这里写图片描述

总结:
1.必须想清楚每种可能的符号,如果有任何模式未被匹配,scanner的扫描过程就会终止,这也是为什么需要特别定义空字符的符号
2.符号的顺序也需要注意

编写解析器

BNF:
这里写图片描述
EBNF:
这里写图片描述

表达式计算器

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:
            return False

    def _expect(self, toktype):
        if not self._accept(toktype):
            raise SyntaxError('Expected ' + toktype)

    def expr(self):
        expval = self.term()

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

        return expval

    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 == 'DIVIDE':
                termval /= right

        return termval

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

e = ExpressionEvaluator()
print(e.parse('2'))
print(e.parse(' 2 + (3 * 3) + 4 / 5 + (5 / 5) * 10'))

2
21.8
  • 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

解析器

class ExpressionTreeBuilder(ExpressionEvaluator):
    def expr(self):
        expval = self.term()

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

        return expval

    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 = ('*', termval, right)
            elif op == 'DIVIDE':
                termval = ('/', termval, right)

        return termval

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

e = ExpressionTreeBuilder()
print(e.parse('2 + 3 * (4 + 5 / 6)'))

('+', 2, ('*', 3, ('+', 4, ('/', 5, 6))))
  • 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

解析步骤总结(直接上原文吧,感觉自己总结的可能会误导。。。)
这里写图片描述
这里写图片描述

对字节字符串进行文本操作

字节字符串支持大多数文本字符串的内置函数
这里写图片描述

这里写图片描述

注意,将正则表达式应用于字节字符串时,正则表达式也需要是字节形式
这里写图片描述

一点需要注意的字节字符串文本字符串的差异
1.索引后得到的值为整数而不是字符
2.字节字符串输出会形如’b’xxxxxx”
3.format()不支持字节字符串
4.如果将字节字符串作为文件名,会使文件名编解码失效。

总结:
处理文本,尽量使用文本字符串!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/435084
推荐阅读
相关标签
  

闽ICP备14008679号