首先,写这篇文章的起因,是这两天玩了一个网站
用Python做题、出题,交流,很有意思也很有挑战。
————————————进入正题——————————————————————————
其中一道字符串处理的题目如下:
简单来说,输入是一个字符串,输出也是一个字符串。我们要做的,是让输入中的整数每隔三位数加一个小数点。
测试用例:
assert checkio('123456') == '123.456'
assert checkio('333') == '333'
assert checkio('9999999') == '9.999.999'
assert checkio('123456 567890') == '123.456 567.890'
assert checkio('price is 5799') == 'price is 5.799'
assert checkio('he was born in 1966th') == 'he was born in 1966th'
题意应该是表达清楚了。我自己写了一段代码,大概有20~30行的样子,用正则表达式把数字提取出来,然后改掉,然后一段一段填回去。比较麻烦的地方,是修改字符串之后,长度增加了,所以之前在提取字符串的时候要自己处理当前插入位置之类的。
Pass这道题之后,看了下Best solution:
def checkio(txt):
'''
string with comma separated numbers, which inserted after every third digit from right to left '''
return re.sub(r'(?<=\d)(?=(\d\d\d)+\b)', '.', str(txt))
看过之后发现自己对python的加强版正则表达式不够熟悉,看了看这篇文章,补了补功课:
上面的解法非常巧妙,注意一点:(?=...)这种匹配模式是不消耗字符的,所以会形成循环匹配,正好达到了目的。我的意思是:如果不是循环匹配的话,加一个小数点之后可能就继续下一个词了。
经过试验,(?<=...)这种特殊构造有一个限制条件——必须是固定长度的,如果你写(?<=.*?)这种条件,Python会报相应的异常。
————————————re.sub函数————————————————————
这个题的解法很巧妙,当然巧妙也意味着比较特别,通用性不强。
回到之前我的那个破代码,贴出来瞅瞅:
def checkio(txt):
'''
string with dot separated numbers, which inserted after every third digit from right to left
'''
l = []
beg = 0
for e in re.finditer(r'\b\d+\b', txt):
p1, p2 = e.span()
l.append( txt[beg:p1] )
s = txt[p1:p2]
s2 = []
for i in range(len(s)-1, -1, -1):
s2.append(s[i]) if (len(s)-i) % 3 == 0 and i != 0:
s2.append('.')
s2.reverse()
s = ''.join(s2)
l.append( s )
beg = p2
l.append( txt[beg:] )
print ''.join(l)
return ''.join(l)
中间比较长的一段是给字符串加小数点,假如抽出来不看的话,那么多余的地方就是 l、beg、p1、p2等几个变量以及它们的计算了。
仔细玩了下re.findall和re.sub,发现它们查找的方式不大一样,主要是对括号括起来的group处理不太一样。这个问题以前也纳闷过。今天恍然大悟:
re.sub的第二个参数repl,可以是函数!
如果自定义一个函数change(match),在使用re.sub时底层就会在查找到合适的pattern时调用这个change函数。参数是一个match对象,记录了group、span之类的东东。处理这个match对象返回一个字符串,底层就会帮你把这个字符串替换回去。
不容易说明白,试试就明白了。
我用这个办法修改了之前MB文件处理的函数,简洁了很多。re.sub函数的接口设计很自然,值得学习。以前竟然没想到能这么用,小自责一下。