Python常见问题解析

解析不定长的iterable

  • _表示占位符,但只能占一个位置,超过一个位置会抛出ValueError
  • *表示不定长的占位,同时会将不定长的数据放入一个列表中,与切片相比的优势在于可以解析不定长iterable,当然,_和也能配合`_`使用,表示丢弃掉不定长的iterable
    1
    2
    3
    4
    5
    6
    >>> a = (1, 21, 3, 4, 5, 6)
    >>> b, *c, _, _ = a
    >>> print(b)
    1
    >>> print(c)
    [21, 3, 4]

队列

列表的区别:

  • 队列的两端插入和删除的时间复杂度为O(1),列表的两端插入和删除的时间复杂度为O(N)
  • 列表为栈结构,为先进后出,队列为先进先出,列表不限长度,队列默认也为不限长度,但是可以使用maxlen来指定队列长度,超过长度时再append数据时,会先弹出队列第一个数据,再append进队列最后一个数据

队列的相关接口:

  • append
  • appendleft
  • pop
  • popleft
  • extend
  • insert
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    from collections import deque
    >>> a = deque(maxlen=3)
    >>> a.append(1)
    >>> a.append(2)
    >>> a.append(3)
    >>> a.append(4)
    >>> for i in a:
    ... print(i)
    ...
    2
    3
    4
    >>> a[0] = 10
    >>> print(a)
    deque([10, 3, 4], maxlen=3)

提取出iterable中的N个最大数或最小数

  • 可用heapq中的nlargestnsmallest
  • 也可用sorted(iterable, key=key, reverse=True)[:n]sorted(iterable, key=key, reverse=True)[n:]

处理复杂字典类型

1
2
3
4
5
d = {}
for key, value in pairs:
if key not in d:
d[key] = []
d[key].append(value)

相等

1
2
3
4
from collections import defaultdict
d = defaultdict(list)
for key, value in pairs:
d[key].append(value)

字典排序

由于字典是无序的结构,想要控制字典中的顺序,可以使用collections模块中的OrderedDict类,OrderedDict 内部维护着一个根据键插入顺序排序的双向链表,这会在每次插入新元素的时候,会被放入链表尾部。对于一个已经存在的键的重复赋值不会改变键的顺序。
ps:值得注意的是,OrderedDict的空间消耗是普通字典类型的两倍

1
2
3
4
5
6
7
8
9
10
from collections import OrderedDict
import json

d = OrderedDict()
d['foo'] = 1
d['bar'] = 2
d['spam'] = 3
d['grok'] = 4
print(json.dumps(d))
# output:{"foo": 1, "bar": 2, "spam": 3, "grok": 4}

OrderedDict相关接口:

  • popitem
  • move_to_end
  • copy
  • keys
  • values
  • items

zip

zip可与字典配合使用用于根据key或者value排序等等

1
2
3
4
5
6
7
prices = {
'ACME': 45.23,
'AAPL': 612.78,
'IBM': 205.55,
'HPQ': 37.20,
'FB': 10.75 }
print(max(zip(prices.values(), prices.keys())))

值得注意的是:zip()函数创建的是一个只能访问一次的迭代器

1
2
3
prices_and_names = zip(prices.values(), prices.keys())
print(min(prices_and_names)) # OK
print(max(prices_and_names)) # ValueError: max() arg is an empty sequence

字典的key,value和item

字典的 keys() 方法返回一个展现键集合的键视图对象。 键视图的一个很少被了解的特性就是它们也支持集合操作,比如集合并、交、差运算。 所以,如果你想对集合的键执行一些普通的集合操作,可以直接使用键视图对象而不用先将它们转换成一个 set。

字典的 items() 方法返回一个包含 (键,值) 对的元素视图对象。 这个对象同样也支持集合操作,并且可以被用来查找两个字典有哪些相同的键值对。

字典的 values() 方法也是类似,但是它并不支持这里介绍的集合操作。 某种程度上是因为值视图不能保证所有的值互不相同

对于重复数据的思考

set虽然能很好且很快的取出重复数据,但是带来的问题是,由于set中的元素是无序的,会导致set去重之后的序列也会有被打乱的风险,在此提供的一个思路是,使用生成器函数结合set来去重,且当序列中的元素为unhashable时,同样适用。此方法不仅仅适用于处理普通序列,且可以处理文件中消除重复行等操作。

1
2
3
4
5
6
7
def dedupe(items, key=None):
seen = set()
for item in items:
val = item if key is None else key(item)
if val not in seen:
yield item
seen.add(val)

对于iterable中的计数问题

想要得到iterable中出现次数最多的元素的时候,可以采用sort之后手动计数的方法,更为简单的方式是通过collections模块中的Counter函数。

1
2
3
4
5
6
from collections import Counter

words = (
'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' )
count = Counter(words)
print(count.most_common(3)) # output:[('eyes', 8), ('the', 5), ('look', 4)]

Counter提供的常见接口:

  • most_common

  • update

值得注意的是,Counter支持常见的算术运算,包含+,-,and,or

字典列表排序问题优化

在此引入operator模块中的itemgetter函数,f = itemgetter(2)时,调用f(r)返回r[2],当然,也可以传入多个参数,返回为一个包含多个下标的元祖。对于排序的影响在于,sorted函数排序时,可以指定按照指定的值排序。例如:

1
rows_by_fname = sorted(rows, key=itemgetter('fname'))

这表明会按照fname来排序,当然也可以传入多个参数来排序,但是有时候也可以使用lambda 来代替

1
rows_by_fname = sorted(rows, key=lambda r: r['fname'])

相比而言,itemgetter方式会更快一些

排序不支持原生比较的对象

如果需要排序的是一个实例序列(类似于[User(3), User(5), User(6)]),想通过实例的属性来进行排序时,可使用operator模块中的attrgetter函数,例如:

1
2
3
4
5
6
7
8
9
10
11
12
from operator import attrgetter
class User:
def __init__(self, user_id):
self.user_id = user_id

def __repr__(self):
return 'User({})'.format(self.user_id)


def sort_notcompare():
users = [User(23), User(3), User(99)]
print(sorted(users, key=attrgetter('user_id')))

当然也可以通过lambda排序,但lambda排序会慢一点

1
sorted(users, key=lambda u: u.user_id)

对于attrgetter函数,提供了如下接口:

  • f = attrgetter('name'),调用f(r)时,返回f.name

  • f = attrgetter('name','date'),调用f(r)时,返回`(f.name,f.date)``

  • f = attrgetter('name.first','name.last'),调用f(r)时,返回(f.name.first,f.name.last)

字典列表的分组排序

当需要对一个字典或实例的列表采用分组排序时,在此引入itertools模块中的groupby函数,作用类似于MYSQL中的group by函数,使用此函数的前提在于,需要进行分组排序的列表,必须提前根据需要分组的元素进行了排序。以下为示例:

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
from operator import itemgetter
from itertools import groupby
rows = [
{'address': '5412 N CLARK', 'date': '07/01/2012', 'name': 'jack'},
{'address': '5148 N CLARK', 'date': '07/04/2012', 'name': 'mary'},
{'address': '5800 E 58TH', 'date': '07/02/2012', 'name': 'tom'},
{'address': '2122 N CLARK', 'date': '07/03/2012', 'name': 'bob'},
{'address': '5645 N RAVENSWOOD', 'date': '07/02/2012', 'name': 'jay'},
{'address': '1060 W ADDISON', 'date': '07/02/2012', 'name': 'peter'}, {'address': '4801 N BROADWAY', 'date': '07/01/2012', 'name': 'jack'}, {'address': '1039 W GRANVILLE', 'date': '07/04/2012', 'name': 'jan'},]
# Sort by the desired field first
rows.sort(key=itemgetter('date'))
for date, items in groupby(rows, key=itemgetter('date')):
print(date)
for i in items:
print(' ', i)
#output
'''
07/01/2012
{'address': '5412 N CLARK', 'date': '07/01/2012', 'name': 'jack'}
{'address': '4801 N BROADWAY', 'date': '07/01/2012', 'name': 'jack'}
07/02/2012
{'address': '5800 E 58TH', 'date': '07/02/2012', 'name': 'tom'}
{'address': '5645 N RAVENSWOOD', 'date': '07/02/2012', 'name': 'jay'}
{'address': '1060 W ADDISON', 'date': '07/02/2012', 'name': 'peter'}
07/03/2012
{'address': '2122 N CLARK', 'date': '07/03/2012', 'name': 'bob'}
07/04/2012
{'address': '5148 N CLARK', 'date': '07/04/2012', 'name': 'mary'}
{'address': '1039 W GRANVILLE', 'date': '07/04/2012', 'name': 'jan'}
'''

过滤列表中的元素

提供以下几种思路

  • 使用列表生成式,优点在于代码量小,缺点在于过滤复杂条件麻烦且不宜读,而且会占用大量内存
  • 使用filter函数,返回一个迭代器,而且可以将复杂的过滤条件封装在一个函数中
  • 使用itertools模块中的compress函数,也返回一个迭代器,而且采用的是一个Boolean列表来过滤另外一个列表

列表生成式过滤数据:

1
2
>>> mylist = [1, 4, -5, 10, -7, 2, 3, -1]
>>> [n for n in mylist if n > 0]

filter函数:

1
2
3
4
5
6
7
8
9
values = ['1', '2', '-3', '-', '4', 'N/A', '5']
def is_int(val):
try:
x = int(val)
return True
except ValueError:
return False
ivals = list(filter(is_int, values))
print(ivals)

compress函数:

1
2
3
4
5
6
7
8
9
10
11
addresses = [
'5412 N CLARK',
'5148 N CLARK',
'5800 E 58TH',
'2122 N CLARK',
'5645 N RAVENSWOOD',
'1060 W ADDISON',
'4801 N BROADWAY',
'1039 W GRANVILLE',
]
counts = [ 0, 3, 10, 4, 1, 7, 6, 1]
1
2
3
4
5
6
>>> from itertools import compress
>>> more5 = [n > 5 for n in counts]
>>> more5
[False, False, True, False, False, True, True, False]
>>> list(compress(addresses, more5))
['5800 E 58TH', '1060 W ADDISON', '4801 N BROADWAY']

过滤字典

可采用字典推导式

合并多个字典

  • 可采用dict中的update来讲两个字典合并为一个字典

  • 可使用collections模块中的ChainMap函数,而且该函数只会创建一个临时的合并字典,以供数据采用,所以效率会更高一点

1
2
a = {'x': 1, 'z': 3 }
b = {'y': 2, 'z': 4 }
1
2
3
4
from collections import ChainMap
c = ChainMap(a,b)
print(c['x']) # Outputs 1 (from a)
print(c['y']) # Outputs 2 (from b)

ChainMap提供以下接口:

  • new_child新加入一个字典进入一个ChainMap

  • parents返回父节点

  • maps返回字典列表形式

如果出现重复键,那么第一次出现的映射值会被返回

字符串分割问题

  • 大多数情况可以使用str.splite满足要求

  • 更好的选择可以是使用re模块的splite函数

1
2
3
4
>>> line = 'asdf fjdk; afed, fjek,asdf, foo'
>>> import re
>>> re.split(r'[;,\s]\s*', line)
['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']

值得注意的是,需要选择是否使用括号来捕捉分组。如果又想使用括号,并且不想捕捉括号内分组,可使用?:...模式

1
2
>>> re.split(r'(?:,|;|\s)\s*', line)
['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']

re.splite提供了maxsplite参数,默认为0,表示将字符串中所有值都切分,如果不为0,表示将返回包含给定个数字符串的列表,剩余元素将作为最后一个元素输出

1
2
>>> print(re.split('(?:\s|,|;)\s*', line, 3))
['asdf', 'fjdk', 'afed', 'fjek,asdf, foo']

关于字符串开头或结尾的思考

str提供了startwsitchendswitch函数来检查字符串的开头或结尾,值得注意的是,传参只能为str类型或tuple类型。当然,参数也支持startend,作为开始和结束位置的检查点

对于开头或结尾的处理,使用切片或者是正则表达式也是可以的,但是使用startwsitchendswitch会更快且更方便

1
2
3
>>> url = 'http://www.python.org'
>>> print(url.startswith('www', 7))
True

关于字符串替换

  • 对于一般情况,可使用str.replace()即可满足
  • 更多情况,需要使用re模块的sub函数,来个性化定制需要替换的值
  • str提供了translate()来替换和清理较为复杂的字符串
1
2
3
4
>>> text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> import re
>>> re.sub(r'(\d+)/(\d+)/(\d+)', r'\3-\1-\2', text)
'Today is 2012-11-27. PyCon starts 2013-3-13.'

反斜杠数字比如 \3 指向前面模式的捕获组号。如果需要使用相同模式来做多次替换,可以考虑先编译来提升性能

而对于更加复杂的替换,可以使用一个回调函数来替代

1
2
3
4
5
6
7
>>> from calendar import month_abbr
>>> def change_date(m):
... mon_name = month_abbr[int(m.group(1))]
... return '{} {} {}'.format(m.group(2), mon_name, m.group(3))
...
>>> datepat.sub(change_date, text)
'Today is 27 Nov 2012. PyCon starts 13 Mar 2013.'

如果想要查看发生了多少次替换,可用re.subn()来代替

1
2
3
4
5
>>> newtext, n = datepat.subn(r'\3-\1-\2', text)
>>> newtext
'Today is 2012-11-27. PyCon starts 2013-3-13.'
>>> n
2

ps:对于大多数正则表达式来说,都提供一个flags参数,flags=re.IGNORECASE是,表示忽略大小写

多行匹配模式

对于正则表达式来说,点(.)并不支持换行符匹配,对于需要多行匹配的问题,提供两种思路:

  • 在正则表达式中加入换行匹配规则,例:re.compile(r'/\*((?:.|\n)*?)\*/')
  • re.compile()函数接受一个参数叫re.DOTALL,表示点(.)接受包括换行符在内的任意字符
1
2
3
>>> comment = re.compile(r'/\*(.*?)\*/', re.DOTALL)
>>> comment.findall(text2)
[' this is a\n multiline comment ']

字符串对齐

  • str提供了ljust() , rjust()center() 方法可以指定对齐方式
  • 使用foemat()函数对齐
1
2
3
4
5
6
>>> format(text, '>20')
' Hello World'
>>> format(text, '<20')
'Hello World '
>>> format(text, '^20')
' Hello World '

填充字符

1
2
>>> format(text, '=>20s')
'=========Hello World'

格式化多个值

1
2
>>> '{:>10s} {:>10s}'.format('Hello', 'World')
' Hello World'

关于字符串拼接的思考

  • 最主流的还是使用str提供的join函数
  • 对于简单的拼接也可使用+format()函数

对于性能的考虑

使用+连接符去操作大量字符串的效率非常低下,因为加号会带来内存复制和垃圾回收,应尽量避免写下面的函数

1
2
3
s = ''
for p in parts:
s += p

更为聪明的做法是使用生成器表达式

1
2
3
>>> data = ['ACME', 50, 91.1]
>>> ','.join(str(d) for d in data)
'ACME,50,91.1'

同时,注意不必要的字符连接

1
2
3
print(a + ':' + b + ':' + c) # Ugly
print(':'.join([a, b, c])) # Still ugly
print(a, b, c, sep=':') # Better

关于字节类型的字符串(byte类型)

  • 字节类型的字符串支持大部分字符串的操作,例如:replace,splite,切片,正则表达式

  • 值得注意的是,字节类型的字符串通过索引返回的是整数而非操作数,例如

    1
    2
    3
    >>> a = b'hello'
    >>> print(a[0])
    104

ps:与python2对比,字节类型的字符串与普通字符串并无区别,例如:

1
2
3
4
5
>>> print type(bytes('hello'))
<type 'str'>
>>> a = b'hello'
>>> print a[0]
h

字节类型与字符串类型的相互转换:

1
2
3
4
5
6
>>> a = b'hello'
>>> b = a.decode('ascii')
>>> print(b)
hello
>>> print(b.encode('ascii'))
b'hello'

关于数字的处理

四舍五入

一般情况,可用python内置函数round(value, ndigits)即可,ndigits表示对于小数点后几位来四舍五入,当然,也可为负数,表示对整数后几位进行四舍五入

浮点数精度问题

1
2
3
4
5
6
>>> a = 4.2
>>> b = 2.1
>>> print(a+b)
6.300000000000001
>>> print((a+b) == 6.3)
False

这些错误是由底层CPU和IEEE 754标准通过自己的浮点单位去执行算术时的特征.所以没法自己去避免这些误差

想要无误差处理浮点数精度问题,可参考decimal模块Decimal函数

1
2
3
4
from decimal import Decimal
a = Decimal('4.2')
b = Decimal('2')
print(a+b) #output:6.2

随机选择

random.choice()表示随机选择一个字符

1
2
3
4
>>> import random
>>> values = [1, 2, 3, 4, 5, 6]
>>> random.choice(values)
2

random.sample()表示随机选择n个字符

1
2
>>> random.sample(values, 2)
[6, 2]

random.shuffle()表示打乱排序

1
2
3
>>> random.shuffle(values)
>>> values
[2, 4, 6, 5, 3, 1]

random.randint() 表示随机选择一个整数

1
2
>>> random.randint(0,10)
2

random.random() 表示随机生成一个从0到1的浮点数

1
2
>>> random.random()
0.9406677561675867

random.getrandbits(k) 表示随机生成k为二进制随机数的整数

1
2
>>> random.getrandbits(10)
512

PS:值得注意的是,random模块采用的是Mersenne Twister 算法来计算生成随机数。这是一个确定性算法, 但是你可以通过 random.seed() 函数修改初始化种子。所以,对于安全性要求高的应尽量避免random模块的使用

时间与日期

时间段

1
2
3
4
5
6
>>> from datetime import timedelta
>>> a = timedelta(days=2, hours=6)
>>> print(a)
2 days, 6:00:00
>>> print(a.days)
2

值得注意的是,timedelta函数并没有提供的时间段函数,使用timedelta的优势在于可是与datetime的时间做运算

1
2
3
4
>>> from datetime import datetime
>>> a = datetime(2012, 9, 23)
>>> print(a + timedelta(days=10))
2012-10-03 00:00:00

迭代器与生成器

使用生成器实现深度优先算法

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
class Node:
def __init__(self, value):
self._value = value
self._children = []

def __repr__(self):
return 'Node({!r})'.format(self._value)

def add_child(self, node):
self._children.append(node)

def __iter__(self):
return iter(self._children)

def depth_first(self):
yield self
for c in self:
yield from c.depth_first()

# Example
if __name__ == '__main__':
root = Node(0)
child1 = Node(1)
child2 = Node(2)
root.add_child(child1)
root.add_child(child2)
child1.add_child(Node(3))
child1.add_child(Node(4))
child2.add_child(Node(5))

for ch in root.depth_first():
print(ch)
# Outputs Node(0), Node(1), Node(3), Node(4), Node(2), Node(5)

反向迭代

对于一个iterable可以使用reversed()函数来实现反向迭代,例如

1
print(reversed([1, 2, 3]))

当然,列表的内存消耗过大,如果处理大文件形成的列表可以转换为元祖进行处理。

除此之外,可以使用自定义类中的魔法函数__reversed__()来实现反向迭代,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Countdown:
def __init__(self, start):
self.start = start

# Forward iterator
def __iter__(self):
n = self.start
while n > 0:
yield n
n -= 1

# Reverse iterator
def __reversed__(self):
n = 1
while n <= self.start:
yield n
n += 1

相比而言,反向迭代器运行非常高效,因为它不再需要将数据填充到一个列表中然后再去反向迭代这个列表。

生成器与迭代器切片

生成器与迭代器并不能像普通列表切片一样,可以通过itertools模块中的islice()函数实现切片,例

1
2
3
4
5
6
from itertools import islice
a = iter([1, 2, 3, 4, 5])
b = islice(a, 1, 2)
for i in b:
print(i)
#output:2

值得注意的是,islice()是会消耗掉迭代器或生成器中的值,因为必须考虑到迭代器是一个不可逆的过程

1
2
3
for i in a:    
print(i)
#output:3, 4, 5

处理文件中需要跳过的部分

顾名思义,跳过不需要的部分,可使用itertools模块中的dropwhile()函数,例如,可跳过注释部分,代码如下

1
2
3
with open(path, 'r') as f:
for line in dropwhile(lambda line: line.startswith('#'), f):
print(line, end='')

值得注意的是,dropwhile函数只能跳过文章开头的部分,并不能跳过中间满足条件的部分

如果想要提取文件中的部分值,仍可通过islice函数获取

1
2
3
with open(path, 'r') as f:
for line in islice(f, 2, 5):
print(line, end='')

如果想要跳过所有满足条件的注释行,可改动下代码

1
2
3
4
with open(path, 'r') as f:
lines = (line for line in f if not line.startswith('#'))
for line in lines:
print(line, end='')

同时迭代多个序列

简言之,就是将多可序列+起来,可使用内置函数zip()zip()函数返回一个迭代器,并且迭代长度和参数中最短序列长度一致

1
2
3
4
5
a = [1, 2, 3]
b = ['m', 'n']
for i in zip(a, b):
print(i)
#output:(1, 'm'),(2, 'n')

当然,返回参数中最大长度也是可以的,可以使用itertools模块中的zip_longest函数

1
2
3
4
5
6
from itertools import zip_longest
a = [1, 2, 3]
b = ['m', 'n']
for i in zip_longest(a, b, fillvalue=0):
print(i)
#output:(1, 'm'),(2, 'n'),(3, 0)

关于多个可迭代对象求和

1
2
3
4
a = [1, 2, 4]
b = [5, 6, 7]
for i in a+b:
print(i)

通过这种常规的方式是可以达到目的的,有两个问题:

  • 列表过大时,消耗内存也会太大
  • a,b不是同一种类型时,这种操作会抛异常

所以,在此引入itertools模块中的chain函数,上面的可改为:

1
2
3
4
5
from itertools import chain
a = [1, 2, 4]
b = [5, 6, 7]
for i in chain(a, b):
print(i)

chain的优势在于,可处理两种不同类型的iterable,且会省内存

关于嵌套序列的处理

关于嵌套序列的处理存在多种方法,在此使用生成器的方式,在于节省内存且代码优雅,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
from collections import Iterable

def flatten(items, ignore_types=(str, bytes)):
for x in items:
if isinstance(x, Iterable) and not isinstance(x, ignore_types):
yield from flatten(x)
else:
yield x

items = [1, 2, [3, 4, [5, 6], 7], 8]
# Produces 1 2 3 4 5 6 7 8
for x in flatten(items):
print(x)

文件与IO

关于文件读取

  • open()函数打开文件时,对于换行符的识别在UNIX和Windows下的识别是不一样的(分别为\n\r\n),默认情况下,python会统一处理换行符,并在输出时,将换行符替换为\n,当然也可以手动指定换行符,使用newline参数指定

  • 在文件编码时,可能出现编码和解码方式不一样而导致打开文件失败的情况,可指定error参数,来处理打开失败的情况

1
2
3
with open(path, 'rt', encoding='ascii', errors='replace') as f: 
print([f.read()])
#output:['172.24.107.153\n������']
1
2
3
with open(path, 'rt', encoding='ascii', errors='ignore') as f:
print([f.read()])
#output:['172.24.107.153\n']

关于print函数

1
print(self, *args, sep=' ', end='\n', file=None)

其中seq是指定多个参数时的分隔符,默认为空格;end是在输出的末尾加上需要的字符,使用end参数在输出中禁止换行,默认为换行符;file是指流文件,可以用作重定向字符到文件中

1
print(2019, 7, 24, sep='-', end='!!!')

值得注意的是,对于参数合并时,''.join也支持字符串合并,但是仅支持字符串合并,而sep参数能将不同类型的参数合并在一起

处理文件不存在时才能写入

言下之意是,在目录中不能存在这个文件名,如果存在就会抛异常,通常会存在以下两种方案:

  • 通过os.path.exists判断该文件是否存在
  • open函数提供了x参数,表示会创建一个文件,并以写的方式打开,且文件如果已经存在会抛异常
1
2
with open(path, 'x') as f:  
f.write('write text')

值得注意的是,x参数是python3才引入的,之前版本并不支持

字符串I/O操作

在涉及到需要创建一个文件来存储数据时,通常存在以下两种方式:

  • 在本地磁盘创建一个文件,以写的方式放入数据,用完之后再删了,这样的有点在于不会存在太大的内存限制,缺点在于可能会存在读写速度问题
  • 使用io.StringIO()函数来处理,优势在于将数据存在内存中,读写速度会高于磁盘读写,缺点在于,数据量过大可能会带来内存问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# write a file
>>> from io import StringIO
>>>
>>> s = StringIO()
>>> s.write('这是一个文件io操作\n')
11
>>> print('今天是星期四\n',file=s)
>>> print('明天星期五\n',end='',file=s)
>>> s.getvalue()
'这是一个文件io操作\n今天是星期四\n\n明天星期五\n'
# read a file
>>> s = StringIO('today\n')
>>> s.read(3)
'tod'

同理,涉及到二进制数据时,需要用BytesIO函数来代替

需要注意的是, StringIOBytesIO 实例并没有正确的整数类型的文件描述符。 因此,它们不能在那些需要使用真实的系统级文件如文件,管道或者是套接字的程序中使用。

固定字符串长度迭代文件

1
2
3
4
5
6
7
8
from functools import partial

RECORD_SIZE = 32

with open('somefile.data', 'rb') as f:
records = iter(partial(f.read, RECORD_SIZE), b'')
for r in records:
...

这样会不断产生一个固定大小的数据块,当然也可以自己根据需要产生的数据块进行逻辑处理。

需要注意的地方在于,普通文本的处理方式,默认迭代方法为一行一行的读取,这通常是更普遍的做法

关于os.path处理文件路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> import os
>>> path = '/Users/beazley/Data/data.csv'

>>> os.path.basename(path) # 获取文件名
'data.csv'

>>> os.path.dirname(path) # 获取文件目录
'/Users/beazley/Data'

>>> os.path.join('tmp', 'data', os.path.basename(path)) # 组合路径
'tmp/data/data.csv'

>>> path = '~/Data/data.csv'
>>> os.path.expanduser(path) # 获取文件绝对路径
'/Users/beazley/Data/data.csv'

>>> os.path.splitext(path) # 分离扩展名和路径
('~/Data/data', '.csv')
1
2
3
4
5
6
7
8
9
10
11
12
>>> os.path.isfile('/etc/passwd')
True
>>> os.path.isdir('/etc/passwd')
False
>>> os.path.islink('/usr/local/bin/redis-sentinel')
True
>>> os.path.realpath('/usr/local/bin/redis-sentinel')
'/usr/local/bin/redis-server'
>>> os.path.getsize('/usr/local/bin/redis-sentinel')
2109680
>>> os.path.getmtime('/usr/local/bin/redis-sentinel')
1472713782.0

不同类型文件格式处理

csv文件处理

  1. 读取csv文件

    包含两种读取方式,可通过字符串读取和字典读取

    1
    2
    3
    4
    5
    import csv
    with open('stocks.csv') as f:
    f_csv = csv.reader(f)
    headers = next(f_csv)
    for row in f_csv:
    1
    2
    3
    4
    import csv
    with open('stocks.csv') as f:
    f_csv = csv.DictReader(f)
    for row in f_csv:
  2. 写入csv文件

    也包含两种写入方式,可通过普通字符串写入和字典写入

    1
    2
    3
    4
    with open('stocks.csv','w') as f:
    f_csv = csv.writer(f)
    f_csv.writerow(headers)
    f_csv.writerows(rows)
1
2
3
4
with open('stocks.csv','w') as f:
f_csv = csv.DictWriter(f, headers)
f_csv.writeheader()
f_csv.writerows(rows)
  1. csv文件中分割字符串

    csv.reader()函数中带有delimiter参数可指定分割方式

    1
    2
    3
    with open('stock.tsv') as f:
    f_tsv = csv.reader(f, delimiter='\t')
    for row in f_tsv:
  2. csv文件字符转换

    由于csv中读取的数据都为字符串类型,只能手动操作数据类型转换

    1
    2
    3
    4
    5
    6
    col_types = [str, float, str, str, float, int]
    with open('stocks.csv') as f:
    f_csv = csv.reader(f)
    headers = next(f_csv)
    for row in f_csv:
    row = tuple(convert(value) for convert, value in zip(col_types, row))

    当然这样存在风险性,是因为实际情况中csv文件或多或少存在缺失的数据,这样可能导致数据转换抛出异常

处理json数据

处理xml文件

python处理xml文件通常存在多种方式,这里分别以处理简单文件和大文件为例

  • 处理一般文件,通常使用ElementTree模块,python3.3之后会自动寻找可用的C库来加快速度
1
2
3
4
try:
import xml.etree.cElementTree as ET
except ImportError:
import xml.etree.ElementTree as ET

解析根节点

1
2
3
tree = ET.parse('111.xml')
root = tree.getroot()
#<Element data at 0x1f74dabef48>, so root is data

而获取root的原因在于方便后面解析使用, 通常情况,xml结构标识为<tag attrib1=1>text</tag>tail

1
2
3
4
5
6
7
#source data:<country name="Liechtenstein">
>>> root[0].attrib #获取属性
{'name': 'Liechtenstein'}
>>> root[0][0].text #获取文本
2
>>> root[0][0].tag #获取标签
rank

当然,root也是可以迭代的

1
2
for i in root: 
print(i.tag, i.attrib, i.text, i.tail, sep=';', end='')

也可以是根据某一个标签进行迭代

1
2
3
4
5
6
7
8
for i in root.findall('country'):	#遍历所有符合条件子节点
...
for i in root.find('country'): #遍历第一个符合条件子节点
...
for i in root.findtext('country'): #只遍历文本
...
for i in root.iter('country'): #以当前节点为树节点
...

修改xml

需要注意的是,xml中所有字符均为字符串类型,需要注意字符转换

1
2
3
4
5
rank.text = str(new_rank)  # 必须将int转为str
rank.set("updated", "yes") # 添加属性
del rank.attrib['updated'] #删除属性
tree.write('111.xml') #将数据写入磁盘
root.remove(country) #删除子节点

修改之后的内容只是放在内存中,所以需要将内存里面的数据保存到磁盘中

  • 处理大型xml文件

    当然,处理大型文档,除了使用固有的函数模块之外,还可以使用普通文档解析方式,这样只不过会导致取值更麻烦而已

    其实只要一想到处理大型数据,就应该第一时间想到迭代器或者生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from xml.etree.ElementTree import iterparse

def parse_and_remove(filename, path):
path_parts = path.split('/')
doc = iterparse(filename, ('start', 'end'))
# Skip the root element
next(doc)

tag_stack = []
elem_stack = []
for event, elem in doc:
if event == 'start':
tag_stack.append(elem.tag)
elem_stack.append(elem)
elif event == 'end':
if tag_stack == path_parts:
yield elem
elem_stack[-2].remove(elem)
try:
tag_stack.pop()
elem_stack.pop()
except IndexError:
pass

iterparse() 方法允许对XML文档进行增量操作。 使用时,你需要提供文件名和一个包含下面一种或多种类型的事件列表: start, end, start-nsend-ns 。由 iterparse() 创建的迭代器会产生形如 (event, elem) 的元组, 其中 event 是上述事件列表中的某一个,而 elem 是相应的XML元素。

start 事件在某个元素第一次被创建并且还没有被插入其他数据(如子元素)时被创建。 而 end 事件在某个元素已经完成时被创建。

yield 之后的下面这个语句才是使得程序占用极少内存的ElementTree的核心特性:

1
elem_stack[-2].remove(elem)

这个语句使得之前由 yield 产生的元素从它的父节点中删除掉。 假设已经没有其它的地方引用这个元素了,那么这个元素就被销毁并回收内存。

对节点的迭代式解析和删除的最终效果就是一个在文档上高效的增量式清扫过程。 文档树结构从始自终没被完整的创建过。尽管如此,还是能通过上述简单的方式来处理这个XML数据。

将字典类型数据转换为xml

存在两种解决方案:

  • 手动构造,以字符串的format函数替代的方式来构造,不过这样显得有点蠢
  • 使用xml.etree.ElementTree模块中的Element函数
1
2
3
4
5
6
7
8
9
10
11
12
from xml.etree.ElementTree import Element, tostringdef
dict_to_xml(tag, d):
elem = Element(tag)
for key, val in d.items():
child = Element(key)
child.text = str(val)
elem.append(child)
return elem
s = {'name': 'GOOG', 'shares': 100, 'price': 490.1}
e = dict_to_xml('stock', s) #<Element 'stock' at 0x000001CE0548C908>
print(tostring(e).decode('utf-8'))
#<stock><name>GOOG</name><shares>100</shares><price>490.1</price></stock>

这样做的目的在于,可以通过查询数据库中的值放进字典中,利用字典生成xml文件

-------------本文结束感谢您的阅读-------------