Python Cookbook II 的学习笔记,第01章:文本操作。一些常用的文本操作手段,字符串的提取,删等,构建高级的正则替换函数或对象。

常用的技巧

测试结尾部分是否为提供参数之一

字符串的 s.startswith() s.endswith() 方法测试其开始和结束是否为特定的序列,扩展这个方法:

def ends_with(s, *endings):
  return any(map(s.endswith, endings))

for filename in os.listdir('.'):
  if ends_with(filename, '.jpg', '.png', '.gif'):
    print(filename)

以换行符分割多行字符串

s.splitlines()

以换行符分割多行字符串,且保留行尾的换行符

s.splitlines(True)

取得第一个空格前的部分,这种简单的任务不需要用 split

word = word[:word.index(' ')]

转换 ASCII

print(ord('a')) # => 97
print(chr(97)) # => a

分割字符串时捕获分割组

import re
pieces1 = re.compile(r'\d+').split('aaa 123 bbb 456 ccc')
print(pieces1)   # => ['aaa ', ' bbb ', ' ccc']
pieces2 = re.compile(r'(\d+)').split('aaa 123 bbb 456 ccc')
print(pieces2)   # => ['aaa ', '123', ' bbb ', '456', ' ccc']
testsplit = re.split(r'((1)((2)(3)))', 'aaa1234bbb')
print(testsplit) # => ['aaa', '123', '1', '23', '2', '3', '4bbb']

反转字符串

按照字符反转

rev = astring[::-1]

按照单词反转

rev_words = ' '.join(astring.split()[::-1])

单词反转,保留多个空格

rev_words = re.split(r'(\s+)', astring)
rev_words.reverse()
rev_words = ''.join(rev_words)

提取、删除、替换指定字符串的类

import re
class translator(object):
  def __init__(self, pattern, keep=True, to=''):
    super(translator, self).__init__()
    self.pattern = re.compile(pattern)
    self.keep = keep
    self.replace_by = to
    if to:
      self.keep = False

  def __call__(self, text):
    if self.keep:
      return ''.join(re.findall(self.pattern, text))
    else:
      return re.subn(self.pattern, self.replace_by, text)[0]

digits_only = translator(r'\d')
r = digits_only('Chris Perkins : 224-7992f')
print(r) # => 2247992

no_digits = translator(r'\d', keep=False)
r = no_digits('Chris Perkins : 224-7992f')
print(r) # => 'Chris Perkins : -f'

digits_to_hash = translator(r'\d', to='#')
r = digits_to_hash('Chris Perkins : 224-7992f')
print(r) # => 'Chris Perkins : ###-####f'

locals 的用法

locals() 函数将创建一个字典,字典的 key 就是本地变量:

msg = string.Template('the square of $number is $square')
for number in range(10):
  square = number * number
  print(msg.substitute(locals()))
  # same as
  print(msg.substitute(number=number, square=sqaure))

一次替换多个子字符串

给出一段文本和字典,所有能够在指定字典中找到的子串都被替换为字典中的对应值:

import re
def multiple_replace(text, adict):
  rx = re.compile('|'.join(map(re.escape, adict)))
  def one_xlat(match):
    return adict[match.group(0)]
  return rx.sub(one_xlat, text)

s = 'aaa bbb aaa 12312'
print(multiple_replace(s, {'aaa':'xxx', '12': '0'}))
# => 'xxx bbb xxx 030'

首先根据想要匹配的key创建一个正则表达式,形式为 a1|a2|...|aN, 然后不直接给 re.sub 传递用于替换的字符串,而是传入一个回调函数参数。每当遇到一次匹配 re.sub 就会调用该回调函数,并将 re.MatchObject 的实例作为唯一参数传递给该回调函数,并期望着该回调函数返回作为替换物的字符串。

上面的做法有个缺陷,函数 multiple_replace 每次被调用都会重算正则表达式并重定义 one_xlat 辅助函数。但经常只需要使用同一个固定不变的翻译表来完成很多文本的替换,这种情况下会希望只做一次准备工作。出于这种需求,最好使用下面的基于闭包的方式:

import re
def make_xlat(*args, **kwds):
  adict = dict(*args, **kwds)
  rx = re.compile('|'.join(map(re.escape, adict)))
  def one_xlat(match):
    return adict[match.group(0)]
  def xlat(text):
    return rx.sub(one_xlat, text)
  return xlat

text = "Larry Wall is the creator of Perl"
adict = {
  "Larry Wall" : "Guido van Rossum",
  "creator" : "Benevolent Dictator for Life",
  "Perl" : "Python",
}
translate = make_xlat(adict)
print(translate(text))
# => 'Guido van Rossum is the Benevolent Dictator for Life of Python'

此外,替换任务常常是基于单词的,而不是基于任意一个子字符串。通过特殊的 r'\b' 序列,正则表达式可以很好地找出单词的开始和结束位置。可以修改 multiple_replace 和 make_xlat 中创建和分配正则表达式 rx 的部分从而完成一些自定任务。

rx = re.compile(r'\b%s\b' % r'\b|\b'.join(map(re.escape, adict)))
# 当同时有 % 和 join 时,计算顺序是先 join ,然后 %

这样对基于单词和不基于单词的替换,都完成了任务,但是用到了两组函数,大部分片段都是重复的。好代码的一个关键规则是”只做一次”,当注意到代码重复的时候,应该能够很快嗅到不妙的气味,并对原来的代码进行重构以提髙复用性。这里为了便于定制,我们更需要一个类,而不是函数或闭包。

class make_xlat:
  def __init__(self, *args, **kwds):
    self.adict = dict(*args, **kwds)
    self.rx = self.make_rx()
  def make_rx(self):
    return re.compile('|'.join(map(re.escape, self.adict)))
  def one_xlat(self, match):
    return self.adict[match.group(0)]
  def __call__(self, text):
    return self.rx.sub(self.one_xlat, text)

类的优势是可以子类化或重载某些函数,轻易地实现重新定制。为了对单词进行翻译替换,代码可以这样写:

class make_xlat_by_whole_words(make_xlat):
  def make_rx(self):
    return re.compile(r'\b%s\b' % r'\b|\b'.join(map(re.escape, self.adict)))

text = "creat, creator"
adict = {
          "creat" : "will not replace creator",
        }
translate = make_xlat_by_whole_words(adict)
print(translate(text))
# => 'will not replace creator, creator'

WarmGrid

Answerers: April and Probe