Python: от данных до своей сводки Урок 11 из 56
Файлы и pathlib: путь, который не ломается на чужой машине
Одиннадцатый урок курса по Python. Программа пишет данные, читает их построчно и кладёт отчёт рядом. Путь строит `Path`, а не склейка строк: в Windows тот же путь пишется через обратную косую. И кодировку указывают всегда — без неё её выбирает система, а не вы.
Зачем это нужно
В прошлом уроке мы поймали FileNotFoundError и на этом остановились. Сегодня файл открывается по-настоящему — и выясняется, что сломать это можно двумя способами ещё до чтения.
Первый: путь. Строка "data/2026/cpi.csv" работает у вас и может не работать у того, кто запустит программу из другой папки или на другой системе.
Второй: кодировка. Файл на диске — это байты; текстом их делает договор о том, как эти байты читать. Не назовёте договор — его выберет за вас операционная система, и казахский текст приедет крякозябрами.
Сразу целиком
Файл fail.py. Запуск: python fail.py из окружения.
Обязательное — первые два блока: Path, запись, чтение через with и encoding. Третий блок пишет отчёт и убирает за собой; он же показывает, что файл, который создала программа, программа же должна уметь удалить.
"""Урок 11: файл — это путь, кодировка и договор о том, что внутри.
Путь строят не склейкой строк, а Path: одна и та же программа должна работать
и на macOS, и в Windows. Кодировку пишут всегда: без неё её выберет система.
"""
from pathlib import Path
# Папка, где лежит сама программа. Не текущая папка: она зависит от того,
# откуда программу запустили, и относительный путь тогда ломается.
HERE = Path(__file__).parent
data = HERE / "dannye.csv" # / здесь соединяет части пути, а не делит
report = HERE / "otchet.txt"
rows = ["2021;8.0", "2022;15,0", "2023;n/a", "2024;8.7"]
data.write_text("\n".join(rows) + "\n", encoding="utf-8")
print("== что записали")
print(f"имя: {data.name}")
print(f"без точки: {data.stem}, расширение: {data.suffix}")
print(f"есть: {data.exists()}, размер: {data.stat().st_size} байт")
print()
print("== читаем построчно")
good = []
bad = []
with data.open(encoding="utf-8") as source:
for line in source:
year, _, value = line.strip().partition(";")
try:
good.append((int(year), float(value.replace(",", "."))))
except ValueError:
bad.append(year)
print("разобрано: ", good)
print("не вышло: ", bad)
print()
print("== пишем отчёт рядом с данными")
lines = []
for year, value in good:
lines.append(f"{year}: {value:.1f}%")
report.write_text("\n".join(lines) + "\n", encoding="utf-8")
print(report.read_text(encoding="utf-8"), end="")
# Учебные файлы за собой убираем.
data.unlink()
report.unlink()
print(f"убрано, файлов осталось: {len(list(HERE.glob('*.csv')))}")
Выводит:
== что записали
имя: dannye.csv
без точки: dannye, расширение: .csv
есть: True, размер: 37 байт
== читаем построчно
разобрано: [(2021, 8.0), (2022, 15.0), (2024, 8.7)]
не вышло: ['2023']
== пишем отчёт рядом с данными
2021: 8.0%
2022: 15.0%
2024: 8.7%
убрано, файлов осталось: 0
Разбор
Косая черта, которая соединяет
data = HERE / "dannye.csv"
Path переопределяет /: между путём и именем это не деление, а соединение. Разделитель подставляет сама система, и одна и та же строчка кода даёт разное:
Windows: data\2026\cpi.csv
macOS/Linux: data/2026/cpi.csv
Именно поэтому пути не склеивают строками. "data" + "/" + "2026" в Windows даст data/2026 — обычно сработает, но перестанет ровно в тот день, когда путь попадёт туда, где косую ждут обратную.
Образ. Адрес на конверте. Вы пишете улицу и дом, а не рисуете карту проезда: почта сама знает, как повернуть.
Папка программы, а не папка запуска
HERE = Path(__file__).parent
Path("dannye.csv") — путь относительно текущей папки, а текущая папка — та, из которой запустили программу, а не та, где она лежит. Запустите её из соседней директории — и файл не найдётся, хотя лежит рядом с программой.
__file__ — путь к самому файлу программы, .parent — его папка. Дальше от неё строят всё остальное, и программа перестаёт зависеть от того, откуда её позвали.
with: файл закрывается сам
with data.open(encoding="utf-8") as source:
for line in source:
with закрывает файл на выходе из блока — и когда всё прошло гладко, и когда внутри случилась ошибка. Это тот самый случай, ради которого в прошлом уроке не понадобился finally.
Забытый close — не безобидная мелочь: записанное может остаться в буфере и не доехать до диска, а число открытых файлов у процесса ограничено. with снимает оба вопроса.
Файл, открытый так, читается построчно: for line in source не грузит его в память целиком. Для гигабайтной выгрузки это разница между «работает» и «не работает» — к ней мы вернёмся в уроке про генераторы.
encoding="utf-8" — не украшение
На диске лежат байты. Кодировка — договор о том, какие байты какими буквами считать. Не укажете — Python возьмёт кодировку системы, а она разная: на macOS и в современном Linux это UTF-8, в Windows исторически нет.
Результат чужой кодировки знаком всем: нет данных вместо «нет данных». Хуже, что программа при этом не падает — она честно читает то, что ей сказали читать.
Начиная с Python 3.15 UTF-8 станет режимом по умолчанию (PEP 686). Писать encoding="utf-8" всё равно стоит: ваш код будут запускать и на более старых версиях, и явно названный договор читается лучше умолчания.
Быстрое чтение и запись целиком
data.write_text("\n".join(rows) + "\n", encoding="utf-8")
report.read_text(encoding="utf-8")
Для небольшого файла не нужен ни open, ни цикл: write_text и read_text делают всё одной строкой. Граница простая — помещается ли файл в память. Отчёт на сотню строк помещается; выгрузка на гигабайт нет, и её читают построчно.
Заметьте + "\n" в конце: текстовый файл принято заканчивать переводом строки. Без него последняя строка «слипается» с тем, что допишут следом.
Что ещё умеет Path
Из вывода: .name — имя с расширением, .stem — без него, .suffix — расширение, .exists() — есть ли файл, .stat().st_size — размер в байтах. Рядом живут .parent, .mkdir(parents=True, exist_ok=True) для создания папок, .glob("*.csv") для перебора файлов по маске и .unlink() для удаления.
Всё это — один объект вместо десятка функций из старого модуля os.path. Если встретите в чужом коде os.path.join(a, b) — это то же самое, написанное до pathlib.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Почему путь строят через
Path, а не склейкой строк? - Чем
Path(__file__).parentотличается от текущей папки? - Что произойдёт, если открыть файл без
encoding, а он записан в UTF-8?
Разминка
Три коротких шага перед заданием: предсказать, дописать, починить. Ответы — в конце урока, но сначала ответьте сами.
1. Предскажите. Что напечатает эта программа? Байты считать не надо — скажите, из чего складывается число.
from pathlib import Path
HERE = Path(__file__).parent
data = HERE / "dannye.txt"
data.write_text("520\n546\n", encoding="utf-8")
print(data.name, data.suffix, data.stat().st_size)
data.unlink()
2. Заполните пропуск. Вместо ... постройте путь к файлу dannye.csv рядом с программой.
from pathlib import Path
HERE = Path(__file__).parent
data = ...
data.write_text("8.0\n", encoding="utf-8")
print(data.exists(), data.name)
data.unlink()
3. Почините. Программа печатает крякозябры вместо русских букв, хотя файл записала сама. Найдите строку, в которой нарушен договор.
from pathlib import Path
HERE = Path(__file__).parent
data = HERE / "dannye.txt"
data.write_text("нет данных\n", encoding="utf-8")
print(data.read_text(encoding="cp1251"))
data.unlink()
Задание
Обязательное. Дано:
rows = ["январь;520", "февраль;546,5", "март;нет цены", "апрель;498"]
Постройте пути от Path(__file__).parent. Запишите строки в файл ceny.csv с encoding="utf-8" и напечатайте его имя и размер в байтах. Прочитайте файл построчно через with, разберите каждую строку по ; (запятая в числе считается точкой), негодные месяцы соберите отдельно. Напечатайте отчёт по годным с двумя знаками, потом список пропущенных, а в конце уберите оба файла за собой.
Ожидаемый вывод:
записано: ceny.csv, 80 байт
январь: 520.00
февраль: 546.50
апрель: 498.00
пропущено: март
убрано, файлов рядом: 0
Готово, когда: вывод совпадает построчно; ни один путь не собран склейкой строк; у каждого чтения и каждой записи названа кодировка; после работы рядом с программой не осталось учебных файлов.
На своих данных. Напишите программу, которая рядом с собой создаёт файл со своими числами (год и значение через точку с запятой), читает его построчно через with и encoding="utf-8", разбирает строки и пишет отчёт во второй файл. Пути стройте от Path(__file__).parent.
Всё это собрано в step-4 — сверьтесь после того, как напишете сами.
По желанию.
- Запустите программу из другой папки (
python путь/к/программе.py) и убедитесь, что она всё равно находит свой файл. - Уберите
encodingиз чтения и запуститеPYTHONUTF8=0 python …, чтобы увидеть, что бывает. - Соберите список всех
*.csvрядом с программой черезglobи напечатайте их размеры.
Куда это встанет в проекте
Сводка получает диск. Данные, которые она забрала из сети, ложатся в файл рядом с программой, отчёт пишется вторым файлом, а пропущенные строки — те самые, что вчера печатались на экран, — теперь можно сложить в журнал.
Долги. Мы пишем поверх старого файла: если программа упадёт на середине записи, от прежних данных не останется ничего. Настоящая запись идёт во временный файл, а потом переименовывается на место — до этого дойдём, когда сводка начнёт работать по расписанию.
Ответы
Показать ответы
На вопросы
- Потому что разделитель пути у систем разный, и
Pathподставляет нужный сам. Склейка строк даёт путь, который работает у автора и ломается у читателя. - Текущая папка — та, из которой запустили программу; она меняется от запуска к запуску.
Path(__file__).parent— папка, где лежит сам файл программы, и она не зависит от того, откуда её позвали. - Python возьмёт кодировку системы. На macOS и в современном Linux это UTF-8 и всё совпадёт, а в Windows буквы приедут искажёнными — причём без ошибки, поэтому заметит это только человек.
К разминке
dannye.txt .txt 8. Размер — это байты на диске: восемь символов520\n546\n, и каждый из них в UTF-8 занимает один байт.
dannye.txt .txt 8
HERE / "dannye.csv". Косая черта уPathсоединяет, а не делит, и разделитель подставляет система.
from pathlib import Path
HERE = Path(__file__).parent
data = HERE / "dannye.csv"
data.write_text("8.0\n", encoding="utf-8")
print(data.exists(), data.name)
data.unlink()
True dannye.csv
- Файл записан в UTF-8, а прочитан как
cp1251: те же байты, другой договор — и программа не падает, потому что для неё это законные буквы. Читают тем же, чем писали:
from pathlib import Path
HERE = Path(__file__).parent
data = HERE / "dannye.txt"
data.write_text("нет данных\n", encoding="utf-8")
print(data.read_text(encoding="utf-8"))
data.unlink()
нет данных
Источники
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.