字符串处理
字符串最主要的处理之一就是分割.
最基础简单的分割方式就是使用split
函数.
"Hello".split("l")
# ["He", "", "o"]
哎?出现了一个空字符串!, 这并不是我们想要的, 再讲如何消去它之前, 我们先来说一下为什么会产生.
可以做做这样的小实验:
"aaaa".split("a")
# ['','','','','']
产生了五个空字符串, 然而我们只传了四个呀. 那么就开始猜测, 会不会是两个字符串的叠加效果呢?(此时可以试试"a".split("a")
)
所以说, 这个情况是在寻找符合条件的字符串的两边时,发现要么没有了, 要么还是原来的条件.
现在提供一个可以快速消去空字符串的方法,不仅仅在这里适用,只要是为了剔除空数据, 都是可以的!
[x for x in ["","","Only",""] if x]
# ["Only"]
这种基本方法还是有一点的笨拙的, 如果遇到字符串中含有多个split点时, 就懵逼了.
所以,还是要请出主角--正则表达式re模块
当你遇到了一个问题的时候, 为了解决它, 你想到了使用正则表达式, 这时, 你就有了两个问题. :)
正则确实强大, 但是今天不会来讨论正则, 如果想要了解, 请前往正则表达式入门教程.
正则模块需要引入.
import re
text = ''
for n in re.split("[,./|]", "Hel.lo,W|or/ld!"):
text += n
print(text)
# HelloWorld!
小结一下:当字符串不是很复杂的时候,仍然建议使用默认的字符串的split
函数, 因为他更快速.如果遇到较为棘手的切割, 最好使用正则的切割.
想象这么一种场景: 你需要获得文件的后缀名/你想知道你个网址使用的协议是什么.
抽象出来就是判断字符串的开头或者结尾.
就如同方法名一样简单, 两个方法分别用来检测头和尾. startswith
和endswith
.
举个例子:
import os, stat
os.listdir('.')
# ['e.py', 'c.java', 'd.cpp', 'b.sh', 'f.sh', 'a.py', 'g.js'] 事先写好了有一些测试用数据
# 接下来使用列表解析找出所有的py和sh
[ file for file in os.listdir('.') if file.endswith(('.py', '.sh')) ]
# ['e.py', 'b.sh', 'f.sh', 'a.py']
方法内可以传入元组进行多适配, 但一定不接受列表.
接下来就可以使用这样的方法找到一堆数据中所有的ftp://
的服务器地址.(略 :) )
下面说一个超实用的正则魔法!
re.sub
可以进行替换, 如果继续利用正则的捕获组就能进行格式的变换,(比如把04/05/2017
变成2017-05-04
).
sub
接受三个参数, 分别是['正则表达式', '替换后的字符串', '需要替换的字符串']
.
接下来再说一下什么是正则中的捕获组.
捕获组简单的说就是一个小整体,还是用实例来说明下:
import re
log = open("/var/log/nginx/access.log", 'r').read()
print(re.sub(r'(\d{2})/([a-zA-Z]{3})/(\d{4})', r'\3-\2-\1', log))
一个一个来说明, access.log
的文件类似
5?.6?.2?.?1? - - [03/May/2017:23:19:25 +0800] "GET /??/ HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.98 Safari/537.36"
这个实例是将所有的时间改为2017-May-03
的形式.
接着来看一下捕获组, 就是那个用括号抱起来的东西. 被抱起来的从0开始计数, 在后面使用反斜线加上组号就可以进行引用.
另外, 正则的捕获组可以添加名字的, 像这样使用: ?P<名字>
, 引用时这样: \g<名字>
.
将上面的例子进行一下更改就变成这样: re.sub(r'(?P
.
说完了分割, 接下来再来看另一个重要的操作--拼接.
传统的拼接方法上面也展示过了:
l = ["Hel", "lo,", "Worl", "d"]
greeting = ''
for part in l:
greeting += part
print(greeting)
# Hello,World
这样拼接还是有一点麻烦的, 更好地方法是使用str.join
方法.
这个方法接受一个可迭代对象作为参数, 调用者(self)就是分割符, 比如:
",".join(['Hello', 'world'])
# Hello,world
但是,我们遗漏了一种情况, 如果传入的可迭代对象包含多个类型的数据怎么办呢?
先试一试吧:
"".join(['123', 456, '789'])
呀!报错了! 看样子还是要处理这个问题的.
一种解决方法是使用列表解析:
example = ['123', 456, '789']
"".join([str(x) for x in example])
这样虽然可以处理, 但如果数据很大的话, 会占用很多资源.
所以, 使用Generator
是更优的选择.
因为他的开销更小.
example = ['123', 456, '789']
"".join((str(x) for x in example))
接着再来说说文本的排版.
我们经常遇到的一种情况是, 对一个字典进行输出.
{
"name":'J',
"age":22,
"sex":1
}
我们期望的输出是:
name : 'J'
age : 22
sex : 1
由于键的长度并不一样, 所以直接输出是做不到的.
str
有几个方法可以帮助我们达成这个目标.
准确的说, 这些方法只是填充字符串而已.
s = 'abc'
s.ljust(10)
# ' abc'
s.ljust(10,'=')
# '=======abc'
s.center(10)
# ' abc '
另外一个类似的方法是使用format
函数, format
接受一个字符串和格式字符串, 使用起来的效果就像是这样:
format(s, '>10')
# ' abc'
format(s,, '^10')
# ' abc '
那么现在来完成开头提出的问题, 排版输出键值长度不一致的字典:
kv = {
'name':'Python',
'createTime':'2017-04-26',
'mtime':'2017-05-03',
'author':'Justin'
}
# 得到字典
m = max(map(len, kv.keys()))
# 得到键的最大长度
for k in d:
print(k.ljust(m), ":", kv[k])
# name : Python
# createTime : 2017-04-26
# mtime : 2017-05-03
# author : Justin
除了分割和拼接, 字符串另一个重要的处理就是删除(叫剔除应该更好),
比如剔除两端的空格, 剔除指定字符,等等..
str类仍然封装了很多丰富的方法.
*strip一类方法
s = "! ++Hello== ?"
s.strip("? !") # "++Hello=="
s.lstrip("+") # "Hello=="
s.rstrip("=") # "Hello"
*strip不能搞定中间的字符, 我们可以通过切片加拼接的方式完成.
比如:
s = "Hel:lo"
s = s[:3] + s[4:]
print(s)
# Hello
但是切片的方法略显笨重,来看一下字符串的replace
方法(别忘了正则的sub
方法呀.)
s = "123\n456\n789"
s = s.replace("\n", "")
s
# "123456789"
但是, 你现在也注意到了, replace
方法只能匹配一个字符, 如果遇到多个就不行了.
比如剔除字符串s = "123.456,789;"
中所有的特殊字符.
这个时候还是需要靠强大的正则了, 也就是之前说的sub
函数.
re.sub("[.,;]", "",s)
就可以了.
最后再来说一下translate
方法, str
和re
, 他们的作用稍有不同, 我们分开来说.
首先, translate
方法可以实现 伪加密(更换顺序)和剔除指定字符的效果. 一个联动的函数是str.maketrans
.
其中, translate
接受一个参数, 一个映射表(需要实现__getitem__
接口), 这个映射表一般有str.maketrans
产生.
str.maketrans
最多接受三个参数, 前两个为映射字符串, 最后一个是将选定的字符串映射成None
, 被映射成None
的字符串会在translate
的过程中被删除.
s = "123\n456\t789\r"
s.translate(str.maketrans("123789", "789123","\n\r\t"))
# "789456123"
到此, 字符串处理的基本就结束了.
函数
Python有个特别好用的函数特性, 叫做装饰器.
装饰器可以帮助我们减少大量代码的冗余, 使得函数的利用更加便利.
一个非常的经典的例子就是这样:
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
这是一个非常经典的斐波那契数列递归,但如果使用这个函数进行fib(50)
的话, 也许你可以先去泡杯咖啡.
一种优化方案是:
def fib(n, cache=None):
if cache is None:
cache = {}
if n <= 1:
return 1
if n in cache:
return cache[n]
cache[n] = fib(n-1, cache) + fib(n-2, cache)
return cache[n]
在这里,我们建立了一个缓存池(原函数缓慢的原因就是因为大量的重复计算, 计算得到的值都没有得到保存.), 如果能从缓存池中读取, 就不再计算而直接读取.
对于这个问题,事实上好多递归函数都存在这个问题.
比如爬楼梯问题:
一个共有10个台阶的楼梯, 从下面走到上面, 一次只能迈1-3的阶梯, 在不能后退的情况下, 走完楼梯一共有多少种方法?
传统的递归解法是:
def climb(n, steps):
if n == 0:
return 1
count = 0
if n >0:
for step in steps:
count += climb(n - step, steps)
return count
计算climb(50, (1,2,3))
试试, 你的咖啡已经凉了.
我们仍然可以考虑加上缓存的方法, 但是那就意味着还要再重新书写一遍近乎是一样的代码.
这时引入装饰器, 装饰器也是个函数, 接受一个函数, 在他的内部生产一个符合要求的函数(比如生成缓存池, 记录日志等等), 再调用参数(也就是传进来的函数), 接着将这个包装的函数返回出去.就成为了一个满足要求的函数.
本来的过程是这样的:
func = wraaper(func)
这样有点麻烦, 所以Python提供了这样的语法糖:
@wraaper
def func():
pass
为上述递归问题写一个装饰器就像是这样:
def caching(func):
cache = {}
def wrap(*args):
if args not in cache:
cache[args] = func(*args)
return cache
return wrap
这样再调用fib
和climb
, 就可秒出结果.
再比如说一个很常用的功能, 我想知道我的函数执行了多长时间.我就可以也写个装饰器:
import time
def record(func):
def wrap(*args):
start = time.time()
func(*args)
end = time.time()
print(end-start)
return wrap
但是这个装饰器不是太实用, 我们可以添加让他在超过一定时间就记录日志的功能.
import time, logging, random
def record(func):
def wrapper(*args):
start = time.time()
res = func(*args)
used = time.time() - start
if used > 2.0:
logging.warn("%s : %s > %s" % (func.__name__, str(used), str(2.0)))
return res
return wrapper
@record
def test():
if randint(0,1):
time.sleep(2.5)
print("Test")
for n in range(10):
test()
接着运行的结果就像是这样:
Test
Test
WARNING:root:test : 2.500215768814087 > 2.0
Test
Test
Test
Test
Test
WARNING:root:test : 2.501840591430664 > 2.0
Test
WARNING:root:test : 2.5007271766662598 > 2.0
Test
Test
WARNING:root:test : 2.5005836486816406 > 2.0
但是这个装饰器仍然不够灵活, 他的时间阈值是个固定值,如果是个可调节的值就更好了.
为了达成装饰器同样可以接受参数, 我们就需要写个三层嵌套的装饰器.
也就是说: 一个返回装饰器的函数.
def log(text):
def decorator(func):
def wrapper(*args **kw):
print("%s %s():" % (text, func.__name__))
res = func(*args, **kw)
return res
return wraaper
return decorator
这就是一个最简单的展示接受参数的装饰器.
使用中你会发现这样的情况.
@log("execute")
def now():
print("2017-05-05")
now()
# execute now():
# 2017-05-05
看起来很正常是吗.但是如果你试试查看now.__name__
的时候就会发现, now
已经被wrapper
污染了.
所以返回的是wrapper
.这并不是我们所希望的.
Python
的functools
包内的wraps
提供了我们需要的功能.
由于wraps
也是一个装饰器, 所以使用起来也非常方便, 在原基础上加上这样的一行代码即可解决问题 :
import functools
def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args **kw):
print("%s %s():" % (text, func.__name__))
res = func(*args, **kw)
return res
return wraaper
return decorator