Правка файла скриптом — дело трёх строк: прочитать, заменить, записать. На Windows у каждой из трёх строк есть способ испортить файл так, что заметно это станет не сразу. Мы на всех трёх споткнулись: однажды потеряли файл целиком, однажды сломали сборку фронта на 181 ошибку, однажды не смогли запустить скрипт из-за одного невидимого символа.
Это пятая часть цикла «Мастерская разработчика».
1. open(«w») стирает файл до записи
s = open(p, encoding="utf-8").read()
s = s.replace(old, new)
with open(p, "w", encoding="utf-8") as f: # в этот момент файл уже нулевой длины
f.write(s) # а если запись упала — так и останется
Режим "w" обрезает файл в момент открытия. Если запись после этого упадёт, файл останется пустым. Падает она чаще, чем кажется:
UnicodeEncodeError: surrogates not allowed— в строке, собранной из\uXXXX-последовательностей, осталась половинка суррогатной пары (так бывает с эмодзи);UnicodeEncodeError: 'charmap'изprint()внутри блока записи — консоль Windows в кодировке cp1251 не умеет печатать «→».
Исключение видно, но выглядит как «ничего не сделал». На деле содержимое уже уничтожено. Так мы однажды потеряли рабочий файл на 685 строк — собирали заново по журналам работы, где нашлись его последнее полное чтение и все правки после него.
Правильно — писать во временный файл и подменять одной операцией:
import os, tempfile
def write_atomic(path, text):
d = os.path.dirname(os.path.abspath(path))
fd, tmp = tempfile.mkstemp(dir=d, suffix=".tmp")
try:
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as f:
f.write(text)
os.replace(tmp, path) # атомарно: либо старый файл, либо новый целиком
except BaseException:
os.unlink(tmp)
raise
Проверки (assert old in s) — до открытия на запись. И после правки посмотреть размер: ноль байт — авария, а не «файл не изменился».
2. Текстовый режим переписал все переводы строк
Python на Windows в текстовом режиме переводит каждый \n в \r\n при записи. Скрипт поменял две строки, а записал заново весь файл — уже с CRLF. В диффе это выглядит как изменение каждой строки, а сборка с prettier в линтере падает:
error Delete `␍` prettier/prettier
… и так 181 раз — на каждой строке файла
Хуже всего то, что следует дальше. Watch-сборка пишет «compiled with 1 error» и не пересобирает JS, а CSS при этом собирает. По грепу правка на месте, стили обновились — и полчаса уходит на поиск, почему не работает скрипт.
Лечение — newline="\n" при открытии на запись, как в функции выше, или бинарный режим. Проверка:
tr -cd '\r' < file.js | wc -c # 0 — переводы строк LF
git show HEAD:path/file.js | tr -cd '\r' | wc -c # а как в репозитории?
grep -c $'\r$' в Git Bash врёт и считает все строки — пользуйтесь tr. И не нормализуйте файл, который в репозитории хранится с CRLF: получите дифф на весь файл.
3. PowerShell дописал невидимый символ в начало
Windows PowerShell 5.1 по команде Set-Content -Encoding UTF8 или Out-File пишет файл с меткой порядка байтов — BOM, три байта EF BB BF в начале. Python на такой скрипт отвечает:
SyntaxError: invalid non-printable character U+FEFF
Конфиг nginx, файл .env или shell-скрипт с BOM ломаются так же: первая строка начинается с невидимого символа. Обратная грабля: .ps1 с кириллицей без BOM PowerShell 5.1 читает в системной кодировке и не может разобрать вовсе. Правила простые:
- файлы для Linux и Python не писать средствами PowerShell 5.1;
- передавать файл на сервер — через base64 или
scp, а не черезechoв удалённую команду; Set-Content -Encoding ASCIIмолча меняет кириллицу на?— не выход.
Короткий список
| Симптом | Причина | Что делать |
|---|---|---|
| файл пустой после упавшего скрипта | open("w") обрезал до записи | временный файл + os.replace |
| сборка падает на каждой строке, JS не пересобирается | CRLF после записи в текстовом режиме | newline="\n"; проверка tr -cd '\r' |
U+FEFF в первой строке | BOM от PowerShell 5.1 | писать файлы Python-ом или редактором; на сервер — base64 |
UnicodeEncodeError в print | консоль cp1251 | в вывод только ASCII или PYTHONIOENCODING=utf-8 |
Скрипты и сборка ведут себя «мистически»?
Пришлите, что происходит, — найду, где теряются байты.
// загружаю комментарии…