Pertanyaan Apa cara terbaik untuk membuka file untuk akses eksklusif dengan Python?


Apa cara paling elegan untuk menyelesaikan ini:

  • buka file untuk membaca, tetapi hanya jika belum dibuka untuk menulis
  • buka file untuk menulis, tetapi hanya jika belum dibuka untuk membaca atau menulis

Fungsi built-in berfungsi seperti ini

>>> path = r"c:\scr.txt"
>>> file1 = open(path, "w")
>>> print file1
<open file 'c:\scr.txt', mode 'w' at 0x019F88D8>
>>> file2 = open(path, "w")
>>> print file2
<open file 'c:\scr.txt', mode 'w' at 0x02332188>
>>> file1.write("111")
>>> file2.write("222")
>>> file1.close()

scr.txt sekarang mengandung '111'.

>>> file2.close()

scr.txt ditimpa dan sekarang berisi '222' (pada Windows, Python 2.4).

Solusinya harus bekerja di dalam proses yang sama (seperti pada contoh di atas) serta ketika proses lain telah membuka file.
Lebih disukai, jika program tabrakan tidak akan membuat kunci tetap terbuka.


32
2017-10-09 06:58


asal


Jawaban:


Saya tidak berpikir ada cara lintas platform sepenuhnya. Pada unix, modul fcntl akan melakukan ini untuk Anda. Namun pada windows (yang saya anggap Anda berada di jalur), Anda harus menggunakan modul win32file.

Untungnya, ada implementasi portabel (portalocker) menggunakan platform yang sesuai metode di buku masak python.

Untuk menggunakannya, buka file, lalu panggil:

portalocker.lock(file, flags)

di mana bendera adalah portalocker.LOCK_EX untuk akses tulis eksklusif, atau LOCK_SH untuk berbagi, akses baca.


20
2017-10-09 09:00



Solusinya harus bekerja di dalam proses yang sama (seperti pada contoh di atas) serta ketika proses lain telah membuka file.

Jika dengan 'proses lain' yang Anda maksud 'proses apa pun' (bukan program Anda), di Linux tidak ada cara untuk mencapai hal ini hanya dengan mengandalkan pada panggilan sistem (fcntl & teman). Apa yang kamu inginkan adalah penguncian wajib, dan cara Linux untuk mendapatkannya sedikit lebih terlibat:

Remount partisi yang berisi file Anda dengan mand pilihan:

# mount -o remount,mand /dev/hdXY

Mengatur sgid bendera untuk file Anda:

# chmod g-x,g+s yourfile

Dalam kode Python Anda, dapatkan kunci eksklusif pada file itu:

fcntl.flock(fd, fcntl.LOCK_EX)

Sekarang bahkan kucing tidak akan dapat membaca file sampai Anda melepaskan kunci.


8
2017-10-12 02:46



Ini adalah awal dari win32 setengah dari implementasi portabel, yang tidak memerlukan mekanisme penguncian yang terpisah.

Membutuhkan Python untuk Windows Extensions untuk turun ke api win32, tapi itu cukup banyak wajib untuk python pada windows sudah, dan alternatifnya dapat dilakukan dengan ctypes. Kode dapat diadaptasi untuk mengekspos lebih banyak fungsi jika diperlukan (seperti memungkinkan FILE_SHARE_READ daripada tidak berbagi sama sekali). Lihat juga dokumentasi MSDN untuk CreateFile dan WriteFile panggilan sistem, dan artikel tentang Membuat dan Membuka File.

Seperti yang telah disebutkan, Anda bisa menggunakan standar fcntl modul untuk menerapkan setengah unix ini, jika diperlukan.

import winerror, pywintypes, win32file

class LockError(StandardError):
    pass

class WriteLockedFile(object):
    """
    Using win32 api to achieve something similar to file(path, 'wb')
    Could be adapted to handle other modes as well.
    """
    def __init__(self, path):
        try:
            self._handle = win32file.CreateFile(
                path,
                win32file.GENERIC_WRITE,
                0,
                None,
                win32file.OPEN_ALWAYS,
                win32file.FILE_ATTRIBUTE_NORMAL,
                None)
        except pywintypes.error, e:
            if e[0] == winerror.ERROR_SHARING_VIOLATION:
                raise LockError(e[2])
            raise
    def close(self):
        self._handle.close()
    def write(self, str):
        win32file.WriteFile(self._handle, str)

Begini cara contoh Anda dari atas berperilaku:

>>> path = "C:\\scr.txt"
>>> file1 = WriteLockedFile(path)
>>> file2 = WriteLockedFile(path) #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
    ...
LockError: ...
>>> file1.write("111")
>>> file1.close()
>>> print file(path).read()
111

3
2017-10-09 19:30



EDIT: Saya memecahkannya sendiri! Dengan menggunakan keberadaan direktori & usia sebagai mekanisme penguncian! Mengunci dengan file hanya aman di Windows (karena Linux diam-diam menimpa), tetapi mengunci dengan direktori berfungsi dengan sempurna baik di Linux dan Windows. Lihat GIT saya tempat saya membuat kelas yang mudah digunakan 'lockbydir.DLock' untuk itu:

https://github.com/drandreaskrueger/lockbydir

Di bagian bawah readme, Anda menemukan 3 GITplayer di mana Anda dapat melihat contoh kode dijalankan langsung di browser Anda! Cukup keren, bukan? :-)

Terima kasih atas perhatian anda


Ini adalah pertanyaan asli saya:

Saya ingin menjawab ke parity3 (https://meta.stackoverflow.com/users/1454536/parity3) tetapi saya tidak dapat berkomentar secara langsung ('Anda harus memiliki 50 reputasi untuk berkomentar'), saya juga tidak melihat cara untuk menghubungi dia secara langsung. Apa yang Anda sarankan kepada saya, untuk melaluinya?

Pertanyaan saya:

Saya telah menerapkan sesuatu yang mirip dengan paritas3 yang disarankan di sini sebagai jawaban: https://stackoverflow.com/a/21444311/3693375 ("Dengan asumsi penerjemah Python Anda, dan ...")

Dan itu bekerja dengan cemerlang - di Windows. (Saya menggunakannya untuk menerapkan mekanisme penguncian yang bekerja di seluruh proses yang dimulai secara independen. https://github.com/drandreaskrueger/lockbyfile )

Tetapi selain parity3 mengatakan, itu TIDAK bekerja sama di Linux:

os.rename (src, dst)

Ubah nama file atau direktori src menjadi dst. ... Pada Unix, jika ada dst   dan merupakan file,   itu akan diganti secara diam-diam jika pengguna memiliki izin.   Operasi mungkin gagal pada beberapa rasa Unix jika src dan dst   berada di sistem file yang berbeda. Jika berhasil, penggantian nama akan   menjadi operasi atom (ini adalah persyaratan POSIX).   Pada Windows, jika dst sudah ada, OSError akan dinaikkan   (https://docs.python.org/2/library/os.html#os.rename)

Penggantian diam adalah masalahnya. Di Linux. "Jika sudah ada, OSError akan dinaikkan" sangat bagus untuk tujuan saya. Tetapi hanya pada Windows, sayangnya.

Saya kira parity3 contoh masih bekerja sebagian besar waktu, karena kondisinya jika

if not os.path.exists(lock_filename):
    try:
        os.rename(tmp_filename,lock_filename)

Tapi kemudian semuanya bukan lagi atom.

Karena kondisi jika mungkin benar dalam dua proses paralel, dan kemudian keduanya akan mengganti nama, tetapi hanya satu yang akan memenangkan perlombaan penggantian nama. Dan tidak terkecuali dibesarkan (di Linux).

Ada saran? Terima kasih!

P.S .: Saya tahu ini bukan cara yang tepat, tetapi saya kekurangan alternatif. TOLONG jangan menghukum saya dengan menurunkan reputasi saya. Saya melihat-lihat banyak, untuk menyelesaikan ini sendiri. Bagaimana cara pengguna PM di sini? Dan meh kenapa aku tidak bisa?


2
2018-02-15 23:41



Untuk membuat Anda aman saat membuka file dalam satu aplikasi, Anda dapat mencoba sesuatu seperti ini:

import time
class ExclusiveFile(file):
    openFiles = {}
    fileLocks = []

    class FileNotExclusiveException(Exception):
        pass

    def __init__(self, *args):

        sMode = 'r'
        sFileName = args[0]
        try:
            sMode = args[1]
        except:
            pass
        while sFileName in ExclusiveFile.fileLocks:
            time.sleep(1)

        ExclusiveFile.fileLocks.append(sFileName)

        if not sFileName in ExclusiveFile.openFiles.keys() or (ExclusiveFile.openFiles[sFileName] == 'r' and sMode == 'r'):
            ExclusiveFile.openFiles[sFileName] = sMode
            try:
                file.__init__(self, sFileName, sMode)
            finally:
                ExclusiveFile.fileLocks.remove(sFileName)
         else:
            ExclusiveFile.fileLocks.remove(sFileName)
            raise self.FileNotExclusiveException(sFileName)

    def close(self):
        del ExclusiveFile.openFiles[self.name]
        file.close(self)

Dengan cara itu Anda subclass file kelas. Sekarang lakukan:

>>> f = ExclusiveFile('/tmp/a.txt', 'r')
>>> f
<open file '/tmp/a.txt', mode 'r' at 0xb7d7cc8c>
>>> f1 = ExclusiveFile('/tmp/a.txt', 'r')
>>> f1
<open file '/tmp/a.txt', mode 'r' at 0xb7d7c814>
>>> f2 = ExclusiveFile('/tmp/a.txt', 'w') # can't open it for writing now
exclfile.FileNotExclusiveException: /tmp/a.txt

Jika Anda membukanya terlebih dahulu dengan mode 'w', itu tidak akan memungkinkan lagi terbuka, bahkan dalam mode baca, seperti yang Anda inginkan ...


1
2017-10-09 07:48



Dengan asumsi interpreter Python Anda, dan os dan sistem berkas yang mendasari memperlakukan os.rename sebagai operasi atom dan akan salah ketika tujuan ada, metode berikut ini bebas dari kondisi balapan. Saya menggunakan ini dalam produksi pada mesin linux. Tidak memerlukan lib pihak ketiga dan tidak bergantung pada os, dan selain dari file tambahan, hit performa dapat diterima untuk banyak kasus penggunaan. Anda dapat dengan mudah menerapkan pola dekorator fungsi python atau pengelola konteks 'with_statement' di sini untuk mengabstraksikan kekacauan.

Anda harus memastikan bahwa lock_filename tidak ada sebelum proses / tugas baru dimulai.

import os,time
def get_tmp_file():
    filename='tmp_%s_%s'%(os.getpid(),time.time())
    open(filename).close()
    return filename

def do_exclusive_work():
    print 'exclusive work being done...'

num_tries=10
wait_time=10
lock_filename='filename.lock'
acquired=False
for try_num in xrange(num_tries):
    tmp_filename=get_tmp_file()
    if not os.path.exists(lock_filename):
        try:
            os.rename(tmp_filename,lock_filename)
            acquired=True
        except (OSError,ValueError,IOError), e:
            pass
    if acquired:
        try:
            do_exclusive_work()
        finally:
            os.remove(lock_filename)
        break
    os.remove(tmp_filename)
    time.sleep(wait_time)
assert acquired, 'maximum tries reached, failed to acquire lock file'

EDIT

Telah terungkap bahwa os.rename diam-diam menimpa tujuan di OS non-windows. Terima kasih telah menunjukkan ini @ akrueger!

Berikut ini adalah solusi, yang dikumpulkan dari sini:

Alih-alih menggunakan os.rename yang dapat Anda gunakan:

try:
    if os.name != 'nt': # non-windows needs a create-exclusive operation
        fd = os.open(lock_filename, os.O_WRONLY | os.O_CREAT | os.O_EXCL)
        os.close(fd)
    # non-windows os.rename will overwrite lock_filename silently.
    # We leave this call in here just so the tmp file is deleted but it could be refactored so the tmp file is never even generated for a non-windows OS
    os.rename(tmp_filename,lock_filename)
    acquired=True
except (OSError,ValueError,IOError), e:
    if os.name != 'nt' and not 'File exists' in str(e): raise

@ akrueger Anda mungkin baik-baik saja dengan solusi berbasis direktori Anda, hanya memberi Anda metode alternatif.


1
2018-01-29 22:45