Это не совсем обычный пост по «питону». Здесь мы не только решаем частую проблему при работе с путями и файлами в Python, объясняя, как это сделать максимально правильно. Здесь мы также попытаемся рассказать, как мыслит опытный программист, наглядно покажем, как постепенно он дорабатывает свой код. Увидев и поняв, как это работает, вы получите возможность значительно поднять свой профессиональный уровень. Не верите? Прочитайте и попробуйте!
Недавно во время работы над проектом коллега спросил меня, можно ли в Python вывести список содержимого дисков. Конечно, можно. Более того, поскольку это совсем не сложно, я хотел бы рассмотреть этот случай подробно, чтобы проиллюстрировать лучшие практики, рекомендуемые для работы с путями к дискам.
Поверьте, это не голая теория — подобные вещи очень часто встречаются в реальной жизни. Давайте посмотрим, как эти проблемы решать правильно.
Предполагая, что вы хотите точно получить листинг файлов от конкретного пути, начнем с выбора пользовательского каталога в системе Windows 10:
path_dir: str = "C:\Users\sselt\Documents\blog_demo"
Переменные, назначенные при выполнении, немедленно вызывают ошибку:
SyntaxError: (unicode error) 'unicodeescape' codec can't decode bytes in position 2-3: truncated \UXXXXXXXX escape
Интерпретатор не понимает последовательность символов \U
, так как она инициирует символы Unicode аналогичной последовательности. Эта распространенная проблема, которую постоянно гуглят тысячи начинающих питонистов, возникает потому, что в системе Windows в качестве разделителя путей используется обратная косая черта «\» (бэкслеш), а в Linux — обычная косая черта «/» (слеш).
К сожалению, поскольку разделитель Windows также является инициатором для различных специальных символов или escape-последовательностью в Unicode, это явно все запутывает и сбивает с толку. Один и тот же код работает на одних системах и творит чудеса на вторых, хотя внешне все выглядит одинаково.
Так же, как мы не ожидаем в ближайшее время согласованности в использовании десятичных разделителей в разных странах, наш единственный выбор — это одно из трех решений.
Просто избегайте разделителя Windows и вместо этого пишите путь, используя только разделители Linux:
path_dir: str = "C:/Users/sselt/Documents/blog_demo"
После этого интерпретатор распознает правильный путь, считая, что это Linux-система. Как минимум на серверных системах это должно работать как надо… Но универсальностью здесь и не пахнет.
Давайте не будем останавливаться, и покажем, до чего порой додумываются некоторые новички, решая эту проблему.
Используйте экранирующие последовательности — в интернете полно подобных советов.
path_dir: str = "C:\\Users\sselt\Documents\\blog_demo"
Помимо неразборчивости такого кода, меня беспокоит то, что не нужно использовать экранирующие последовательности в каждой комбинации символов-разделителей, а только перед «U
» и «b
».
Слабо держать все это под контролем (особенно если ваша программа размером на несколько десяток тысяч строк кода)?
Используйте необработанные строки с «r
» в качестве префикса, чтобы указать, что специальные символы не должны оцениваться.
path_dir: str = r"C:\Users\sselt\Documents\blog_demo"
Как тебе такое решение, Маск? Все просто и универсально. Попробуйте найти в интернете аналог моего решения — это будет сложно, поверьте.
Вернемся к нашей типичной задаче — перечислить все элементы в папке. Мы уже знаем путь, а также то, как его правильно записать без косяков.
Простая команда os.listdir
перечисляет все строки, то есть только имена файлов по пути. Здесь и во всех других примерах я использую подсказки типов для дополнительного документирования кода, что сразу приучает нас писать качественно. Этот синтаксис стал доступен, начиная с Python 3.5.
import os from typing import List path_dir: str = r"C:\Users\sselt\Documents\blog_demo" content_dir: List[str] = os.listdir(path_dir)
Отображение файлов в порядке, но меня больше интересует статистика файлов, для которой у нас есть os.stat
.
Продираемся дальше в решение вроде бы простой и частой задачи.
Чтобы передать путь к файлу, мы должны сначала объединить имя файла и путь. Я часто встречал следующие конструкции в гугле и даже использовал их, когда был молодой. Например, распространены такие варианты:
path_file: str = path_dir + "/" + filename path_file: str = path_dir + "\\" + filename path_file: str = "{}/{}".format(path_dir, filename) path_file: str = f"{path_dir}/{filename}"
Первые два варианта сверху — отвратительны, потому что они конкатенируют строки со знаком «+
», который в новом Python не нужен.
Особенно все это отвратительно, потому что в Windows нужен двойной разделитель, иначе он будет оценен как управляющая последовательность для закрывающей кавычки. Что создает непредсказуемый «оопс» в процессе эксплуатации такого кода.
Нижние два варианта несколько лучше, поскольку они используют форматирование строк, но они все равно не решают проблему системной зависимости. Если я применю результат под Windows, то получу функциональный, но непоследовательный путь со смесью разделителей.
Смотрите сами:
filename = "some_file" print("{}/{}".format(path_dir, filename)) ...: 'C:\\Users\\sselt\\Documents\\blog_demo/some_file'
Решение из Python — os.sep
или os.path.sep
. Оба возвращают разделитель путей соответствующей системы. Функционально они идентичны, но второй, более явный синтаксис сразу показывает задействованный разделитель.
Это означает, что можно написать так:
path_file = "{}{}{}".format(path_dir, os.sep, filename)
Результат будет лучше, но за счет сложного кода, если вы хотите объединить несколько сегментов пути. Поэтому принято объединять элементы пути через конкатенацию строк. Это еще короче и универсальнее:
path_file = os.sep.join([path_dir, filename])
В качестве домашнего задания погуглите триллионов решений в гугле для этой задачи и сравните с нашим подходом — теперь вы сразу увидите косяки во множестве чужих решений.
Перейдем к каталогу:
for filename in os.listdir(path_dir): path_file = os.sep.join([path_dir, filename]) print(os.stat(path_file))
Один из результатов — st_atime
— время последнего обращения к файлу, st_mtime
— время последней модификации и st_ctime
— время создания. Кроме того, st_size
дает размер файла в байтах. В данный момент мне нужны только размер и дата последней модификации, поэтому я решил сохранить простой формат списка.
import os from typing import List, Tuple filesurvey: List[Tuple] = [] content_dir: List[str] = os.listdir(path_dir) for filename in content_dir: path_file = os.sep.join([path_dir, filename]) stats = os.stat(path_file) filesurvey.append((path_dir, filename, stats.st_mtime, stats.st_size))
Полученный результат поначалу кажется удовлетворительным, большинство на этом бы и остановилось. Но почесав как следует репу, мы осознаем, что возникают две новые проблемы. Listdir
не делает различий между файлами и папками, обращается только к уровню папок и не обрабатывает вложенные папки.
Следовательно, нам нужна рекурсивная функция, которая различает файлы и папки и спускается вниз для сканирования низких уровней. os.path.isdir
как раз проверяет, есть ли папка ниже пути.
def collect_fileinfos(path_directory: str, filesurvey: List[Tuple]): content_dir: List[str] = os.listdir(path_directory) for filename in content_dir: path_file = os.sep.join([path_directory, filename]) if os.path.isdir(path_file): collect_fileinfos(path_file, filesurvey) else: stats = os.stat(path_file) filesurvey.append((path_directory, filename, stats.st_mtime, stats.st_size)) filesurvey: List[Tuple] = [] collect_fileinfos(path_dir, filesurvey)
Готово! Мы решили проблему менее чем за 10 строк. Проблема только в том, что если мы запустим такой скрипт на большой файловой системе, то мы получим большой (нет, ОГРОМНЫЙ) монотонный вывод из строчек. Как все это понять и переварить в реальном мире? Я сразу обещал, что мы решаем не академические задачки, а разбираем решения из реального мира.
Поскольку я планировал иметь filesurvey
как список кортежей, я могу легко перенести результат во фрейм данных Pandas и проанализировать его там для анализа итогов поиска, сохраненных в папках, и т.д. Pandas позволит найти и вычленить любые закономерности, которые бы вы хотели узнать, с минимальными усилиями.
import pandas as pd df: pd.DataFrame = pd.DataFrame(filesurvey, columns=('path_directory', 'filename', 'st_mtime', 'st_size'))
… но, к сожалению, и это не самая лучшая практика.
Я знаю, этот пост сразу обещал решать проблемы с помощью только лучших практик. Поэтому придется снова все переписать.
Далее я собираюсь снова обратиться к этому сценарию и решить его по-настоящему элегантно.
Выше мы использовали рекурсивную функцию для решения, состоящего менее чем из 10 строк, для сканирования папок и обеспечения возможности оценки файлов по дате модификации и размеру. Теперь я собираюсь несколько поднять планку для этого примера, показав лучшие альтернативы.
Старое вино в новых бутылках? Решение предыдущего примера с помощью склеивания путей было следующим:
path_file = os.sep.join([path_dir, filename])
Преимущество этого способа в том, что решение не зависит от операционной системы, и не нужно объединять строки знаком «+» или как-то париться с их форматированием.
Однако это чревато ошибками, поскольку можно случайно или по ошибке определить путь к каталогу с закрывающим разделителем путей.
path_dir: str = r"C:/Users/sselt/Documents/blog_demo/" # abschließender Trenner filename: str = "some_file" path_file = os.sep.join([path_dir, filename]) # C:/Users/sselt/Documents/blog_demo/\some_file
Хотя в этом примере показан работающий код, тренированный взгляд бывалого кодера сразу видит, что неправильный разделитель приводит к ошибке при вызове пути. Такие ошибки могут возникать, когда пользователи управляют путями в конфигурационных файлах, вдали от кода, не обращая внимания на общее соглашение.
Начиная с Python 3.4, появилось лучшее решение — модуль pathlib
. Он обрабатывает функции файлов и папок модуля os в Python с помощью объектно-ориентированного подхода.
Напомню старый вариант:
import os path = "C:/Users/sselt/Documents/blog_demo/" os.path.isdir(path) os.path.isfile(path) os.path.getsize(path)
А вот новая альтернатива:
from pathlib import Path path: Path = Path("C:/Users/sselt/Documents/blog_demo/") path.is_dir() path.is_file() path.stat().st_size
Оба варианта дают абсолютно одинаковый результат. Так почему же второй вариант намного лучше? Давайте пораскинем мозгами.
Вызовы в основном объектно-ориентированы, и это может быть или не быть вашим предпочтением — но мне так нравится гораздо больше. Здесь у нас есть объект, например, определение пути, у которого есть атрибуты и методы. Это правильный подход для энтерпрайз-программирования.
Однако пример, примененный здесь для перегрузки операторов, более интересен:
filename: Path = Path("some_file.txt") path: Path = Path("C:/Users/sselt/Documents/blog_demo") print( path / filename ) # C:\Users\sselt\Documents\blog_demo\some_file.txt
Сначала разделение на два пути кажется недопустимым. Однако объект path был просто перегружен таким образом, что функционирует как объединяющий путь.
В дополнение к этому синтаксическому сахару, объекты path
будут перехватывать и другие типичные ошибки:
filename: Path = Path("some_file.txt") path: Path = Path("C:/Users/sselt/Documents/blog_demo/") path: Path = Path("C:/Users/sselt/Documents/blog_demo//") path: Path = Path("C:\\Users/sselt\\Documents/blog_demo") print(path/filename) # C:\Users\sselt\Documents\blog_demo\some_file.txt
Этот вариант не только красивее, но и устойчивее к ложным вводам. В дополнение к другим преимуществам код также не зависит от операционной системы. Определяется только общий объект пути, который в системе Windows выглядит как WindowsPath, а в системе Linux — как PosixPath
.
Большинство функций, которые обычно ожидают строку в качестве пути, могут работать непосредственно с путем. В редких случаях вам может понадобиться разрешить объект просто с помощью str(Path)
.
В моем последнем решении я использовал os.listdir, os.path.isdir
и рекурсивную функцию для итерации по дереву путей и различения папок и файлов.
Но os.walk
предлагает лучшее решение. Этот метод создает не список, а итератор, который можно вызывать построчно. Результат содержит соответствующий путь к папке и список всех файлов данных в пределах этого пути. Все это происходит рекурсивно, так что вы получаете все файлы одним вызовом. Не правда ли круто?
Если вы объедините две вышеупомянутые техники, вы получите более простое решение, полностью независимое от ОС, более устойчивое к непоследовательным форматам путей и свободное от явных рекурсий:
filesurvey = [] for row in os.walk(path): # row beinhaltet jeweils einen Ordnerinhalt for filename in row[2]: # row[2] ist ein tupel aus Dateinamen full_path: Path = Path(row[0]) / Path(filename) # row[0] ist der Ordnerpfad filesurvey.append([path, filename, full_path.stat().st_mtime, full_path.stat().st_size])
Я предлагаю остановиться. Хотя приведенное решение типичной задачи вполне рабочее и достаточно устойчиыо к превратностям судьбы, его можно продолжать совершенствовать и дальше. Я бы посоветовал программистам находить некий разумный баланс между перфекционизмом и разумной стабильностью, на которой пора остановиться.
Если вы можете дополнить этот пример лучшей практикой, не стесняйтесь, напишите коммент под этим постом.
И в заключение самое главное — этот пост не про сканирование файлов и папок, хотя именно этим мы и занимались. Помимо решения задачи, я попытался показать, КАК ДУМАЕТ программист, когда развивает свое решение, которое, как правило, улучшает последовательными итерациями, постепенно достигая некоего приемлемого уровня стабильности.
Это приходит с опытом, но теперь увидев как это работает, вы посмотрите на чужой код из интернета совсем другими глазами — глазами человека, который учится предугадывать дефекты и видеть неявные слабые места. А не просто бездумно копипастит чужие примеры, «потому что они ведь работают». Возможно, такой подход усложнит вам жизнь и замедлит в плане работы, но зато именно он и даст толчок к вашему профессиональному росту как программиста.
Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…
Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…
Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…
В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…
Скрипт (англ. — сценарий), — это небольшая программа, как правило, для веб-интерфейса, выполняющая определенную задачу.…
Дедлайн (от англ. deadline — «крайний срок») — это конечная дата стачи проекта или задачи…