Shanraq.org Shanraq.org
Файлы и pathlib: путь, который не ломается на чужой машине
IT

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.

Карта урока

Карта урока: путь, кодировка и запись рядом

Скажите своими словами

Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.

  1. Почему путь строят через Path, а не склейкой строк?
  2. Чем Path(__file__).parent отличается от текущей папки?
  3. Что произойдёт, если открыть файл без 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 и напечатайте их размеры.

Куда это встанет в проекте

Сводка получает диск. Данные, которые она забрала из сети, ложатся в файл рядом с программой, отчёт пишется вторым файлом, а пропущенные строки — те самые, что вчера печатались на экран, — теперь можно сложить в журнал.

Долги. Мы пишем поверх старого файла: если программа упадёт на середине записи, от прежних данных не останется ничего. Настоящая запись идёт во временный файл, а потом переименовывается на место — до этого дойдём, когда сводка начнёт работать по расписанию.

Ответы

Показать ответы

На вопросы

  1. Потому что разделитель пути у систем разный, и Path подставляет нужный сам. Склейка строк даёт путь, который работает у автора и ломается у читателя.
  2. Текущая папка — та, из которой запустили программу; она меняется от запуска к запуску. Path(__file__).parent — папка, где лежит сам файл программы, и она не зависит от того, откуда её позвали.
  3. Python возьмёт кодировку системы. На macOS и в современном Linux это UTF-8 и всё совпадёт, а в Windows буквы приедут искажёнными — причём без ошибки, поэтому заметит это только человек.

К разминке

  1. dannye.txt .txt 8. Размер — это байты на диске: восемь символов 520\n546\n, и каждый из них в UTF-8 занимает один байт.
dannye.txt .txt 8
  1. 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
  1. Файл записан в 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()
нет данных

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

Сначала решите и запустите в VS Code — редактор покажет ошибку на месте. Готовое решение вставьте сюда. Проверяет модель: она укажет на ошибку, но не даст готовый ответ.

Чтобы проверить, нужно войти. Войти

Комментарии (0)

Пока нет комментариев. Будьте первым.