2016-08-23 8 views
1

Как добавить utf8-bom в текстовый файл без open()?Python - могу ли я добавить спецификацию UTF8 в файл, не открывая его?

Теоретически, нам просто нужно добавить utf8-bom в начало файла, нам не нужно читать «все» содержимое?

+5

Добавление чего-то к началу файла включает в себя переписывание всего файла, вы можете добавлять только в конец файлов, а не вставлять содержимое где-нибудь. И вы также не можете изменять файл, не открывая его. Итак, нет: то, что вы хотите, невозможно. – dhke

+0

@dhke 'без его открытия' действительно неточно. У меня много больших файлов, скажем 1giga-байтов. Каков наилучший способ добавления utf8-bom? – minion

+0

@minion: Ничто из того, что вы можете сделать, не позволит читать и писать полный 1 ГБ. Ваш единственный выбор - это временные файлы (с атомарностью и безопасностью, но с более высокими временными требованиями к дисковым пространствам) или модификация места (обычно медленнее, может испортить данные, если они прерываются на полпути, но требует минимального дополнительного дискового пространства). – ShadowRanger

ответ

3

Вам необходимо прочитать данные, потому что вам нужно переместить все данные, чтобы освободить место для спецификации. Файлы не могут просто добавлять произвольные данные. Делать это на месте труднее, чем просто писать новый файл с BOM, затем исходные данные, а затем заменить исходный файл, поэтому самое простое решение, как правило, что-то вроде:

import os 
import shutil 

from os.path import dirname, realpath 
from tempfile import NamedTemporaryFile 

infile = ... 

# Open original file as UTF-8 and tempfile in same directory to add sig 
indir = dirname(realpath(infile)) 
with NamedTemporaryFile(dir=indir, mode='w', encoding='utf-8-sig') as tf: 
    with open(infile, encoding='utf-8') as f: 
     # Copy from one file to the other by blocks 
     # (avoids memory use of slurping whole file at once) 
     shutil.copyfileobj(f, tf) 

    # Optional: Replicate metadata of original file 
    tf.flush() 
    shutil.copystat(f.name, tf.name) # Replicate permissions of original file 

    # Atomically replace original file with BOM marked file 
    os.replace(tf.name, f.name) 

    # Don't try to delete temp file if everything worked 
    tf.delete = False 

Это также подтверждает, что входной файл был фактически UTF-8 побочным эффектом, и исходный файл никогда не существовал в несогласованном состоянии; это либо старые, либо новые данные, а не промежуточные рабочие копии.

Если ваши файлы большие, а дисковое пространство ограничено (так что вы не можете одновременно иметь две копии на диске), то мутация на месте может быть приемлемой. Самый простой способ сделать это mmap модуль, который упрощает процесс перемещения данных вокруг значительно по сравнению с использованием в месте операции объекта файла:

import codecs 
import mmap 

# Open file for read and write and then immediately map the whole file for write 
with open(infile, 'r+b') as f, mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_WRITE) as mm: 
    origsize = mm.size() 
    bomlen = len(codecs.BOM_UTF8) 
    # Allocate additional space for BOM 
    mm.resize(origsize+bomlen) 

    # Copy file contents down to make room for BOM 
    # This reads and writes the whole file, and is unavoidable 
    mm.move(bomlen, 0, origsize) 

    # Insert the BOM before the shifted data 
    mm[:bomlen] = codecs.BOM_UTF8 
1

Если вам нужно на месте обновления, что-то вроде

def add_bom(fname, bom=None, buf_size=None): 
    bom = bom or BOM 
    buf_size = buf_size or max(resource.getpagesize(), len(bom)) 
    buf = bytearray(buf_size) 
    with open(fname, 'rb', 0) as in_fd, open(fname, 'rb+', 0) as out_fd: 
     # we cannot just just read until eof, because we 
     # will be writing to that very same file, extending it. 
     out_fd.seek(0, 2) 
     nbytes = out_fd.tell() 
     out_fd.seek(0) 
     # Actually, we want to pass buf[0:n_bytes], but 
     # that doesn't result in in-place updates. 
     in_bytes = in_fd.readinto(buf) 
     if in_bytes < len(bom) or not buf.startswith(bom): 
      # don't write the BOM if it's already there 
      out_fd.write(bom) 
     while nbytes > 0: 
      # if we still need to write data, do so. 
      # but only write as much data as we need 
      out_fd.write(buffer(buf, 0, min(in_bytes, nbytes))) 
      nbytes -= in_bytes 
      in_bytes = in_fd.readinto(buf) 

должен сделать трюк.

Как вы можете видеть, на месте обновления немного finnicky, потому что вы

  1. Запись данных на месте, вы только чтение. Чтение должно всегда оставаться впереди записи, иначе вы переписываете еще не обработанные данные.
  2. Расширение файла, которое вы читаете, поэтому чтение до EOF не работает.

Кроме того, это может оставить файл в противоречивом состоянии. Скорее всего, копия во временное -> перемещение временного оригинального метода является предпочтительной.

+0

Я добавил альтернативное решение на месте [мой ответ] (http://stackoverflow.com/a/39094533/364696), используя 'mmap' для упрощения работы. Я нахожу это намного проще, чем пытаться выполнить работу с файловыми объектами. – ShadowRanger

+0

@ShadowRanger Приятно, я тоже подумал об этом, посмотрев на исходный код 'cp', вы обнаружите, что он также использует chunked' mmap', чтобы избежать перебора VM для больших файлов. Не так много проблем на 64-битных ОС, но если есть файл с 3 + ГБ, вы не сможете «mmap()» на 32-битной машине. – dhke

+0

Я бы предположил, что это использовало chunked 'mmap', чтобы избежать ограничений памяти VM, а не измельчения, но да, он не масштабируется на 32-битных машинах за пределами (обычно) 1,5 ГБ или около того. Решение: это 2016 год, запустить 64-битную ОС и установить Python. :-) – ShadowRanger