原文:http://www.zopatista.com/python/2013/11/26/inplace-file-rewriting/

使用上下文管理器进行无痛的文件重写。

如果你需要就地处理文件,修改其内容并且重新输出在同一个位置,你可以使用fileinput模块使用inplace选项

1
2
3
4
5
import fileinput
for line in fileinput.input(somefilename, inplace=True):
line = 'additional information ' + line.rstrip('\n')
print line

fileinput模块有一些问题,我觉得最大的就是模块非常依赖于全局变量。比如fileinput.input()创建了一个全局的fileinput.FileInput()对象,在这个模块的其他函数就可以使用它。你当然可以忽略这些并且直接去使用fileinput.FileInput()构造器,但是fileinput.input()却是主的API入口。

其他的问题在于in-place方法劫持了sys.stdout,作为写回替换文件的方法。显然这可以方便的使用print语句来写回,但是你必须记得从以前的文件中读取的数据行中移除新的数据行。

最后,也是很重要的,fileinput在Python3中的版本不支持指定编码方式,错误模式或者处理新数据行。你可以在二进制模式下打开文件,但是输出总是在文本模式下的。这些问题极大的降低了这个库的可用性。

因此我写了一个替代方案,使用了非常棒的`@contextlib.contextmanager装饰器,这个在Python2和Python3都可以使用,依赖于io.open()`来在不同的Python版本中保持兼容性。

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
from contextlib import contextmanager
import io
import os
@contextmanager
def inplace(filename, mode='r', buffering=-1, encoding=None, errors=None,
newline=None, backup_extension=None):
"""允许使用新的内容来替换一个文件
truetrue产生一个(readable, writable)文件对象元组,writable会替换readable
truetrue异常发生的时候,原始文件会被重新保存,移除写入的数据
truetrue模式不应该使用w a 或者 + 只支持只读模式
"""
# move existing file to backup, create new file with same permissions
# borrowed extensively from the fileinput module
if set(mode).intersection('wa+'):
raise ValueError('Only read-only file modes can be used')
backupfilename = filename + (backup_extension or os.extsep + 'bak')
try:
truetruetruetrue# 删除文件
os.unlink(backupfilename)
except os.error:
pass
os.rename(filename, backupfilename)
readable = io.open(backupfilename, mode, buffering=buffering,
encoding=encoding, errors=errors, newline=newline)
try:
truetruetruetrue# 返回文件描述符fd的状态, st_mode文件信息的掩码,包含了文件的权限信息,文件的类型信息
perm = os.fstat(readable.fileno()).st_mode
except OSError:
writable = open(filename, 'w' + mode.replace('r', ''),
buffering=buffering, encoding=encoding, errors=errors,
newline=newline)
else:
os_mode = os.O_CREAT | os.O_WRONLY | os.O_TRUNC
if hasattr(os, 'O_BINARY'):
os_mode |= os.O_BINARY
fd = os.open(filename, os_mode, perm)
writable = io.open(fd, "w" + mode.replace('r', ''), buffering=buffering,
encoding=encoding, errors=errors, newline=newline)
try:
if hasattr(os, 'chmod'):
os.chmod(filename, perm)
except OSError:
pass
try:
yield readable, writable
except Exception:
# move backup back
try:
os.unlink(filename)
except os.error:
pass
os.rename(backupfilename, filename)
raise
finally:
readable.close()
writable.close()
try:
os.unlink(backupfilename)
except os.error:
pass

译者注:首先将原文件备份,然后readable是从备份文件中读取的,writable是读取了一个与原文件名相同的空文件,在yield readable, writable处相当于__enter__方法返回了一个元组,之后如果在上下文管理器中产生了异常就会删除已经写入的文件,然后将备份文件重命名为原文件名,即相当于执行了__exit__方法

这个上下文管理器只关注于一个文件,并且忽略了sys.stdin,不像fileinput模块。它的目标就是只将一个文件原地替换。

使用的例子,Python2:

1
2
3
4
5
6
7
8
9
import csv
with inplace(csvfilename, 'rb') as (infh, outfh):
reader = csv.reader(infh)
writer = csv.writer(outfh)
for row in reader:
row += ['new', 'columns']
writer.writerow(row)

Python3

1
2
3
4
5
6
7
8
9
import csv
with inplace(csvfilename, 'r', newline='') as (infh, outfh):
reader = csv.reader(infh)
writer = csv.writer(outfh)
for row in reader:
row += ['new', 'columns']
writer.writerow(row)