如何将文件创建变为原子操作?
我正在用Python一次性把一段段文字写入文件:
open(file, 'w').write(text)
如果脚本在写文件的时候被中断,导致文件没有写完,我希望这个文件根本不存在,而不是一个写了一半的文件。这样可以做到吗?
7 个回答
因为处理这些细节很容易出错,所以我建议使用一个小型的库来帮忙。使用库的好处是,它可以处理所有这些繁琐的细节,而且这个库是由社区不断审查和改进的。
其中一个这样的库是 python-atomicwrites
,由 untitaker 开发,它甚至支持 Windows 系统:
注意(截至2023年):
这个库目前没有维护。作者的评论:
[...] 我觉得现在是时候不再支持这个包了。Python 3 有 os.replace 和 os.rename,这两个功能对于大多数使用场景来说已经足够好了。
最初的推荐:
来自 README 文件:
from atomicwrites import atomic_write
with atomic_write('foo.txt', overwrite=True) as f:
f.write('Hello world.')
# "foo.txt" doesn't exist yet.
# Now it does.
通过 PIP 安装:
pip install atomicwrites
这是一个简单的代码片段,使用Python的tempfile
模块来实现原子写入,也就是确保写入操作的安全性。
with open_atomic('test.txt', 'w') as f:
f.write("huzza")
甚至可以在同一个文件中进行读写操作:
with open('test.txt', 'r') as src:
with open_atomic('test.txt', 'w') as dst:
for line in src:
dst.write(line)
这里使用了两个简单的上下文管理器。
import os
import tempfile as tmp
from contextlib import contextmanager
@contextmanager
def tempfile(suffix='', dir=None):
""" Context for temporary file.
Will find a free temporary filename upon entering
and will try to delete the file on leaving, even in case of an exception.
Parameters
----------
suffix : string
optional file suffix
dir : string
optional directory to save temporary file in
"""
tf = tmp.NamedTemporaryFile(delete=False, suffix=suffix, dir=dir)
tf.file.close()
try:
yield tf.name
finally:
try:
os.remove(tf.name)
except OSError as e:
if e.errno == 2:
pass
else:
raise
@contextmanager
def open_atomic(filepath, *args, **kwargs):
""" Open temporary file object that atomically moves to destination upon
exiting.
Allows reading and writing to and from the same filename.
The file will not be moved to destination in case of an exception.
Parameters
----------
filepath : string
the file path to be opened
fsync : bool
whether to force write the file to disk
*args : mixed
Any valid arguments for :code:`open`
**kwargs : mixed
Any valid keyword arguments for :code:`open`
"""
fsync = kwargs.pop('fsync', False)
with tempfile(dir=os.path.dirname(os.path.abspath(filepath))) as tmppath:
with open(tmppath, *args, **kwargs) as file:
try:
yield file
finally:
if fsync:
file.flush()
os.fsync(file.fileno())
os.rename(tmppath, filepath)
先把数据写到一个临时文件里,等数据成功写入后,再把这个临时文件改名为目标文件,比如:
with open(tmpFile, 'w') as f:
f.write(text)
# make sure that all data is on disk
# see http://stackoverflow.com/questions/7433057/is-rename-without-fsync-safe
f.flush()
os.fsync(f.fileno())
os.replace(tmpFile, myFile) # os.rename pre-3.3, but os.rename won't work on Windows
根据文档 http://docs.python.org/library/os.html#os.replace
把文件或文件夹
src
改名为dst
。如果dst
是一个非空的文件夹,就会出现OSError
错误。如果dst
已经存在并且是一个文件,且用户有权限的话,它会被静默替换。如果src
和dst
在不同的文件系统上,这个操作可能会失败。如果成功,重命名操作会是一个原子操作(这是 POSIX 的要求)。
注意:
如果
src
和目标位置dst
不在同一个文件系统上,可能就不是原子操作了。如果在像停电、系统崩溃等情况下,性能或响应速度比数据完整性更重要,可以跳过
os.fsync
这一步。